#========================================================================================#
#	Laboratory 11
#
# Deterministic CHAOS and the course project.
#
# Author: Niall Palfreyman, 23/03/2022
#========================================================================================#
[
	Activity(
		"""
		This is the last laboratory in this course, and it presents the assessed project for
		the course. In this laboratory, you will implement a program to demonstrate chaos in a
		gravitational system containing three bodies of equal mass in two dimensions. You will use
		Julia to analyse the execution of a complex simulation program, and adapt this program to
		use Runge-Kutta integration to (a) simulate 3-body motion and (b) evaluate its chaoticity.
		You will present your methods and results in ONE deliverable: A complete Julia module with
		associated demonstration code that presents a pedagogically pleasing description of the
		n-body problem, its solution using Runge-Kutta-2 integration and an analysis of the
		dynamics of your chosen system. This deliverable will also contain a title, information on
		authors and references, and diagrams.
		""",
		"",
		x -> true
	),
	Activity(
		"""
		In your project, you will investigate DETERMINISTIC CHAOS. Chaos is important for biology
		because it is the source of the spontaneity that we observe in living organisms. To see
		how chaos works, first define a Julia version of the Mathematica function nestlist():

			function nestlist( f::Function, x0, n::Integer)
				(n ≤ 0) ? [x0] : begin
					list = Vector{typeof(x0)}(undef, n+1)
					list[1] = x0
					for i in 1:n
						list[i+1] = f(list[i])
					end
					list
				end
			end

		Experiment with nestlist(). For example, what is the result of the following invocation?

			nestlist(x->2x,3,5)
		""",
		"",
		x -> x == [3,6,12,24,48,96]
	),
	Activity(
		"""
		nestlist() applies the function f repeatedly to the initial value x0, creating a list of
		the values that it generates in this way. You can see how this might be useful if we want
		to create a simulation of timesteps that repeat themselves over time. We'll do that now ...

		Imagine a population of wasps living on an island with renewable but limited resources.
		They can fill the island's carrying capacity (population x = 1), they can die out (x = 0),
		or else their population can have any value in the range 0 ≤ x ≤ 1. Also, they all die each
		winter, so their population takes on a new value each year: [x0,x1,x2,...,xn].

		For specific birthrate r, the wasp population's growth is governed by this logistic equation:

			x[i+1] = Λ(r)(x[i]), where
			Λ(r) is the function: (x -> r x (1-x))

		Implement this model now and experiment with it, then tell me the population two years
		after an initial population of x0 = 0.3, if r = 0.5
		""",
		"Λ(r) = (x -> r .* x .* (1 .- x))",
		x -> (0.04698 < x < 0.04699)
	),
	Activity(
		"""
		Use nestlist() to create a list of 5 simulation steps of the wasp population, starting from
		an initial value of 0.3 and using specific growth rate r = 0.5. You will see that the
		population x is tending towards a particular limiting value - what is that limit value?
		""",
		"nestlist(Λ(1),0.3,5)",
		x -> x==0
	),
	Activity(
		"""
		You should now be able to do the following: Use nestlist() to create a list of 20
		simulation steps of the wasp population with x0=0.3, r=0.9, then plot them in a graph.
		What is the limit point of this sequence?
		""",
		"lines(nestlist(Λ(0.9),0.3,20))",
		x -> x==0
	),
	Activity(
		"""
		OK, now we have the equipment, we can investigate the onset of chaos. We will slowly
		increase the value of the specific birthrate r to discover how this affects the
		developmental motion of the wasp population. Use the initial value x0=0.3 for all of the
		folloiwng excercises until I tell you otherwise.

		Just to make our language clear: a LIMIT POINT of the wasp population's motion is any
		value of the population that stays the same from one generation to the next: Λ(r)(x) == x.

		What was the limit point of the motion Λ(0.9)?
		""",
		"",
		x -> x==0
	),
	Activity(
		"""
		What is the approximate value of the limit point of the motion Λ(1.1)?
		""",
		"",
		x -> 0.07<x<0.1
	),
	Activity(
		"""
		What is the EXACT value of the limit point of the motion Λ(1.5)?
		""",
		"You can work it out for yourself by solving the equation Λ(r)(x) == x, or: r x (1-x) = x",
		x -> x == 1//3
	),
	Activity(
		"""
		Use the technique from the hint in the previous activity to calculate the limit point of
		the motion Λ(2), then check this value using your simulator:
		""",
		"",
		x -> x==0.5
	),
	Activity(
		"""
		Now investigate the motion Λ(2.1). In which direction does the population change from x[6]
		to x[7]: positive (+) or negative (-)?
		""",
		"",
		x -> (x == -)
	),
	Activity(
		"""
		This is interesting 00! Up to now, the development of the wasps has been monotone: the
		population has only either shrunk or grown. But now it grows above the value, then drops
		back down again. Let's investigate this further: find the limit point of the motion
		Λ(2.5) both by calculating and by simulating:
		""",
		"",
		x -> x==0.6
	),
	Activity(
		"""
		Notice that now we have an oscillating motion that dies away as x comes to rest at the
		limit point. Try out various values of r between 2.5 and 2.95. Does the oscillation
		still die away?
		""",
		"",
		x -> occursin("y",lowercase(x))
	),
	Activity(
		"""
		Now find the limiting motion of the population for Λ(3). Does the oscillation die away?
		""",
		"",
		x -> occursin("n",lowercase(x))
	),
	Activity(
		"""
		It now no longer makes sense to speak of a limit VALUE, since the motion oscillates
		forever around the value 2/3. Instead, we speak of a limit CYCLE: the motion Λ(3) displays
		a limit cycle around the value 2/3. "cycle" means the value oscillates forever, and
		"limit" means that it will converge to this cycle, no matter where we start the motion.
		Check this for yourself by changing the value of x0. Does the motion always converge on
		a cycle around 2/3?
		""",
		"",
		x -> occursin("y",lowercase(x))
	),
	Activity(
		"""
		How many oscillatory periods are contained within one complete limit cycle of the motion
		Λ(3)? That is, how many times does the population wobble up and down within ONE cycle? If
		you have difficulty understanding exactly what I am asking, try looking at the hint for
		this activity.
		""",
		"We call this limit cycle a \"period-1 limit-cycle\"!",
		x -> x==1
	),
	Activity(
		"""
		Let's investigate further. Look at the limit-cycle of the motion Λ(3.2) (You may want
		to extend the simulation's duration to 50). How many periods are in this limit cycle?
		""",
		"The number of periods should not (yet) have changed",
		x -> x==1
	),
	Activity(
		"""
		How many periods are in the limit-cycle of the motion Λ(3.45)? That is, how many complete
		oscillations does the population make before the motion repeats itself?
		""",
		"Count very carefully: This should be a period-2 limit-cycle!",
		x -> x==2
	),
	Activity(
		"""
		How many periods are in the limit-cycle of the motion Λ(3.55)? By now, things will move
		quite quickly and the wasp population will need a while to stabilise towards the limit-
		cycle. You may like to save time by using a line like this:

			lines(nestlist(Λ(3.55),0.5,5000)[end-40:end])
		""",
		"Look very carefully!",
		x -> x==4
	),
	Activity(
		"""
		How many periods are in the limit-cycle of the motion Λ(3.567)?
		""",
		"Count ve-ery carefully - remembering to inspect the smaller oscillations as well!",
		x -> x==8
	),
	Activity(
		"""
		How many periods are in the limit-cycle of the motion Λ(3.5695)? You will find this
		one difficult to count, but it is actually a period-16 limit-cycle. If you can't
		distinguish the oscillations, don't worry - just go on to the next activity.
		""",
		"",
		x -> x==16
	),
	Activity(
		"""
		We have seen that as r increases, period-doubling occurs, so the motion takes longer
		and longer before it repeats. Now check out the motion Λ(3.7). How long does it take
		before this motion repeats itself? Or in other words: What is the period-length of
		this limit-cycle?
		""",
		"Find out how to write \"Infinity\" in Julia :)",
		x -> x==Inf
	),
	Activity(
		"""
		We have come a long way, and what we have learned is that some naturally occurring systems
		can enter a type of motion in which they require an infinite amount of time to repeat, and
		so we call them CHAOTIC. Notice that this phenomenon has nothing to do with randomness. The
		population of Vespula Island is perfectly deterministic - we can always predict what will
		happen in the next step, but ONLY by actually simulating it! We cannot calculate in advance
		what will happen after 1000 iterations without actually performing ALL of the 1000 steps
		that lead up to it.

		Actually, the situation is even worse than this. Use the following command to inspect the
		behaviour of Λ(3.7) for the different starting conditions x0 ∈ [0.5,0.50001,4.99999]

			lines(nestlist(Λ(3.7),0.5,5000)[end-40:end])

		Are these three graphs at all similar to each other?
		""",
		"",
		x -> occursin("n",lowercase(x))
	),
	Activity(
		"""
		So you see, chaotic motion meanns that even tinsey-tiny changes in the initial conditions
		of a chaotic system lead to completely different behaviour. So even if we measured the
		current wasp population, we could never be sure that we had done it sufficiently accurately
		to be ABSOLUTELY sure that we are accurately predicting the development of the system!

		So. What has all this to do with your course project? Henri Poincaré was the first to
		notice deterministic chaos. In 1908, he showed that the gravitational motion of three
		orbiting bodies cannot be solved exactly. Weather, dripping taps and driven pendula have a
		similar problem. We cannot predict the story of their future motion without simulation,
		which as you know is never precise! As an introduction to chaos in the three-body problem,
		please view the following video-clip now, and then proceed to the next activity:

		https://www.youtube.com/watch?v=LwkvO3t1b30&t=113s
		""",
		"",
		x -> true
	),
	Activity(
		"""
		The demo() function of the module NBodies creates an instance of the type NBody to simulate
		the motion of TWO bodies in ONE spatial dimension. Run it now: do the blue and red bodies
		orbit each other? Investigate this further by increasing the resolution of the simulation
		and of the figure. Try focussing your graphic on the time-snapshots around where the two
		bodies meet, and magnify the image. Do they orbit now? What exactly is happening here?
		This is the origin of chaotic motion!

		Check that the vector x returned by the method NBodies.simulate() has three levels: the
		elements of x span the individual time snapshots of the simulation; each snapshot contains
		a list of the bodies participating in the motion; and each body contains a list of that
		body's coordinates. What do the contents of p represent?
		""",
		"",
		x -> occursin("time",lowercase(x))
	),
	Activity(
		"""
		Software development is always driven by the needs of its CLIENT program - in our case,
		this is the function NBodies.demo(). As you work with demo(), think about what kinds of
		behaviour you as a user demand of the type NBody, and include that functionality in your
		NBodies module design. Your use-cases and newly developed design will form an important
		part of your deliverables for this project.

		Notice that the two bodies (blue and red) do not orbit around each other. Why not? It may
		help your thinking to consider the following question: Is energy conserved in this system?
		""",
		"Think about the bodies' positions and momenta during the simulation",
		x -> occursin("no",lowercase(x))
	),
	Activity(
		"""
		Be aware that I have not given much attention to the naming of variables - you will need to
		comment, reformat and document the module much better than I have! Doing this will help you
		to understand the matrix programming techniques I have used, but they are not easy! You
		will need to thoroughly analyse the code and use Help and internet search to look up
		EVERYTHING you do not yet understand. Here is how to do this ...

		First remember: Matrices work very naturally in Julia, so if a=[1 2;3 5] and b=[2 3;4 5],
		then a*b and a.*b will deliver very different results. First test this idea at the Julia
		prompt, and then tell me the value of a*b-a.*b :
		""",
		"",
		x -> x == [8 7;14 9]
	),
	Activity(
		"""
		The secret of matrix programming is this: Whenever you think you need a for-loop to
		manipulate some values, build them together instead into a matrix that does the job for you.
		For example define and test an anonymous version of the factorial function that requires no
		recursion or iteration, but uses the prod() function.
		""",
		"",
		x -> occursin("->prod(1:",replace(x," "=>""))
	),
	Activity(
		"""
		Analyse the internal helper function relpos(), which calculates the relative positions
		x[i]-x[j] between all pairs of particles. Instead of looping over the particles, I compute
		the relative positions more efficiently in a matrix operation. Find where relpos() is
		called, and notice that its argument is a Vector - for example [1,2].

		At the Julia prompt, define relpos() and then call relpos([1,2]). What does the result look
		like? What does relpos() do? At the Julia prompt, analyse the calls

			locations = [1,2]
			locPerBody = repeat(locations,1,length(locations))
			permutedims(locPerBody)

		Discuss your findings with others: How, exactly, does relPos() work?

		Now tell me the result of repeat([1,2],2,3)?
		""",
		"",
		x -> x==[1 1 1;2 2 2;1 1 1;2 2 2]
	),
	Activity(
		"""
		Set a breakpoint and use the Debugger to investigate how simulate() uses relpos() to
		calculate the gravitational forces acting between the different bodies in the system.
		Which variable is used to calculate Newton's inverse-square law that falls with
		increased distance between the sources?
		""",
		"",
		x -> x=="invSq"
	),
	Activity(
		"""
		OK, now we can start on your actual project. First, extend my simulator to 2 bodies in
		2 dimensions and get the two bodies to orbit each other. This is your first major
		success - well done!

		Remember to keep your solution very neat, so that others can understand what you have
		done by reading your code.
		""",
		"Just move on when you have completed this activity",
		x -> true
	),
	Activity(
		"""
		Notice that your two bodies spiral outwards on their orbits because of my inaccurate
		simulator. Improve my integration method to achieve (practically) closed orbits.

		What is the name of the integration technique you are using?
		""",
		"",
		x -> occursin("runge-kutta",lowercase(x))
	),
	Activity(
		"""
		Now introduce a third body into your system (use the same mass for all three bodies). Try
		to create a situation in which the three bodies circle around each other without any of
		them getting thrown out of the system to infinity. Ensure that your system is conserving
		energy; that is, the three bodies taken together should neither gain nor lose energy!
		""",
		"Just move on when you have completed this activity",
		x -> true
	),
	Activity(
		"""
		You will need to find a way of testing your simulation for chaotic motion. There are
		several ways to do this - some are easy, some more difficult; some are approximate, some
		are very accurate. Here is a paper containing an accurate method of detecting chaos:

		https://www.nature.com/articles/s42003-019-0715-9.pdf

		Alternatively, in chaotic dynamics, small differences in initial conditions grow
		exponentially over time in the short term. The easiest way to test for exponential growth
		is by plotting a logarithm graph over time to see if it is linear. Decide how you want to
		measure chaoticity in your system.
		""",
		"",
		x -> true
	),
	Activity(
		"""
		Your project deliverable is a Julia module that must fulfil the following constraints:
			- the demonstration program MUST execute correctly within its own folder;
			- simulates the motion of three or more gravitational bodies;
			- executes an aesthetically pleasing chaotic trajectory;
			- demonstrates that the motion conserves energy;
			- demonstrates that the motion is indeed chaotic;
			- displays explanatory text so that anyone can understand it WITHOUT studying the code!
			- the code must be neat, well documented and easily readable and comprehensible.
		""",
		"Good luck!!!",
		x -> true
	),
]


"""
function logmean(aL,aR)
"""

function logmean(aL,aR)
    return logmean(aL,aR,log(aL),log(aR))
end

"""
function logmean(aL,aR,logL,logR)
    Compute logarithmic mean using pre-computed log values
"""

function logmean(aL,aR,logL,logR)

    "from: Entropy stable num. approx. for the isothermal and polytropic Euler"
    # assumes the specific value γ = 1.4
    da = aR-aL;
    aavg = .5*(aR+aL);
    f = da/aavg;
    v = f^2;
    if abs(f)<1e-4
        return aavg*(1 + v*(-.2-v*(.0512 - v*0.026038857142857)))
    else
        return -da/(logL-logR)
    end
end


using Plots
using ForwardDiff
using LinearAlgebra

a = 1.0
b = 100.0

f(x) = (a - x[1])^2.0 + b * (x[2] - x[1]^2.0)^2.0
f(x, y) = f([x; y])
∇f(x) = ForwardDiff.gradient(f, x)
∇²f(x) = ForwardDiff.hessian(f, x)

n = 1000
x_limits = range(-1.0, stop = 1.0, length = n)
y_limits = range(-1.0, stop = 1.0, length = n)

x = zeros(2)

function solve(x;
        max_iter = 100,
        visualize = true)
    visualize && (plt = Plots.contour(x_limits, y_limits, f))
    visualize && (plt = plot!((x[1], x[2]), markershape = :circle, color = :cyan, outline = :cyan))

    for i = 1:max_iter
        grad = ∇f(x)
        println("x:  $x")
        println("grad norm: $(norm(grad))")
        norm(grad) < 1.0e-3 && break

        hess = ∇²f(x)
        # Δ = hess \ grad
        Δ = grad

        α = 1.0
        iter = 1
        while norm(∇f(x - α * Δ)) > norm(grad) && iter < 100
            α *= 0.5
            iter += 1
        end

        x .-= α * Δ

        if i % 10 == 0 && visualize
            plt = plot!((x[1], x[2]),
                markershape = :circle, color = :cyan, outline = :cyan,
                legend = false)
            display(plt)
        end
    end
    print("iters: $i")
    return x
end

solve([0.0, -1.0], max_iter = 1000)


#normalization factor
abstract type YLMNorm{T} end

struct Schmidt{T} <: YLMNorm{T}; end
struct Laplace{T} <: YLMNorm{T}; end
struct Nonorm{T} <: YLMNorm{T}; end
struct Full{T} <: YLMNorm{T}; end

function ylmcoeff(N::Schmidt{T},l::Int64, m::Int64) where T

	k = one(T)
	for i in (l-m+1):(l+m)
	  k *= i
	end
	return sqrt(1/k)
end

function ylmcoeff(N::Laplace{T}, l::Int64, m::Int64) where T

  k = one(T)
  for i in (l-m+1):(l+m)
    k *= i
  end

  return sqrt((2*l+1) / (4*T(pi)*k))
end

function ylmcoeff(N::Full{T}, l::Int64, m::Int64) where T
	fac = m==0 ? one(T) : sqrt(2*one(T))
	return ylmcoeff(Laplace{T}(),l,m)*sqrt(4T(pi))*fac
end

function ylmcoeff(N::Nonorm{T}, l::Int64, m::Int64) where T
 return one(T)
end

function cossinpoly(m::Int64, x::Variable, y::Variable)

  sum = zero(x*y)
  for j in 0:div(m,2)
	  k = 2j
    sum += ((-1)^j)*binomial(m, k)*y^k*x^(m-k)
  end
  return sum
end

function sinsinpoly(m::Int64, x::Variable, y::Variable)

  sum = zero(x*y)
  for j in 0:div((m-1),2)
	  k = 2j+1
    sum += ((-1)^j)*binomial(m, k)*y^k*x^(m-k)
  end
  return sum
end

# #legendre polynomial:
# P(l::BigInt,x) = differentiate((x^2 - 1)^l,x,l)
#
# #associated legendre polynomial (without sin(θ)^m)
# P(l::BigInt,m::BigInt,x) = 1//(big(2)^l*factorial(l))*differentiate(P(l,x),x,m)

"""
    ylm(l::Int64, m::Int64, x::Variable, y::Variable, z::Variable)
*Description:*  Calculation of the spherical harmonic for a given order (l,m) in Cartesian coordinates\\

*Input:*  `l`       - Degree of the spherical harmonic\\
          `m`       - Order of the spherical harmonic\\
          `x, y, z` - Cartesian coordinates\\

*Output:*  Spherical harmonic polynomial
"""
function ylm(l::Int64, m::Int64, x::Variable, y::Variable, z::Variable;
			 norm::YLMNorm{T}=Laplace{Float64}(),real=true) where T

	if abs(m) > l
	throw(DomainError(m,"-l <= m <= l expected, but m = $m and l = $l."))
	end

	p = (z^2 - 1)^l

	for i = 1:l+abs(m)
	  c = i <= l ? 1/(2one(T)*i) : one(T)
	  p = c*differentiate(p, z)
	end

	p *= (-1)^m

	if real
		if m > 0
			out = ylmcoeff(norm, l, m)*cossinpoly(m,x,y)*p
			if norm != Nonorm{T}()
				out *= sqrt(2one(T))
			end
			return out
		elseif m < 0
			out = ylmcoeff(norm, l, abs(m))*sinsinpoly(abs(m),x,y)*p
			if norm != Nonorm{T}()
				out *= sqrt(2one(T))
			end
			return out
		else
			return ylmcoeff(norm, l, 0)*p
		end
	else
		return ylmcoeff(norm, l, m)*(cossinpoly(m,x,y)+sign(m)*im*sinsinpoly(m,x,y))*p
	end
end

# multiplying r^l*ylm(x,y,z)
function rlylm(l::Int64, m::Int, x::Variable, y::Variable, z::Variable;
			   norm::YLMNorm{T}=Laplace{Float64}(),real=true) where T
	p = ylm(l,m,x,y,z;norm=norm,real=real)
	tout = []
	# Zerlegung des Polynoms in Terme:
	for t in terms(p)
		deg = degree(monomial(t)) # Gibt den gesamten Grad des Monoms an
		degR = l-deg # durch das Kürzen ergibt sich ein Grad von l-deg fuer r
		push!(tout,(x^2+y^2+z^2)^div(degR,2)*t) # r² wird durch x²+y²+z² ersetzt
	end

	return polynomial(tout)
end

# solid harmonics
function rlm(l::Int64, m::Int64, x::Variable, y::Variable, z::Variable;
			 norm::YLMNorm{T}=Laplace{Float64}(),real=true) where T
	rlm = rlylm(l,m,x,y,z; norm=norm,real=real)
	if norm != Nonorm{T}()
		rlm = sqrt(4one(T)*T(pi)/(2*l+1))*rlm
	end
	return rlm
end


#  Copyright 2017, Oscar Dowson

export binexpand, bincontract, bitsrequired

# Cache these for speed
const log2inv = 1 / log(2)
# dereferencing an array is faster than a 2^x call
const _2i_ = Int[2^(i-1) for i in 1:floor(Int, log(typemax(Int)) * log2inv)+1]
const _2i_L = length(_2i_)

function binexpand!(y::Vector{Int}, x::Int)
    if x < 0
        error("Values to be expanded must be nonnegative. Currently x = $x.")
    end
    @inbounds for i in length(y):-1:1
        k = _2i_[i]
        if x >= k
            y[i] = 1
            x -= k
        end
    end
    if x > 0
        error("Unable to expand binary. Overflow of $x.")
    end
end

function bitsrequired(x::Int)
    floor(Int, log(x) * log2inv) + 1
end
function bitsrequired(x::Float64, eps::Float64=0.1)
    xx = round(Int, x / eps)
    floor(Int, log(xx) * log2inv) + 1
end

"""
    binexpand(x::Int; length::Int=-1, maximum::Real=-1)

Returns an array of 0/1 coefficients for the binary expansion of `x`.
If trailing zeroes are needed, use `length` to specify the length of the output.
"""
function binexpand(x::Int; length::Int=-1, maximum::Real=-1)
    x < 0 && error("Cannot perform binary expansion on a negative number.")
    if maximum != -1
        if length != -1
            warn("Length is being ignored.")
        end
        length = bitsrequired(floor(Int, maximum))
    end
    if length == -1
        y = zeros(Int, bitsrequired(x))
    else
        y = zeros(Int, length)
    end
    binexpand!(y, x)
    y
end

"""
    binexpand(x::Float64, eps::Float64=0.1; length::Int=-1, maximum::Real=-1)

Returns an array of 0/1 coefficients for the binary expansion of `x`.
If trailing zeroes are needed, use `length` to specify the length of the output.
"""
function binexpand(x::Float64, eps::Float64=0.1; length::Int=-1, maximum::Real=-1)
    x < 0 && error("Cannot perform binary expansion on a negative number.")
    if eps <= 0.0
        error("Epsilon tolerance for Float binary expansion must be strictly greater than 0.")
    end
    xx = round(Int, x / eps)
    if maximum != -1
        if length != -1
            warn("Length is being ignored.")
        end
        length = bitsrequired(floor(Int, maximum / eps))
    end
    binexpand(xx, length=length)
end

function bincontract_2i_(y::Vector{T}) where T
    x = zero(T)
    @inbounds for i in 1:length(y)
        x += _2i_[i] * y[i]
    end
    x
end
function bincontract_pow(y::Vector{T}) where T
    x = zero(T)
    @inbounds for i in 1:length(y)
        x += 2^(i-1) * y[i]
    end
    x
end

"""
    bincontract{T}(y::Vector{T})

For vector `y`, evaluates ∑ᵢ 2ⁱ⁻¹yᵢ.
"""
function bincontract(y::Vector{T}) where T
    if length(y) < _2i_L
        bincontract_2i_(y)
    else
        bincontract_pow(y)
    end
end

function bincontract(::Type{Float64}, y::Vector{T}, eps::Float64=0.1) where T
    if eps <= 0
        error("Epsilon tolerance for Float binary contraction must be strictly greater than 0.")
    end
    xx = bincontract(y)
    xx * eps
end


# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     formats: ipynb,jl:hydrogen
#     text_representation:
#       extension: .jl
#       format_name: hydrogen
#       format_version: '1.3'
#       jupytext_version: 1.10.3
#   kernelspec:
#     display_name: Julia 1.7.0
#     language: julia
#     name: julia-1.7
# ---

# %%
using Distributions

tstat(μ, X) = (mean(X) - μ)/√(var(X) / length(X))
pvalue(μ, X) = 2ccdf(TDist(length(X) - 1), abs(tstat(μ, X)))

function ci(X, α)
    X̄ = mean(X)
    c = quantile(TDist(length(X) - 1), 1 - α/2)
    d = √(var(X) / length(X))
    X̄ - c*d, X̄ + c*d
end

isininterval(x, int) = first(int) ≤ x ≤ last(int)
isinci(μ, X, α) = isininterval(μ, ci(X, α))

prob_trueisinci(dist, n, α; L=10^6) = mean(isinci(mean(dist), rand(dist, n), α) for _ in 1:L)

# %%
prob_trueisinci(Normal(), 20, 0.05)

# %%
prob_trueisinci(Uniform(), 20, 0.05)

# %%
prob_trueisinci(Exponential(), 20, 0.05)

# %%
prob_trueisinci(MixtureModel([Normal(), Normal(10, 1)], [0.95, 0.05]), 20, 0.05)

# %%
using Plots

# %%
normal = Normal(mean(Uniform()), std(Uniform()))
P = plot(x -> pdf(Uniform(), x), -1, 2; label="Uniform()")
plot!(x -> pdf(normal, x), -1, 2; label="normal approx.", ls=:dash)
title!("pdfs")
Q = plot(x -> cdf(Uniform(), x), -1, 2; label="Uniform()")
plot!(x -> cdf(normal, x), -1, 2; label="normal approx.", ls=:dash)
title!("cdfs"; legend=:bottomright)
plot(P, Q; size=(720, 300))

# %%
normal = Normal(mean(Exponential()), std(Exponential()))
P = plot(x -> pdf(Exponential(), x), -3, 6; label="Exponential()")
plot!(x -> pdf(normal, x), -3, 6; label="normal approx.", ls=:dash)
title!("pdfs")
Q = plot(x -> cdf(Exponential(), x), -3, 6; label="Exponential()")
plot!(x -> cdf(normal, x), -3, 6; label="normal approx.", ls=:dash)
title!("cdfs"; legend=:bottomright)
plot(P, Q; size=(720, 300))

# %%
mixnormal = MixtureModel([Normal(), Normal(10, 1)], [0.95, 0.05])
P = plot(x -> pdf(mixnormal, x), -5, 15; label="mixnormal")
plot!(x -> pdf(Normal(), x), -5, 15; label="Normal()", ls=:dash)
title!("pdfs")
Q = plot(x -> cdf(mixnormal, x), -5, 15; label="mixnormal")
plot!(x -> cdf(Normal(), x), -5, 15; label="Normal()", ls=:dash)
title!("cdfs"; legend=:bottomright)
plot(P, Q; size=(720, 300))

# %%
mixnormal = MixtureModel([Normal(), Normal(10, 1)], [0.95, 0.05])
normal = Normal(mean(mixnormal), std(mixnormal))
P = plot(x -> pdf(mixnormal, x), -5, 15; label="mixnormal")
plot!(x -> pdf(normal, x), -5, 15; label="normal approx.", ls=:dash)
title!("pdfs")
Q = plot(x -> cdf(mixnormal, x), -5, 15; label="mixnormal")
plot!(x -> cdf(normal, x), -5, 15; label="normal approx.", ls=:dash)
title!("cdfs"; legend=:bottomright)
plot(P, Q; size=(720, 300))

# %%
model = Normal()
@show model
μ, σ² = mean(model), var(model)
PP = []
for n in (5, 10, 20, 100)
    Z = [(mean(rand(model, n)) - μ)/√(σ²/n) for _ in 1:10^5]
    P = histogram(Z; norm=true, alpha=0.3, bin = -3.5:0.1:6, label="")
    plot!(x -> pdf(Normal(), x); label="", ls=:dash, lw=1.5)
    title!("sample size n = $n")
    push!(PP, P)
end
plot(PP...; size=(800, 600), layout=(2, 2), titlefontsize=11)

# %%
model = Uniform()
@show model
μ, σ² = mean(model), var(model)
PP = []
for n in (5, 10, 20, 100)
    Z = [(mean(rand(model, n)) - μ)/√(σ²/n) for _ in 1:10^5]
    P = histogram(Z; norm=true, alpha=0.3, bin = -3.5:0.1:6, label="")
    plot!(x -> pdf(Normal(), x); label="", ls=:dash, lw=1.5)
    title!("sample size n = $n")
    push!(PP, P)
end
plot(PP...; size=(800, 600), layout=(2, 2), titlefontsize=11)

# %%
model = Exponential()
@show model
μ, σ² = mean(model), var(model)
PP = []
for n in (5, 10, 20, 100)
    Z = [(mean(rand(model, n)) - μ)/√(σ²/n) for _ in 1:10^5]
    P = histogram(Z; norm=true, alpha=0.3, bin = -3.5:0.1:6, label="")
    plot!(x -> pdf(Normal(), x); label="", ls=:dash, lw=1.5)
    title!("sample size n = $n")
    push!(PP, P)
end
plot(PP...; size=(800, 600), layout=(2, 2), titlefontsize=11)

# %%
model = MixtureModel([Normal(), Normal(10, 1)], [0.95, 0.05])
@show model
μ, σ² = mean(model), var(model)
PP = []
for n in (5, 10, 20, 100)
    Z = [(mean(rand(model, n)) - μ)/√(σ²/n) for _ in 1:10^5]
    P = histogram(Z; norm=true, alpha=0.3, bin = -3.5:0.1:6, label="")
    plot!(x -> pdf(Normal(), x); label="", ls=:dash, lw=1.5)
    title!("sample size n = $n")
    push!(PP, P)
end
plot(PP...; size=(800, 600), layout=(2, 2), titlefontsize=11)

# %%

0012/probability of true value being in CI.jl


###############################################################
#                Combined Mean and Variance
###############################################################
"""
    combined_mean_and_var(xs...) -> meanc, varc
Calculates the combined mean and variance of the concatenated sample `vcat(xs...)`.
"""
function combined_mean_and_var(xstuple::AbstractVector{T}...) where T
    xs = collect(xstuple)
    ns = length.(xs)
    μs = mean.(xs)
    vs = var.(xs)
    return combined_mean_and_var(ns, μs, vs)
end

export combined_mean_and_var

"""
    combined_mean_and_var(ns, μs, vs) -> meanc, varc
Given N samples characterized by their lengths `ns`,
their means `μs`, and their variances `vs`,
calculates the combined (or pooled) mean and variance of the
overall (concatenated) sample.
"""
function combined_mean_and_var(ns::AbstractVector{<:Integer},
                               μs::AbstractVector{<:Number},
                               vs::AbstractVector{<:Number})
    nsum = sum(ns)
    meanc = dot(ns, μs) / nsum
    varc = sum((ns .- 1) .* vs + ns .* abs2.(μs .- meanc)) / (nsum - 1)
    return meanc, varc
end

function combined_mean_and_var(ns::AbstractVector{<:Integer},
                               μs::AbstractVector{<:AbstractArray{<:Number}},
                               vs::AbstractVector{<:AbstractArray{<:Number}})
    meanc = zero(μs[1])
    varc = zero(vs[1])

    nsum = sum(ns)
    N = length(μs) # number of samples
    for i in eachindex(meanc)

        for k in 1:N
            meanc[i] += ns[k] * μs[k][i]
        end
        meanc[i] = meanc[i] / nsum

        for k in 1:N
            varc[i] += (ns[k] - 1) * vs[k][i] + ns[k] * abs2(μs[k][i] - meanc[i])
        end
        varc[i] = varc[i] / (nsum - 1)
    end

    return meanc, varc
end

###############################################################
#       Combined Mean and Variance ("Hand-written")
###############################################################
"""
    combined_mean_and_var(x1, x2) -> meanc, varc
Given two samples `x1`,`x2` calculates the mean and variance of the
concatenated sample.
"""
function combined_mean_and_var(x1::AbstractVector{<:Number}, x2::AbstractVector{<:Number})
    n1, n2 = length(x1), length(x2)
    μ1, μ2 = mean(x1), mean(x2)
    v1, v2 = var(x1), var(x2)
    return combined_mean_and_var(n1, μ1, v1, n2, μ2, v2)
end

"""
    combined_mean_and_var(n1, μ1, v1, n2, μ2, v2) -> meanc, varc
Given two samples characterized by their lengths `n1`, `n2`,
their means `μ1`, `μ2`, and their variances `v1`, `v2`,
calculates the combined (or pooled) mean and variance of the
concatenated sample.
"""
function combined_mean_and_var(n1::Integer, μ1::Number, v1::Number,
                               n2::Integer, μ2::Number, v2::Number)
    meanc = (n1 * μ1 + n2 * μ2) / (n1 + n2)

    # Based on https://www.emathzone.com/tutorials/basic-statistics/combined-variance.html,
    # including Robert Matheson's comment and adding abs for complex number support.
    varc = ((n1-1)*v1 + (n2-1)*v2 + n1*abs2(μ1 - meanc) + n2*abs2(μ2 - meanc)) /
                                (n1 + n2 - 1)
    return meanc, varc
end

# Explicit version for three samples (only for testing)
function combined_mean_and_var_three(x1, x2, x3)
    n1, n2, n3 = length(x1), length(x2), length(x3)
    μ1, μ2, μ3 = mean(x1), mean(x2), mean(x3)
    v1, v2, v3 = var(x1), var(x2), var(x3)

    meanc12, varc12 = combined_mean_and_var(n1, μ1, v1, n2, μ2, v2)
    n12 = n1 + n2
    return combined_mean_and_var(n12, meanc12, varc12, n3, μ3, v3)
end

"""
    combined_mean_and_var(ors::Vector{<:ObservableResult}) -> meanc, stderrc
Calculates the combined mean and standard error of the concatenated observable results.
"""
function combined_mean_and_error(ors::Vector{<:ObservableResult})
    @assert length(ors) > 0 "Provided vector of ObservableResults must not be empty."

    ns = [r.count for r in ors]
    μs = [r.mean for r in ors]
    vs = [r.error^2 for r in ors]

    meanc, varc = combined_mean_and_var(ns, μs, vs)
    return meanc, sqrt.(varc)
end


# Struct to hold an x, y position. A tuple would also be fine.
struct Pos
    x::Float64
    y::Float64
end

# This returns a measure of how far "left" the vector is with a cross product
function cross(point1::Pos, point2::Pos, point3::Pos)
    vec1 = Pos(point2.x - point1.x, point2.y - point1.y)
    vec2 = Pos(point3.x - point2.x, point3.y - point2.y)
    ret_angle = vec1.x*vec2.y - vec1.y*vec2.x
    return ret_angle*ret_angle
end

function jarvis_march(points::Vector{Pos})
    hull = Vector{Pos}()

    # sorting array based on leftmost point
    sort!(points, by = item -> item.x)
    push!(hull, points[1])

    i = 1
    curr_point = points[2]

    # Find angle between points
    curr_product = cross(Pos(0,0), hull[1], curr_point)

    # We will hold a temp variable with the highest cross product as we iterate
    # through all the points and move our hull vector forward.
    while (curr_point != hull[1])
        for point in points
            product = 0.0

            # Special case for the first element when there is no hull[i-1]
            if (i == 1)
                if (hull[i] != point)
                    product = cross(Pos(0,0), hull[i], point)
                end
            else
                if (hull[i] != point && hull[i-1] != point)
                    product = cross(hull[i-1], hull[i], point)
                end
            end
            if (product > curr_product)
                curr_point = point
                curr_product = product
            end
        end

        # Pushing to hull, moving simulation forward and resetting the product
        push!(hull, curr_point)
        curr_product = 0
        i += 1
    end

    return hull
end

function main()

    # These points are chosen such that there is a clearly defined hull with
    # several interior points. As a note, these will be generated either
    # randomly or via some mesh in practice.
    points = [Pos(2,1.5), Pos(1, 1), Pos(2, 4), Pos(3, 1), Pos(2,2), Pos(2,0.5)]
    hull = jarvis_march(points)
    println(hull)
end

main()


function fib(x, y)
  return y, x+y
end
a, b, s = 1, 1, 0
while a+b < 4000_000
  global a, b, s
  a, b = fib(a, b)
  if iseven(b)
    s += b
  end
end
println(s)


# See https://jp.mathworks.com/help/matlab/ref/peaks.html
function peaks(N=49::Int)
    function formula(x::T, y::T) where T<:Float64
        z = 3(1-x)^2*exp(-(x^2)-(y+1)^2)-10(x/5 - x^3 - y^5)*exp(-x^2-y^2)-(1/3)exp(-(x+1)^2-y^2)
    end
    vec = collect(Float64, LinRange(-3.,3.,N))
    x = repeat(vec', outer=(N,1))
    y = repeat(vec, outer=(1,N))
    z = formula.(x,y)
    return (x,y,z)
end


# =============================================================================
# CALCULATRICE SCIENTIFIQUE
# =============================================================================

# opérateurs  classiques : +-*/\

1 + 3
3 / 7
3 \ 7 # ≈ 7 / 3
1e2
im # nombre complexe i
im^2
# Les fonctions usuelles : exp, log, log10, abs, sqrt, cbrt, sign

log10(100_000)
log(exp(2))
1 / (1 + exp(1))
# Les fonctions trigonométriques usuelles : sin, cos, tan, cot, sec, csc, sinh, cosh, tanh, coth, sech, csch, asin, acos, atan...

sin(π+√2) # Code Is Math !!  tapez "\pi<tab>" et "\sqrt<tab>2"

# =============================================================================
# VALEURS (tout ce qui est manipulable en Julia) & TYPES (Int32, Int64, Int128, Float32, Float64, Complex32, Complex64, Complex128...)
# =============================================================================

# types et collections

3 + 5 # nombre entier stockée en 32/64 bits en fonction de l'architecture machine
typeof(3 + 5)
3. * 4. # nombre en virgule flottante stockée en 32/64 bits en fonction de l'architecture machine
typeof(3. * 4.)
typeof(3. * 4)
typeof(Float64)
π # tapez "\pi<tab>"
typeof(π)
Float64(π)
Float32(π)
typeof(1 + 2im)
[1,2,3]
typeof([1,2,3])
1:5 # itérateur
typeof(1:5)
collect(1:5)
0:0.1:1
typeof(0:0.1:1)
collect(0:0.1:1)
[4. 5. 6.;7. 8. 9.]
(1, 2.0, "test") # tuple
typeof((1, 2.0, "test"))

# Caractères et chaînes de caractères

'A'
typeof('A')
"hello"

typeof("hello")

"racine carrée de 2 : $(sqrt(2))"

" √3 : $(√3)"

"Hello" * " John Doe"

# Boléens et opérateurs de comparaison | "==" | "!=" | > | ">=" | "<" | "<=" | et de conjonction | "&" | "|" | "!"|

1 > 2
typeof(1 > 2)

# supertype et sous-types

supertype(Int64)
subtypes(Integer)

# type utilisateur

struct MyComplex
    _real::Float64
    _imag::Float64
end
typeof(MyComplex)
MyComplex(1,2)
typeof(MyComplex(1, 2))

# =============================================================================
# REGLE DE NOMMAGE(variables, fonctions/méthodes, modules, types, macros)
# =============================================================================
#=
- noms des variables  en minuscule, la séparation des mots dans une variable se fait à l'aide d'un underscore ('_') ("snake case")
- noms de type et de modules commencent par une lettre majuscule, les majuscules séparant les différents mots du nom (exemple "MonModule") ("camel case")
- noms des fonctions et macros en minuscule sans underscores.
- une fonction qui modifie ses arguments s'écrit avec ! à la fin. =#

# =============================================================================
# VARIABLES (étiquettes associées aux valeurs)
# =============================================================================

# Les variables contenant des nombres scalaires

α = 6 # variable latex-unicode  names — tapez "\alpha<tab>"
typeof(α)
β = 2α + 1.5
typeof(β)
γ = α + β * im
typeof(γ)
δ = Float64(10)
δ = MyComplex(1, 2)
δ._real

# Les vecteurs (tableau de nombres scalaires ou non de dim=1)

v = [1, 17, 32, 15]
@show v
size(v)
n = length(v)
eltype(v)
v[2] # accès à l'élément d'index n°2 (indice de départ = 1)
for i in 1:n
	println("v[$i] = $(v[i])")
end
for elt in v
	println(elt)
end
v[2:3] # slicing
v' # transposée de x
v2 = Float64[1, 2, 3, 4]
v + v2
v' * v2
sin.(v) # opérateur . de broadcast ( sin de chaque terme)
v .* v2 # opérateur . de broadcast (ici multiplication terme à terme)
v3 = ones(Int64, 3)
v4 = []
typeof(v4)
push!(v4,1)     # => [1]
push!(v4,2.)     # => [1,2.]
push!(v4,"test")     # => [1,2, "test"]
append!(v4,v4)

# Les matrices (tableau de nombres scalaires ou non dim=2 )

A = [1. 2 3
     4 5 6
     7 8 10]
ni, nj = size(A)
length(A)
eltype(A)
A[2,3]
A[end,end]
A[2,:]
A[:,2]
A[2:3,2:3]
A[1:end]
for j in 1:nj
    for i in 1:ni
        println("A[$i,$j] = $(A[i,j])")
    end
end
for elt in A
	println(elt)
end
As = similar(A)
B = fill(2.0, (3, 3))
C = A + B
C = A * B
sin.(A)
AA = [A  A] # concaténation horizontale par bloc (hcat)
AAAA = [AA;AA] # concaténation verticale par bloc (vcat)
C = [ 1 2 3; 4 5 6; 7 8 9]
C = [ u * v for u = 0:0.1:1, v = 0:0.1:1]

# =============================================================================
# Programmation
# =============================================================================

for i = 1:5
    println("i = $i")
end
for i = 1:1000
    println(i)
    if i >= 5
       break
    end
end
i = 1
if i == 1
    println("i = 1")
end
i = 2
if i != 1
    println("i ≠ 1")
else
    println("i = 1")
end
i = 0
if i == 1
    println("i = 1")
elseif i > 1
    println("i > 1")
else
    println("i < 1")
end
i=0
while i<5
    global i+=1  # i =i+1
    println("i = $i")
end
i=0
while i<10
    global i+=1
    if i <= 5
    println("i = $i")
    else
        break
    end
end

# =============================================================================
# Algèbre linéaire
# Déterminant, valeurs et vecteurs propres...
# =============================================================================

using LinearAlgebra

A = [1. 2. 3.; 4. 5. 6.; 7. 8. 10.]
diag(A)
tr(A)
sum(A)
det(A)
F = eigen(A)
F.values
F.vectors
b = [3, 2, 1] # a vector
x = A \ b # résolution du système linéaire Ax=b
xx = inv(A) * b # résolution du système linéaire Ax=b
x - xx

# =============================================================================
# Fonctions, Méthodes, Compilation, Spécialisation, Multiple Dispatch
# =============================================================================

# Version longue

function fun_name(arg1, arg2)
    tmp = arg1 + arg2
    return tmp * arg1          # <- le mot-clé return est optionnel, la dernière valeur calculée est retournée
end

# Appel

fun_name(2, 3)

# Version courte

fun_name(arg1, arg2) = arg1 * (arg1 + arg2)

# Appel

fun_name(2, 3)

# Multiple Dispatch
# différentes implémentations (méthodes) de la fonction f

function f end
f(x)          = println("Generic method with 1 argument : $x")
f(x::Integer) = println("Specific method with 1 argument (Integer) : $x")
f(x, y)       = println("Generic method with 2 arguments: $x, $y")
f(x::Real, y) = println("Specific method with 2 arguments (Real, Any): $x, $y")

methods(f)

f(π)
f(1)
f(im, 2)
f(π, [1, 2, 3])

@which f(im, im)

using BenchmarkTools

function my_sum(X)
    acc = zero(eltype(X)) # zero du type de l'élément de l'argument
    for x in X
        acc += x
    end
    acc
end

V = rand(1_000_000)
@time my_sum(V) # Compilation + Exécution de la fonction  my_sum avec argument de type vecteur (spécialisation)
@time my_sum(V) # Exécution de la fonction  my_sum
@time my_sum(V)

@code_native debuginfo = :none my_sum(V)

UR = (1:1_000_000)
@time my_sum(UR) # # Compilation + Exécution de la fonction  my_sum avec argument de type UnitRange (spécialisation)
@time my_sum(UR)
@time my_sum(UR)

@code_native debuginfo = :none my_sum(UR) # code natif différent du précédent  avec même implémnetation => spécialisation

@btime my_sum(UR)
@btime Base.sum(UR) # pas de magie dans Julia !!

function my_sum(X::Array{Float64,2})
    acc = zero(eltype(X))
    ni, nj = size(X)
    for j in 1:nj
        for i in 1:ni
            acc += X[i,j]
        end
    end
    acc
end
A = rand(Float64, 1_000, 1_000)
methods(my_sum)
@time my_sum(A) # type est connu, donc pas de perte de temps pour le branchement
@time my_sum(A)
@time my_sum(A)
@btime my_sum(A)
@which my_sum(A)

@code_native debuginfo = :none my_sum(A)

# =============================================================================
# Graphique avec Plots
#  =============================================================================

using Plots
x=0:2π/100:2π
y=sin.(x)
theme(:dark)
plot(x,y,title="Mon premier graphique avec Plots")
y=cos.(x)
plot!(x,y)

# =============================================================================
# Sciences des données
# https://en.wikibooks.org/wiki/Introducing_Julia/DataFrames
# =============================================================================

# using Pkg; Pkg.add("CSV"); Pkg.add("HTTP");Pkg.add("DataFrames")
using CSV
using HTTP
using DataFrames

df = CSV.File(HTTP.get("https://raw.githubusercontent.com/nassarhuda/easy_data/master/programming_languages.csv").body) |> DataFrame
@show df

# =============================================================================
# Calcul parallèle sur GPU NVidia
# =============================================================================

using BenchmarkTools

mcpu = rand(2^12, 2^12)
@btime mcpu*mcpu

# using Pkg; Pkg.add("CUDA")
using CUDA

mgpu = cu(mcpu)
@btime  mgpu*mgpu

# =============================================================================
# Calcul symbolique
# =============================================================================

# using Pkg; Pkg.add("SymPy")
using SymPy

E, S, L, θ = symbols("E S L θ",real=true)
ke = E * S / L * Sym[1 0 -1 0;0 0 0 0;-1 0 1 0;0 0 0 0 ]
Re=Sym[cos(θ) sin(θ) 0 0; -sin(θ) cos(θ) 0 0; 0 0 cos(θ) sin(θ); 0 0 -sin(θ) cos(θ)]
Ke=Re'*ke*Re

# using Pkg; Pkg.add("Symbolics");Pkg.add("SymbolicUtils")
using Symbolics

@variables Eₑ Sₑ Lₑ θₑ
kₑ = Eₑ * Sₑ / Lₑ * [1 0 -1 0;0 0 0 0;-1 0 1 0;0 0 0 0 ]
Rₑ = [cos(θₑ) sin(θₑ) 0 0; -sin(θₑ) cos(θₑ) 0 0; 0 0 cos(θₑ) sin(θₑ); 0 0 -sin(θₑ) cos(θₑ)]
Kₑ = Rₑ' * kₑ * Rₑ


"""
alpha,beta,vk,vkm1 = LanczosStep!(A::Function,vk,vkm1,beta;sigma=0.0,tol=1e-10)

Performs a Lanczos step

Implementation is based on Table 2.1 in

Choi, S.-C. T. (2006).
Iterative Methods for Singular Linear Equations and Least-squares Problems.
Phd thesis, Stanford University.
"""
function LanczosStep!(A::Function,vk,vkm1,beta;sigma=0.0,tol=1e-10)
    n     = length(vk)

    pk    = A(vk)
    if sigma!=0.0
        BLAS.axpy!(n,-sigma,vk,1,pk,1) #pk   -= sigma*vk
    end

    alpha = BLAS.dot(n,vk,1,pk,1)  # alpha = dot(vk,pk)
    BLAS.axpy!(n,-alpha,vk,1,pk,1) # pk   -= alpha*vk
    BLAS.axpy!(n,-beta,vkm1,1,pk,1)# pk   -= beta*vkm1
    vkm1  = BLAS.blascopy!(n,vk,1,vkm1,1) # vkm1 = copy(vk)
    vk    = pk
    beta  = BLAS.nrm2(n,vk,1)
    if beta>tol
         vk /= beta
    end
    return alpha, beta,vk,vkm1
end

"""
c,s,r = SymOrtho(a,b)

Computes a Givens rotation

Implementation is based on Table 2.9 in

Choi, S.-C. T. (2006).
Iterative Methods for Singular Linear Equations and Least-squares Problems.
Phd thesis, Stanford University.
"""
function symOrtho(a,b)
    c = 0.0; s = 0.0; r = 0.0
	if b==0
		s = 0.0
		r = abs(a)
		c = (a==0) ? c=1.0 : c = sign(a)
	elseif a == 0
		c = 0.0
		s = sign(b)
		r = abs(b)
	elseif abs(b) > abs(a)
		tau = a/b
		s   = sign(b)/sqrt(1+tau^2)
		c   = s*tau
		r   = b/s
	elseif abs(a) > abs(b)
		tau = b/a
		c   = sign(a)/sqrt(1+tau^2)
		s   = c*tau
		r   = a/c
	end
	return c,s,r
end


struct PoissonSolver{DT <: Real}
    nx::Int
    Δx::DT
    xgrid::Vector{DT}
    ρ::Vector{DT}
    ϕ::Vector{DT}

    function PoissonSolver{DT}(nx::Int) where {DT}
        Δx = 1/nx
        xgrid = collect(0:Δx:1)
        new(nx, Δx, xgrid, zeros(DT, nx), zeros(DT, nx))
    end
end

function solve!(p::PoissonSolver{DT}, x::AbstractVector{DT}) where {DT}
    h = fit(Histogram, mod.(x, 1), p.xgrid)
    p.ρ .= h.weights ./ length(x)
    ρ̂ = rfft(p.ρ)
    k² = [(i-1)^2 for i in eachindex(ρ̂)]
    ϕ̂ = - ρ̂ ./ k²
    ϕ̂[1] = 0
    p.ϕ .= irfft(ϕ̂, length(p.ρ))
    return p
end

function eval_field(p::PoissonSolver{DT}, x::DT) where {DT}
    y = mod(x, one(x))
    i1 = floor(Int, y / p.Δx) + 1
    i2 = mod( ceil(Int, y / p.Δx), p.nx) + 1
    i1 == i2 && (i1 = i1-1)
    i1 == 0 && (i1 = lastindex(p.ϕ))
    return - (p.ϕ[i2] - p.ϕ[i1]) / p.Δx
end


using SpecialPolynomials;
const SP = SpecialPolynomials;
using Polynomials
import LinearAlgebra: eigvals, det, dot, norm
import SpecialFunctions: gamma
using Test

@testset "Orthogonal" begin
    include("Orthogonal.jl")
end
@testset "Orthogonal compare with tables" begin
    include("Orthogonal-compare.jl")
end
#@testset "ChebyshevT" begin include("ChebyshevT.jl") end
#@testset "ChebyshevU" begin include("ChebyshevU.jl") end
#@testset "Laguerre{α}" begin include("GeneralizedLaguerre.jl") end
#@testset "Gegenbauer{α}" begin include("Gegenbauer.jl") end

@testset "Lagrange" begin
    include("Lagrange.jl")
end
@testset "Newton" begin
    include("Newton.jl")
end

@testset "Bernstein" begin
    include("Bernstein.jl")
end


##---begin-Plot-functions---------------------------------------------------------------------------
"""
    chromatogram(t, tR, τR)

Calculate the chromatogram as a sum of gaussian peaks over the time `t` for peaks centered at retention times `tR` and with peak width `τR`.
"""
function chromatogram(t::Array{Float64,1}, tR::Array{Float64,1}, τR::Array{Float64,1})
	g(t,tR,τR) = 1/sqrt(2*π*τR^2)*exp(-(t-tR)^2/(2*τR^2))
	chromatograms = Array{Array{Float64,1}}(undef, length(tR))
	for j=1:length(tR)
		chromatograms[j] = g.(t, tR[j], τR[j])
	end
	return sum(chromatograms)
end

"""
    plot_chromatogram(peaklist, tlims; annotation=true, number=true, mirror=false, offset=0.0)

Plot the chromatogram of the peaks listed in `peaklist` over the time tupel `tlims = (t_start, t_end`).

# Arguments
* `peaklist`: DataFrame with the names, retention times and peak widths of the simulated substances.
* `tlims`: Tuple defining the start and end time of the plotted chromatogram.
* `annotation`: Boolean, switching the annotation of the peaks on/off; default = true.
* `number`: Boolean, switching the type of the annotation between _number_ of the substance in the peaklist (`number = true`) or _name_ of the substance (`number = false`); default = true.
* `mirror`: Boolean, if `mirror = true` the chromatogram is multiplied by `-1`; default = false.
* `offset`: Float64, this value is added to the chromatogram; default = 0.0.

# Output
Tupel `(p_chrom, t, chrom)`
* `p_chrom`: the plot of the chromatogram `chrom` over time `t`
* `t`: Array of time of the chromatogram
* `chrom`: Array of the abundance values of the chromatogram
"""
function plot_chromatogram(peaklist, tlims; annotation=true, number=true, mirror=false, offset=0.0)
	t₀ = tlims[1]
	tMax = tlims[2]
	t = t₀:tMax/10000:tMax
	if mirror==true
		sgn = -1.0
	else
		sgn = 1.0
	end
	chrom = sgn.*chromatogram(collect(t), peaklist.tR, peaklist.τR) .+ offset
	chrom_tR = sgn.*chromatogram(peaklist.tR, peaklist.tR, peaklist.τR) .+ offset
	p_chrom = plot(t, chrom, xlabel="time in s", ylabel="abundance", legend=false)
	if annotation==true && number==true
		plot!(p_chrom, annotations = [(peaklist.tR[i], chrom_tR[i], text(i, 10, rotation=0, :center)) for i in 1:length(peaklist.tR)])
	elseif annotation==true && number==false
		plot!(p_chrom, annotations = [(peaklist.tR[i], chrom_tR[i], text(peaklist.Name[i], 10, rotation=90, :center)) for i in 1:length(peaklist.tR)])
	end
	return p_chrom, t, chrom
end

"""
    plot_chromatogram!(p_chrom, peaklist, tlims; annotation=true, number=true, mirror=false, offset=0.0)

Add the chromatogram of the peaks listed in `peaklist` over the time tupel `tlims = (t_start, t_end`) to the plot `p_chrom`.

# Arguments
* `p_chrom`: Plot of an existing chromatogram.
* `peaklist`: DataFrame with the names, retention times and peak widths of the simulated substances.
* `tlims`: Tuple defining the start and end time of the plotted chromatogram.
* `annotation`: Boolean, switching the annotation of the peaks on/off; default = true.
* `number`: Boolean, switching the type of the annotation between _number_ of the substance in the peaklist (`number = true`) or _name_ of the substance (`number = false`); default = true.
* `mirror`: Boolean, if `mirror = true` the chromatogram is multiplied by `-1`; default = false.
* `offset`: Float64, this value is added to the chromatogram; default = 0.0.

# Output
Tupel `(t, chrom)`
* `t`: Array of time of the chromatogram
* `chrom`: Array of the abundance values of the chromatogram
"""
function plot_chromatogram!(p_chrom, peaklist, tlims; t₀=0.0, annotation=true, number=true, mirror=false, offset=0.0)
	t₀ = tlims[1]
	tMax = tlims[2]
	t = t₀:tMax/10000:tMax
	if mirror==true
		sgn = -1.0
	else
		sgn = 1.0
	end
	chrom = sgn.*chromatogram(collect(t), peaklist.tR, peaklist.τR) .+ offset
	chrom_tR = sgn.*chromatogram(peaklist.tR, peaklist.tR, peaklist.τR) .+ offset
	plot!(p_chrom, t, chrom)
	if annotation==true && number==true
		plot!(p_chrom, annotations = [(peaklist.tR[i], chrom_tR[i], text(i, 10, rotation=0, :center)) for i in 1:length(peaklist.tR)])
	elseif annotation==true && number==false
		plot!(p_chrom, annotations = [(peaklist.tR[i], chrom_tR[i], text(peaklist.Name[i], 10, rotation=90, :center)) for i in 1:length(peaklist.tR)])
	end
	return t, chrom
end

"""
	plot_flow(par)

Calculate and plot the flow (in mL/min, normalized) of the carrier gas in a GC Column with a program defined in the parameters `par::GasChromatography.Parameters`.
"""
function plot_flow(par)
	t = 0.0:sum(par.prog.time_steps)/1000.0:sum(par.prog.time_steps)
	F = Array{Float64}(undef, length(t))
	for i=1:length(t)
		F[i] = flow(t[i], par.prog.T_itp, par.prog.Fpin_itp, par.prog.pout_itp, par.col.L, par.col.d, par.col.gas; ng=par.opt.ng, vis=par.opt.vis, control=par.opt.control)
	end
	p_flow = plot(t, F.*60e6, xlabel="time in s", ylabel="column flow in mL/min", legend=false)
	return p_flow
end

"""
	plot_pressure(prog)

Plot the inlet and outlet pressure over time of the program `prog::GasChromatographySimulator.Program`.
"""
function plot_pressure(par)
	t = 0.0:sum(par.prog.time_steps)/1000.0:sum(par.prog.time_steps)
	pin = Array{Float64}(undef, length(t))
	pout = Array{Float64}(undef, length(t))
	for i=1:length(t)
		pin[i] = inlet_pressure(t[i], par.prog.T_itp, par.prog.Fpin_itp, par.prog.pout_itp, par.col.L, par.col.d, par.col.gas; ng=par.opt.ng, vis=par.opt.vis, control=par.opt.control)
		pout[i] = par.prog.pout_itp(t[i])
	end
	p_press = plot(t, pin, xlabel="time in s", ylabel="pressure in Pa", label="inlet", legend=:right)
	plot!(p_press, t, pout, label="outlet")
	return p_press
end

"""
	plot_temperature(par; selector="T(t)")

Plot the temperature program of the GC Column.

# Arguments
* `par::GasChromatographySimulator.Parameters`: parameters of the GC system
* `selector::String`: selection of the plot as:
	* `selector = "T(t)"`: 2D-plot of temperature `T` over time `t` at inlet (`x=0`) and outlet (`x=L`)
	* `selector = "T(x)"`: 2D-plot of temperature `T` over column position `x` at the `time_steps` of the program
	* `selector = "T(x,t)"`: 3D-plot of temperature `T` over column position `x` and `t`
"""
function plot_temperature(par::GasChromatographySimulator.Parameters; selector="T(t)")
	if selector=="T(x)"
		nx = 0.0:par.col.L/1000:par.col.L
		p_temp = plot(xlabel="x in m", ylabel="T in °C", legend=:top)
		for i=1:length(par.prog.time_steps)
			T = par.prog.T_itp.(nx, cumsum(par.prog.time_steps)[i]).-273.15
			plot!(p_temp, nx, T, label="t=$(cumsum(par.prog.time_steps)[i])s")
		end
	elseif selector=="T(t)"
		nt = 0.0:sum(par.prog.time_steps)/1000:sum(par.prog.time_steps)
		T0 = par.prog.T_itp.(0.0, nt).-273.15
		p_temp = plot(nt, T0, xlabel="t in s", ylabel="T in °C", legend=:top, label="inlet", c=:red)
		TL = par.prog.T_itp.(par.col.L, nt).-273.15
		plot!(p_temp, nt, TL, label="outlet", c=:blue)
	elseif selector=="T(x,t)"
		nx = 0.0:par.col.L/1000:par.col.L
		nt = 0.0:sum(par.prog.time_steps)/1000:sum(par.prog.time_steps)
		T = Array{Float64}(undef, length(nx), length(nt))
		for j=1:length(nt)
			for i=1:length(nx)
				T[i,j] = par.prog.T_itp(nx[i], nt[j])-273.15
			end
		end
		p_temp = plot(nx, nt, T', st=:surface, xlabel="x in m", ylabel="t in s", zlabel="T in °C")
	end
	return p_temp
end

# functions from GasChromatographyTools.jl:
"""
	local_plots(xx, yy, sol, par)

Show additional 'local' plots of selected `yy` quantities over selected `xx`
quantities.

# Arguments
* `xx`: Selected quantity shown on the x-axis. Possible values: "z", "t", "T",
  "τ", "σ" and "u".
* `yy`: Selected quantity shown on the y-axis. Possible values: "z", "t", "T",
  "τ", "σ" and "u".
* `sol`: The solution of the simulation.
* `par`: The parameters of the simulated GC-system.
"""
function local_plots(xx, yy, sol, par)
	n = size(sol)[1]

	df_sol = GasChromatographySimulator.sol_extraction(sol, par)
	xvalues = Array{Array{Float64,1}}(undef, n)
	yvalues = Array{Array{Float64,1}}(undef, n)

	p_add = plot(legend=false)
	for i=1:n
		if xx=="z"
			xvalues[i] = df_sol.z[i]
			xlabel = "position z in m"
		elseif xx=="t"
			xvalues[i] = df_sol.t[i]
			xlabel = "time t in s"
		elseif xx=="T"
			xvalues[i] = par.prog.T_itp.(df_sol.z[i], df_sol.t[i]).-273.15
			xlabel = "temperature T in °C"
		elseif xx=="τ"
			xvalues[i] = sqrt.(df_sol.τ²[i])
			xlabel = "peak width τ in s"
		elseif xx=="σ"
			xvalues[i] = velocity(df_sol, i, par).*sqrt.(df_sol.τ²[i])
			xlabel = "band width in m"
		elseif xx=="u"
			xvalues[i] = velocity(df_sol, i, par)
			xlabel = "solute velocity in m/s"
		end
		if yy=="z"
			yvalues[i] = df_sol.z[i]
			ylabel = "position z in m"
		elseif yy=="t"
			yvalues[i] = df_sol.t[i]
			ylabel = "time t in s"
		elseif yy=="T"
			yvalues[i] = par.prog.T_itp.(df_sol.z[i], df_sol.t[i]).-273.15
			ylabel = "temperature T in °C"
		elseif yy=="τ"
			yvalues[i] = sqrt.(df_sol.τ²[i])
			ylabel = "peak width in s"
		elseif yy=="σ"
			yvalues[i] = velocity(df_sol, i, par).*sqrt.(df_sol.τ²[i])
			ylabel = "band width in m"
		elseif yy=="u"
			yvalues[i] = velocity(df_sol, i, par)
			ylabel = "solute velocity in m/s"
		end
		plot!(p_add, xvalues[i], yvalues[i], xlabel=xlabel, ylabel=ylabel, label=par.sub[i].name, m=:o)
	end
	return p_add
end

"""
	velocity(df_sol, i, par)

Calculate the velocity (in m/s) coressponding to solution of the `i-th` sunstance of a
GC-system defined by `par`.
"""
function velocity(df_sol, i, par)
	x = df_sol.z[i]
	t = df_sol.t[i]
	T_itp = par.prog.T_itp
	pin_itp = par.prog.Fpin_itp
	pout_itp = par.prog.pout_itp
	L = par.col.L
	d = par.col.d
	df = par.col.df
	gas = par.col.gas
	ΔCp = par.sub[i].ΔCp
	Tchar = par.sub[i].Tchar
	θchar = par.sub[i].θchar
	φ₀ = par.sub[i].φ₀
	u = Array{Float64}(undef, length(x))
	for j=1:length(x)
		u[j] = 1/GasChromatographySimulator.residency(x[j], t[j], T_itp, pin_itp, pout_itp, L, d, df, gas, ΔCp, Tchar, θchar, φ₀; ng=par.opt.ng, vis=par.opt.vis, control=par.opt.control)
	end
	return u
end


## these are differently named than SymPy or missing or ...

Base.abs(x::SymbolicObject) = sympy.Abs(x)
Base.abs2(x::SymbolicObject) = x * conj(x)
Base.max(x::Sym, a) = sympy.Max(x, a)
Base.min(x::Sym, a) = sympy.Min(x, a)

Base.cbrt(x::Sym) = x^(1//3)
Base.ceil(x::Sym) = sympy.ceiling(x)

## Trig
Base.asech(z::Sym) = log(sqrt(1/z-1)*sqrt(1/z+1) + 1/z)
Base.acsch(z::Sym) = log(sqrt(1+1/z^2) + 1/z) ## http://mathworld.wolfram.com/InverseHyperbolicCosecant.html
Base.atan(y::Sym, x) = sympy.atan2(y,x)

Base.sinc(x::Sym) = iszero(x) ? one(x) : sin(PI*x)/(PI*x)
cosc(x::Sym) = diff(sinc(x))

Base.sincos(x::Sym) = (sin(x), cos(x))
Base.sinpi(x::Sym) = sympy.sin(x*PI)
Base.cospi(x::Sym) = sympy.cos(x*PI)
degree_variants = (:sind, :cosd, :tand, :cotd, :secd, :cscd,
                   :asind, :acosd, :atand, :acotd, :asecd, :acscd)

for  methvar in degree_variants
    meth = Symbol(String(methvar)[1:end-1])
    @eval begin
        (Base.$methvar)(ex::SymbolicObject) = ($meth)((PI/180)*ex)
    end
end

Base.rad2deg(x::Sym) = (x * 180) / PI
Base.deg2rad(x::Sym) = (x * PI) / 180

Base.hypot(x::Sym, y::Number) = hypot(promote(x,y)...)
Base.hypot(xs::Sym...) = sqrt(sum(abs(xᵢ)^2 for xᵢ ∈ xs))

## exponential
Base.log1p(x::Sym) = sympy.log(1 + x)
Base.log(x::Sym) = sympy.log(x)
Base.log(b::Number, x::Sym) = sympy.log(x, b)
Base.log2(x::SymbolicObject) = log(2,x)
Base.log10(x::SymbolicObject) = log(10,x)

## calculus.
## use a pair for limit x=>0
limit(x::SymbolicObject, xc::Pair, args...;kwargs...) = limit(x, xc[1], xc[2], args...;kwargs...)
## allow a function
limit(f::Function, x::Sym, c;kwargs...) = limit(Sym(f(x)), x, c; kwargs...)
function  limit(f::Function, c;kwargs...)
    @vars  x
    limit(f, x, c; kwargs...)
end

## This is  type piracy  and a bad   idea
function Base.diff(f::Function, n::Integer=1)
    @vars x
    sympy.diff(f(x), x, n)
end

## integrate(ex,a,b)
function integrate(ex::SymbolicObject, a::Number, b::Number)
    fs = free_symbols(ex)
    if length(fs) !== 1
        @warn "Need exactly on free symbol. Use `integrate(ex, (x, a, b))` instead"
        return
    end
    integrate(ex, (fs[1], a, b))
end
function integrate(f::Function, a::Number, b::Number)
    @vars x
    sympy.integrate(f(x), (x, a, b))
end
function integrate(f::Function)
    @syms  x
    sympy.integrate(f(x), x)
end

## Add interfaces for solve, nonlinsolve when vector of equations passed in

## An alternative to Eq(lhs, rhs) following Symbolics.jl
"""
    lhs ~ rhs

Specify an equation.

Alternative syntax to `Eq(lhs, rhs)` or `lhs ⩵ rhs` (`\\Equal[tab]`) following `Symbolics.jl`.
"""
Base.:~(lhs::Number, rhs::SymbolicObject) = Eq(lhs, rhs)
Base.:~(lhs::SymbolicObject, rhs::Number) = Eq(lhs, rhs)
Base.:~(lhs::SymbolicObject, rhs::SymbolicObject) = Eq(lhs, rhs)

"""
    solve

Use `solve` to solve algebraic equations.

Examples:

```julia
julia> using SymPy

julia> @syms x y a b c d
(x, y, a, b, c, d)

julia> solve(x^2 + 2x + 1, x) # [-1]
1-element Vector{Sym}:
 -1

julia> solve(x^2 + 2a*x + a^2, x) # [-a]
1-element Vector{Sym}:
 -a

julia> solve([a*x + b*y-3, c*x + b*y - 1], [x,y]) # Dict(y => (a - 3*c)/(b*(a - c)),x => 2/(a - c))
Dict{Any, Any} with 2 entries:
  y => (a - 3*c)/(a*b - b*c)
  x => 2/(a - c)

```

!!! note
    A very nice example using `solve` is a [blog](https://newptcai.github.io/euclidean-plane-geometry-with-julia.html) entry on [Napolean's theorem](https://en.wikipedia.org/wiki/Napoleon%27s_theorem) by Xing Shi Cai.
"""
solve() = ()
solve(V::Vector{T}, args...; kwargs...) where {T <: SymbolicObject} =
    sympy.solve(V, args...; kwargs...)

"""
    nonlinsolve

Note: if passing variables in use a tuple (e.g., `(x,y)`) and *not* a vector (e.g., `[x,y]`).
"""
nonlinsolve(V::AbstractArray{T,N}, args...; kwargs...) where {T <: SymbolicObject, N} =
    sympy.nonlinsolve(V, args...; kwargs...)

linsolve(V::AbstractArray{T,N}, args...; kwargs...) where {T <: SymbolicObject, N} =
    sympy.linsolve(V, args...; kwargs...)
linsolve(Ts::Tuple, args...; kwargs...) where {T <: SymbolicObject} =
    sympy.linsolve(Ts, args...; kwargs...)

## dsolve allowing initial condiation to be specified

"""
   dsolve(eqn, var, args..,; ics=nothing, kwargs...)

Call `sympy.dsolve`.

The initial conditions are specified with a dictionary.

Example:

```jldoctest dsolve
julia> using SymPy

julia> @syms α, x, f(), g()
(α, x, f, g)

julia> ∂ = Differential(x)
Differential(x)

julia> eqn = ∂(f(x)) ~ α * x
d
──(f(x)) = x⋅α
dx
```

```julia
julia> dsolve(eqn)
             2
            x ⋅α
f(x) = C₁ + ────
             2
```

```jldoctest dsolve
julia> dsolve(eqn(α=>2); ics=Dict(f(0)=>1)) |> print # fill in parameter, initial condition
Eq(f(x), x^2 + 1)

julia> eqn = ∂(∂(f(x))) ~ -f(x); print(eqn)
Eq(Derivative(f(x), (x, 2)), -f(x))

julia> dsolve(eqn)
f(x) = C₁⋅sin(x) + C₂⋅cos(x)

julia> dsolve(eqn; ics = Dict(f(0)=>1, ∂(f)(0) => -1))
f(x) = -sin(x) + cos(x)

julia> eqn = ∂(∂(f(x))) - f(x) - exp(x);

julia> dsolve(eqn, ics=Dict(f(0) => 1, f(1) => Sym(1//2))) |> print # not just 1//2
Eq(f(x), (x/2 + (-exp(2) - 2 + E)/(-2 + 2*exp(2)))*exp(x) + (-E + 3*exp(2))*exp(-x)/(-2 + 2*exp(2)))
```

Systems

```jldoctest dsolve
julia> @syms x() y() t g
(x, y, t, g)

julia> ∂ = Differential(t)
Differential(t)

julia> eqns = [∂(x(t)) ~ y(t), ∂(y(t)) ~ x(t)]
2-element Vector{Sym}:
 Eq(Derivative(x(t), t), y(t))
 Eq(Derivative(y(t), t), x(t))

julia> dsolve(eqns)
2-element Vector{Sym}:
 Eq(x(t), -C1*exp(-t) + C2*exp(t))
  Eq(y(t), C1*exp(-t) + C2*exp(t))

julia> dsolve(eqns, ics = Dict(x(0) => 1, y(0) => 2))
2-element Vector{Sym}:
 Eq(x(t), 3*exp(t)/2 - exp(-t)/2)
 Eq(y(t), 3*exp(t)/2 + exp(-t)/2)

julia> eqns = [∂(∂(x(t))) ~ 0, ∂(∂(y(t))) ~ -g]
2-element Vector{Sym}:
  Eq(Derivative(x(t), (t, 2)), 0)
 Eq(Derivative(y(t), (t, 2)), -g)

julia> dsolve(eqns)  # can't solve for initial conditions though! (NotAlgebraic)
2-element Vector{Sym}:
           x(t) = C₁ + C₂⋅t
 Eq(y(t), C3 + C4*t - g*t^2/2)

julia> @syms t x() y()
(t, x, y)

julia> eq = (∂(x)(t) ~ x(t)*y(t)*sin(t), ∂(y)(t) ~ y(t)^2 * sin(t))
(Eq(Derivative(x(t), t), x(t)*y(t)*sin(t)), Eq(Derivative(y(t), t), y(t)^2*sin(t)))
```

```julia
julia> dsolve(eq)  # returns a set to be `collect`ed:
PyObject {Eq(x(t), -exp(C1)/(C2*exp(C1) - cos(t))), Eq(y(t), -1/(C1 - cos(t)))}
```

```julia
julia> dsolve(eq) |> collect
2-element Vector{Any}:
 Eq(x(t), -exp(C1)/(C2*exp(C1) - cos(t)))
               Eq(y(t), -1/(C1 - cos(t)))
```

"""
function dsolve(eqn, args...;
                ics::Union{Nothing, AbstractDict, Tuple}=nothing,
                kwargs...)
    if isa(ics, Tuple) # legacy
        _dsolve(eqn, args...; ics=ics, kwargs...)
    else
        sympy.dsolve(eqn, args...; ics=ics, kwargs...)
    end
end

rhs(x::SymbolicObject) = pycall_hasproperty(x, :rhs) ? x.rhs : x
lhs(x::SymbolicObject) = pycall_hasproperty(x, :lhs) ? x.lhs : x

export dsolve, rhs, lhs

## ---- deprecate ----
## used with ics=(u,0,1) style
function _dsolve(eqn::Sym, args...; ics=nothing, kwargs...)

    Base.depwarn("Use of tuple(s), `(u, x₀, u₀)`, to specify initial conditions is deprecated. Use a dictionary: `ics=Dict(u(x₀) => u₀)`.", :_dsolve)

    if isempty(args)
        var = first(free_symbols(eqn))
    else
        var = first(args)
    end
    # var might be f(x) or x, we want `x`
    if Introspection.classname(var) != "Symbol"
        var = first(var.args)
    end
    ## if we have one initial condition, can be passed in a (u,x0,y0) *or* ((u,x0,y0),)
    ## if more than oneq a tuple of tuples
    if eltype(ics) <: Tuple
        __dsolve(eqn, var, ics; kwargs...)
    else
        __dsolve(eqn, var, (ics,); kwargs...)
        end
end

function __dsolve(eqn::Sym, var::Sym, ics; kwargs...)
    if length(ics) == 0
        throw(ArgumentError("""Some initial value specification is needed.
Specifying the function, as in `dsolve(ex, f(x))`, is deprecated.
Use `sympy.dsolve(ex, f(x); kwargs...)` directly for that underlying interface.
"""))
    end

    out = sympy.dsolve(eqn; kwargs...)
    ord = sympy.ode_order(eqn, var)

    ## `out` may be an array of solutions. If so we do each one.
    ## we want to use an array for output only if needed
    if !isa(out, Array)
        return _solve_ivp(out, var, ics,ord)
    else
        output = Sym[]
        for o in out
            a = _solve_ivp(o, var, ics,ord)
            a != nothing && push!(output, a)
        end
        return length(output) == 1 ? output[1] : output
    end
end

## Helper.
## out is an equation in var with constants. Args are intial conditions
## Return `nothing` if initial condition is not satisfied (found by `solve`)
function _solve_ivp(out, var, args, o)

    eqns = Sym[(diff(out.rhs(), var, f.n))(var=>x0) - y0 for (f, x0, y0) in args]
    sols = solve(eqns, Sym["C$i" for i in 1:o], dict=true)
    if length(sols) == 0
       return nothing
    end

    ## massage output
    ## Might have more than one solution, though unlikely. But if we substitute a variable
    ## for y0 we will get an array back from solve which may have length 1.
    if isa(sols, Array)
        if length(sols) == 1
            sols = sols[1]
        else
            return [out([Pair(k,v) for (k,v) in sol]...) for sol in sols]
        end
    end

    out([Pair(k,v) for (k,v) in sols]...)
end

## For System Of Ordinary Differential Equations
## may need to collect return values
# dsolve(eqs::Union{Array, Tuple}, args...; kwargs...) = sympy.dsolve(eqs, args...; kwargs...)


# # Hypercube Examples (Bravais Lattice)

# ## Preamble
using LatticeTools
using Plots

mkpath("example_hypercube")

function draw_hypercube(hypercube::Hypercube, coordinates::AbstractMatrix{<:Integer})
    xlim = (minimum(coordinates[1,:]) - 3.5, maximum(coordinates[1,:]) + 3.5)
    ylim = (minimum(coordinates[2,:]) - 3.5, maximum(coordinates[2,:]) + 3.5)
    boundary = let
        r0 = [0,0]
        r1 = hypercube.shape_matrix[:,1]
        r2 = hypercube.shape_matrix[:,2]
        hcat(r0, r1, r1 .+ r2, r2, r0)
    end
    fig = plot(boundary[1,:], boundary[2,:], label="", size=(400, 400))
    for Ri in Iterators.product(-1:1, -1:1)
        alpha = (Ri == (0,0)) ? 1.0 : 0.1
        Rr = hypercube.shape_matrix * [Ri...]
        scatter!(coordinates[1,:] .+ Rr[1], coordinates[2,:] .+ Rr[2], aspect_ratio=1, markersize=12, xlim=xlim, ylim=ylim, markeralpha=alpha, label="")
        annotation = []
        for i in 1:size(coordinates, 2)
            x = coordinates[1,i] + Rr[1]
            y = coordinates[2,i] + Rr[2]
            if xlim[1] < x < xlim[2] && ylim[1] < y < ylim[2]
                push!(annotation, (x, y, text("$i", 8, :black, :center)))
            end
        end
        if !isempty(annotation)
            annotation = [annotation...]
            annotate!(annotation)
        end
    end
    fig
end

# ## (4,-4) x (4,4)
size_matrix = [ 4 4; -4 4]
hypercube = Hypercube(size_matrix)
generator_translations = find_generators(hypercube)
coordinates = generate_coordinates(hypercube, generator_translations)
coordmat = hcat(coordinates...)
println("All elements")
for (i, c) in enumerate(coordinates)
    println("$i : $c")
end
println("Generator translations")
for (it, t) in enumerate(eachcol(generator_translations))
    println("t($it) = $t")
end
draw_hypercube(hypercube, coordmat)
savefig("example_hypercube/bravais_(4,-4)x(4,4).svg")

# ![](example_hypercube//bravais_(4,-4)x(4,4).svg)

# ## (2,-2) x (2,4)
size_matrix = [ 2 2; -2 4]
hypercube = Hypercube(size_matrix)
generator_translations = find_generators(hypercube)
coordinates = generate_coordinates(hypercube, generator_translations)
coordmat = hcat(coordinates...)
draw_hypercube(hypercube, coordmat)
println("All elements")
for (i, c) in enumerate(coordinates)
    println("$i : $c")
end
println("Generator translations")
for (it, t) in enumerate(eachcol(generator_translations))
    println("t($it) = $t")
end
draw_hypercube(hypercube, coordmat)
savefig("example_hypercube/bravais_(2,-2)x(2,4).svg")

# ![](example_hypercube//bravais_(2,-2)x(2,4).svg)


using ProgressBars, Plots, StatsBase, Statistics, LaTeXStrings, JLD

function RandomWalker(P,TimeSteps)
    X = 0
    for i in 1:TimeSteps
        X += sample([1,-1], Weights([P,1-P]))
    end
    return X
end

function Linear_fit(X, Y)
    A = [hcat(X) reshape(ones(length(X)), length(X), 1)]
    b = reshape(hcat(Y), length(Y), 1)
    line = (A \ b)
    return line
end

PList = [0.1,0.3,0.5,0.7,0.9]
N = 100000
TimeSteps = 10000
RES = [[] for i = 1:5]

for n in 1:5
    P = PList[n]
    for i in ProgressBar(1:N)
        push!(RES[n],RandomWalker(P,TimeSteps))
    end
    push!(Avg,mean(RES[n]))
end

save("../../Data/Q4/Q4-RW.jld", "Data", RES)

Line = round.(Linear_fit(PList,Avg))

p1 = histogram(RES[1],bins = 50, label = L"P = %$(PList[1]), \ l = \tau = 1", xlabel=L"\langle X_{final} \rangle_{(P)}", ylabel = L"Number", c = :steelblue)
p2 = histogram(RES[2],bins = 50, label = L"P = %$(PList[2]), \ l = \tau = 1", xlabel=L"\langle X_{final} \rangle_{(P)}", ylabel = L"Number", c = :purple)
p3 = histogram(RES[3],bins = 50, label = L"P = %$(PList[3]), \ l = \tau = 1", xlabel=L"\langle X_{final} \rangle_{(P)}", ylabel = L"Number", c = :red)
p4 = histogram(RES[4],bins = 50, label = L"P = %$(PList[4]), \ l = \tau = 1", xlabel=L"\langle X_{final} \rangle_{(P)}", ylabel = L"Number", c = :gold)
p5 = histogram(RES[5],bins = 50, label = L"P = %$(PList[5]), \ l = \tau = 1", xlabel=L"\langle X_{final} \rangle_{(P)}", ylabel = L"Number", c = :green)
p6 = begin
    plot(PList,Avg,label = L"Y = %$(round(Line[1],digits=3)) X %$(round(Line[2],digits=3))", c = :black)
    scatter!(PList,Avg,label = L"Data\ Points",legend = 140,xlabel = L"P",ylabel = L"\langle \overline{X_{final}} \rangle_{(P)}",c = :steelblue)
end

Pmain1 = begin
    plot(plot(plot(p1,p2,layout=(1,2)),plot(p3,p4,layout=(1,2)),plot(p5,p6,layout=(1,2)),layout=(3,1)))
    plot!(plot_title = L"Distribution\ of\ X_{final}\ (N = %$N\ , Time\ Steps=%$TimeSteps)",size = (1000,1000))
end
savefig(Pmain1,"../../Figs/Q4/Q4-tot.pdf")


function readfile(filename, row::Int64)
    return readdlm(filename)[:,row]
end

function pn(x::Y, xvalues::T, yvalues::T , n::Int64) where {T, Y}
    @assert length(xvalues)==length(yvalues)
    l = length(xvalues)
    denom = 1.0
    numer = 1.0
    for i in 1:l
        if i != n
            denom = denom*(xvalues[n]-xvalues[i])
        end
    end
    for i in 1:l
        if i != n
            numer = numer*(x-xvalues[i])
        end
    end
    return yvalues[n]*numer/denom
end

function lagrangeinterpolation(x::Y, xvalues::T,yvalues::T) where {T,Y}
    @assert length(xvalues)==length(yvalues)
    l = length(xvalues)
    sumpn = 0.0
    for i in 1:l
        sumpn = sumpn + pn(x, xvalues, yvalues, i)
    end
    return sumpn
end


#' # Optimizing water networks with Knitro
#'
#' This notebook explains how to solve effectively NLP and MINLP
#' problem with the Knitro solver.
#'
#' The problem studied hereafter was originally designed by
#' Pierre Carpentier, for educational purposes.
#' The original code is available [here](http://perso.ensta-paristech.fr/~pcarpent/TP_Reseau/ENSMP/).
#' The author credits him for the following tutorial.
#'
#' # Description of the problem
#'
#' The tutorial studies the optimization of water flows in a water
#' distribution network (at steady state). The optimizer aims at
#' minimizing the network's energy while statisfying some linear constraints
#' corresponding to Kirchhoff's first law.
#'
#' Let $G = (N, E)$ be an oriented graph. We note $n = |E|$ the number of
#' arcs and $m = |N|$ the number of nodes in this graph.
#'
#' We suppose that the network has $r$ tanks storing some waters to fulfill
#' the demands in $d$ nodes (distinct from the tanks).
#' We split the set of nodes $N$ accordingly:
#' $$
#' N = N_r \cup N_d.
#' $$
#'
#' We suppose further that the graph is *connected*, implying
#' $$
#' n \geq m - 1 .
#' $$
#' We note $A \in R^{m \times n}$ the incidence matrix of the graph.
#'
#' Lets introduce physical variables to describe further the water network.
#' - the vector of resulting flows at nodes, denoted $f = (f_d, f_r)$. $f_d$ is given for demands' nodes.
#' - the vector of pressures at nodes, denoted $p = (p_d, p_r)$. Pressures $p_r$ are given for the reservoirs.
#' - the vector of resistances in all arcs, denoted $r$ (parameters of the problem)
#' - the vector of flows across arcs, denoted $q$.
#'
#' The decision variable is the vector of flows $q$.
#'
#' ### Constraints
#'
#' The first Kirchhoff law states that:
#' $$
#' A q -f = 0,
#' $$
#' as we suppose that no accumulation occurs in the nodes.
#' The second Kirchhoff law takes into account the losses in the pipes,
#' which is given on each arc by a function $\phi_{alpha}$ (corresponding
#' to the Colebrooks law):
#' $$
#' \phi_{\alpha}(q_\alpha) = r_\alpha q_\alpha | q_\alpha |
#' $$
#' The second Kirchhoff law writes, in a vectorial manner,
#' $$
#' A^\top p + r \circ q \circ | q | = 0
#' $$
#'
#' ### Objective
#' On each arc $\alpha$, we define the energy function $\Phi_\alpha$
#' as
#' $$
#' \Phi_\alpha(q_\alpha) = \dfrac{1}{3} r_\alpha q_\alpha^2 | q_\alpha |
#' $$
#' On the graph, the overall energy equates
#' $$
#' J(q, f_r) = \dfrac{1}{3} q^\top (r \circ q \circ | q |) + p_r^\top f_r
#' $$
#'
#' The global problem writes
#' $$
#' \min_{q} \dfrac{1}{3} q^\top (r \circ q \circ | q |) + p_r^\top f_r \qquad
#' s.t. \quad Aq  - f = 0
#' $$
#'
#' ### Reformulation
#' By applying some mathematical tricks, we are able to reformulate
#' the problem in the following manner.
#'
#' By considering some properties of the overall graph, we split the vector
#' of flows in two $q = (q_T, q_C)$, where $q_T$ depends linearly on $q_C$.
#' Then, we are able to prove the existence of a matrix $B$ and a fixed
#' vector $q_0$ such that the vector of flows on arcs writes
#' $$
#' q = q_0 + B \; q_C
#' $$
#' By using this formulation, we can reduce the dimension of the search
#' space by selecting as decision variable the subvector $q_C$ instead of $q$.
#'
#' Note that the problem becomes also constraint-free, as the vector $q$
#' statisfying the previous equation sastifies also the first Kirchhoff law.
#'
#' We reformulate the optimization problem as
#' $$
#' \min_{q_c} \dfrac{1}{3} q^\top (r \circ q \circ | q |) + p_r^\top f_r \qquad
#' s.t. \quad q = q_0 + B q_C
#' $$

#' ---
#' # Numerical resolution
#' We start by importing JuMP and Knitro.

using JuMP, KNITRO

#' Import data.
include("data.jl");

#' Plotting utilities
using Pkg

# Turned off by default.
PLOT_GRAPH = false
if haskey(Pkg.installed(), "PyPlot")
    PLOT_GRAPH = true
    include("utils.jl");
end

#' ---
#' ## First case study: the non-linear problem
#'
#' Define Knitro Optimizer with JuMP.
model = Model(with_optimizer(KNITRO.Optimizer, outlev=3))

#' Write non-linear optimization problem first.
# Parameters.
α1 = Ar' * pr
# Dimension of the problem.
nx = n - md
@variable(model, qc[1:nx])
# Add dummy variable in the model...
@variable(model, q[1:n])
# ... as we restrict the problem inside a 9-dimensional manifold:
@constraint(model, q .== q0 + B*qc)
@NLobjective(model, Min,
             sum(r[i] * abs(q[i]) * q[i]^2 / 3 + α1[i] * q[i] for i in 1:n))
optimize!(model)

#' Display results
if PLOT_GRAPH
    optimal_flow = JuMP.value.(q)
    fig = figure()
    plot_network(flow=optimal_flow)
    display(fig)
end

#' Observe that some arcs seem almost useless here...

#' ---
#' ## Extension to Mixed-integer non-linear programming

#' We were able to quickly formule the NLP problem with JuMP, and Knitro
#' finds a solution in few iterations.
#' We now modify slightly the problem. Consider the $n$ arcs inside the graph.
#' The questions are:
# - *how many arcs in $E$ can we remove before the problem becomes infeasible?*
# - *how much is the cost impacted by arc removals?*

#' We define hereafter the MINLP version of the problem.
# Add a maximum flows through the pipes
const QMAX = 10.

function load_mip_model!(model::JuMP.Model; nremovals=3)
    # Reconsider the NLP problem introduced previously.
    @variable(model, qc[1:nx])
    @variable(model, q[1:n])
    @constraint(model, q .== q0 + B*qc)

    # Introduce switch: $z$ is set to 0 if the corresponding arc is removed
    # from the graph.
    @variable(model, z[1:n], Bin)

    # Bounds the abs flows through the arcs by QMAX
    # The constraints write:
    # | q | <= q_{max} * z
    # Note that z_a = 0 implies that q_a = 0.
    @constraint(model,  q .<= QMAX * z)
    @constraint(model, -q .<= QMAX * z)
    # Ensure that we remove exactly nremovals arcs.
    @constraint(model, sum(z) == n - nremovals)

    # Same cost as previously.
    @NLobjective(model, Min,
                 sum(r[i] * abs(q[i]) * q[i]^2 / 3 + α1[i] * q[i] for i in 1:n))
    return
end

#' **Optimization related remark:** Note that the constraint
#' $$
#' | q | \leq q_{max} \; z
#' $$
#' could be reformulated as a complementarity constraint (also supported by Knitro
#' but not natively by JuMP).

#' **Physics related remark:** We are a bit loosy with the physics of the
#' problem if we remove the arcs in the previous manner. Imagine that for
#' a given arc $a \in E$, $z_a =0$ thus implying $q_a =0$. Then, if we
#' note $i$ and $j$ the two adjacent nodes, the physics tells us that
#' $$
#' p_i = 0 \, \quad p_j = 0 ,
#' $$
#' which is not the case if we solve the previous optimization problem.
#' However, the goal of this tutorial is purely pedagogical and as a
#' consequence we allow us to play a bit with the physics.

#' ### Default Knitro

#' Solve with default Knitro.
# Build non-linear solver.
model = Model(with_optimizer(KNITRO.Optimizer, outlev=3))
load_mip_model!(model)
@time JuMP.optimize!(model)

#' We plot the solution by displaying in red the arcs removed from the graph.
# Plot!
if PLOT_GRAPH
    optimal_flow = JuMP.value.(model[:q])
    fig = figure()
    plot_network(flow=optimal_flow)
    display(fig)
end

#' In the resolving, Knitro uses the Branch & Bound algorithm to find
#' the optimal solution corresponding to the (convex) MINLP problem.
#' We refer to the [documentation](https://www.artelys.com/docs/knitro/2_userGuide/minlp.html)
#' for further details.
#' The approach used here is different than in BONMIN (outer approximation).

#' Observe that Knitro with default options computes 165 nodes before
#' finding the solution.
#' Is it possible to find a better tuning for Knitro?

#' ### MIP-Tuner

#' Since Knitro 12.0, a MINLP tuner was added to compute the optimal
#' parameterization to solve a given MINLP problem with Knitro. The MINLP
#' tuner uses an exhaustive search to find the optimal setting.
#'
#' By default, the tuner tests 36 combinations.
model = Model(with_optimizer(KNITRO.Optimizer, outlev=3, tuner=1))
load_mip_model!(model)
@time JuMP.optimize!(model)

#' Setting `mip_branchrule=2` and `mip_selectrule=3` seems to give
#' better results, according to the tuner.
#' Note that setting `outlev` to 0 switch off Knitro's output.
mip_solver = with_optimizer(KNITRO.Optimizer, mip_knapsack=0, mip_branchrule=2,
                            mip_selectrule=3, outlev=0)
model = Model(mip_solver)
load_mip_model!(model)
@time JuMP.optimize!(model)

#' ### Quantify impact of arcs' removals

#' The fewer arcs, the harder the problem.
#' If `nremovals >= 10`, Knitro takes too long to solve the problem.
#' However, the problem remains feasible till n_removals = 13.
#' Compute for instance the solution of the following problem:
feas_model = Model(with_optimizer(KNITRO.Optimizer))
@variable(feas_model, qc[1:nx])
@variable(feas_model, q[1:n])
@constraint(feas_model, q .== q0 + B*qc)
@variable(feas_model, z[1:n], Bin)
@constraint(feas_model,  q .<= QMAX * z)
@constraint(feas_model, -q .<= QMAX * z)
@objective(feas_model, Min, sum(z))

#' We know study the evolution of the objective cost w.r.t. the
#' number of removals.
max_removals = 9

# Save results in some arrays.
cost_values = Float64[]

for nremove in 1:max_removals
    println("Remove ", nremove, " arcs from graph.")
    model = Model(mip_solver)
    load_mip_model!(model, nremovals=nremove)
    @time JuMP.optimize!(model)

    push!(cost_values, JuMP.objective_value(model))

    # Plot!
    if PLOT_GRAPH
        optimal_flow = JuMP.value.(model[:q])
        fig = figure()
        plot_network(flow=optimal_flow)
        title("Optimal solution with $nremove removals")
        display(fig)
    end
end

#' Plot evolution of costs w.r.t. number of removals.
if PLOT_GRAPH
    fig = figure()
    plot(1:max_removals, cost_values, lw=3, c="k")
    xlabel("#removals")
    ylabel("Objective value")
    display(fig)
end

#' We observe that removing up to 6 arcs does not impact significantly
#' the cost.


using StatsBase, Distributions, Plots; pyplot()

lambda, N = 1, 10^6
xGrid = 0:6

expDist = Exponential(1/lambda)
floorData = counts(convert.(Int,floor.(rand(expDist,N))), xGrid)/N
geomDist = Geometric(1-MathConstants.e^-lambda)

plot( xGrid, floorData,
	line=:stem, marker=:circle,
	c=:blue, ms=10, msw=0, lw=4,
	label="Floor of Exponential")
plot!( xGrid, pdf.(geomDist,xGrid),
	line=:stem, marker=:xcross,
	c=:red, ms=6, msw=0, lw=2,
	label="Geometric", ylims=(0,1),
	xlabel="x", ylabel="Probability")


"""Calculates the 2nd order centred gradient in x-direction on any grid (u,v,T or q).
The size of dudx must be m-1,n compared to m,n = size(u)"""
function ∂x!(dudx::AbstractMatrix,u::AbstractMatrix)
    m,n = size(dudx)
    @boundscheck (m+1,n) == size(u) || throw(BoundsError())

    @inbounds for j ∈ 1:n
        for i ∈ 1:m
            dudx[i,j] = u[i+1,j] - u[i,j]
        end
    end
end

"""Calculates the 2nd order centred gradient in y-direction on any grid (u,v,T or q).
The size of dudy must be m,n-1 compared to m,n = size(u)."""
function ∂y!(dudy::AbstractMatrix,u::AbstractMatrix)
    m,n = size(dudy)
    @boundscheck (m,n+1) == size(u) || throw(BoundsError())

    @inbounds for j ∈ 1:n
        for i ∈ 1:m
            dudy[i,j] = u[i,j+1] - u[i,j]
        end
    end
end

""" ∇² is the 2nd order centred Laplace-operator ∂/∂x^2 + ∂/∂y^2.
The 1/Δ²-factor is omitted and moved into the viscosity coefficient."""
function ∇²!(du::AbstractMatrix,u::AbstractMatrix)
    m, n = size(du)
    @boundscheck (m+2,n+2) == size(u) || throw(BoundsError())

    @inbounds for i ∈ 1:n
        for j ∈ 1:m
            du[j,i] = minus_4*u[j+1,i+1] + u[j,i+1] + u[j+2,i+1] + u[j+1,i] + u[j+1,i+2]
        end
    end
end


module PoissonTests

using Gridap
using Gridap.Algebra
using Gridap.FESpaces
using GridapDistributed
using PartitionedArrays
using Test

function main(parts)

  output = mkpath(joinpath(@__DIR__,"output"))

  domain = (0,4,0,4)
  cells = (4,4)
  model = CartesianDiscreteModel(parts,domain,cells)

  labels = get_face_labeling(model)
  add_tag_from_tags!(labels,"dirichlet",[1,2,3,5,7])
  add_tag_from_tags!(labels,"neumann",[4,6,8])

  Ω = Triangulation(model)
  Γn = Boundary(model,tags="neumann")
  n_Γn = get_normal_vector(Γn)

  k = 2
  u((x,y)) = (x+y)^k
  f(x) = -Δ(u,x)
  g = n_Γn⋅∇(u)

  reffe = ReferenceFE(lagrangian,Float64,k)
  V = TestFESpace(model,reffe,dirichlet_tags="dirichlet")
  U = TrialFESpace(u,V)

  dΩ = Measure(Ω,2*k)
  dΓn = Measure(Γn,2*k)

  a(u,v) = ∫( ∇(v)⋅∇(u) )dΩ
  l(v) = ∫( v*f )dΩ + ∫( v*g )dΓn
  op = AffineFEOperator(a,l,U,V)

  uh = solve(op)
  eh = u - uh
  @test sqrt(sum( ∫(abs2(eh))dΩ )) < 1.0e-9

  # Now with DG
  h = 2
  γ = 10

  V_dg = FESpace(model,reffe,conformity=:L2)

  Λ = Skeleton(model)
  Γd = Boundary(model,tags="dirichlet")

  dΛ = Measure(Λ,2*k)
  dΓd = Measure(Γd,2*k)

  n_Γd = get_normal_vector(Γd)
  n_Λ = get_normal_vector(Λ)

  a_dg(u,v) =
    ∫( ∇(v)⋅∇(u) )*dΩ +
    ∫( (γ/h)*v*u  - v*(n_Γd⋅∇(u)) - (n_Γd⋅∇(v))*u )*dΓd +
    ∫( (γ/h)*jump(v*n_Λ)⋅jump(u*n_Λ) -
       jump(v*n_Λ)⋅mean(∇(u)) -
       mean(∇(v))⋅jump(u*n_Λ) )*dΛ

  l_dg(v) =
    ∫( v*f )*dΩ +
    ∫( v*g )dΓn +
    ∫( (γ/h)*v*u - (n_Γd⋅∇(v))*u )*dΓd

  op = AffineFEOperator(a_dg,l_dg,V_dg,V_dg)
  uh = solve(op)
  eh = u - uh
  @test sqrt(sum( ∫(abs2(eh))dΩ )) < 1.0e-9

end

end # module


"""
    GaussianLikelihood(σ²)

Gaussian likelihood with `σ²` variance. This is to be used if we assume that the
uncertainity associated with the data follows a Gaussian distribution.

```math
    p(y|f) = Normal(y | f, σ²)
```
On calling, this would return a normal distribution with mean `f` and variance σ².
"""
struct GaussianLikelihood{T<:Real}
    σ²::Vector{T}
end

GaussianLikelihood() = GaussianLikelihood(1e-6)

GaussianLikelihood(σ²::Real) = GaussianLikelihood([σ²])

@functor GaussianLikelihood

(l::GaussianLikelihood)(f::Real) = Normal(f, sqrt(first(l.σ²)))

(l::GaussianLikelihood)(fs::AbstractVector{<:Real}) = MvNormal(fs, first(l.σ²) * I)

"""
    HeteroscedasticGaussianLikelihood(l::AbstractLink=ExpLink())

Heteroscedastic Gaussian likelihood.
This is a Gaussian likelihood whose mean and variance are functions of
latent processes.

```math
    p(y|[f, g]) = Normal(y | f, sqrt(l(g)))
```
On calling, this would return a normal distribution with mean `f` and variance `l(g)`.
Where `l` is link going from R to R^+
"""
struct HeteroscedasticGaussianLikelihood{Tl<:AbstractLink}
    invlink::Tl
end

HeteroscedasticGaussianLikelihood() = HeteroscedasticGaussianLikelihood(ExpLink())

function (l::HeteroscedasticGaussianLikelihood)(f::AbstractVector{<:Real})
    return Normal(f[1], sqrt(l.invlink(f[2])))
end

function (l::HeteroscedasticGaussianLikelihood)(fs::AbstractVector)
    return MvNormal(first.(fs), Diagonal(l.invlink.(last.(fs))))
end


using LinearAlgebra

function so3ToVec(so3mat)
    if( size(so3mat)!=(3,3) )
        println("!Warning! : Input matrix should be 3x3.")
        return
    end
    if( !((so3mat[1,1]==so3mat[2,2]==so3mat[3,3]==0)) )
        println("!Warning! : Diagonals should be zero.")
        return
    end
    if( so3mat[2,1]!=-so3mat[1,2] ||
        so3mat[3,1]!=-so3mat[1,3] ||
        so3mat[3,2]!=-so3mat[2,3] )
        println("!Warning! : Entered matrix isn't a skew symmetric matrix.")
        return
    end
    ret_vec = [so3mat[3,2];
               so3mat[1,3];
               so3mat[2,1] ]
        return ret_vec
end

function vecToSo3(omg)
    if( size(omg)!=(3,) )
        println("!Warning! : Input matrix should be 3x1.")
        return
    end

    ret_ss_matrix = [0      -omg[3]   omg[2];
                     omg[3]    0     -omg[1];
                    -omg[2]  omg[1]      0];
    return ret_ss_matrix
end

using LinearAlgebra

function so3ToVec(so3mat)
    if( size(so3mat)!=(3,3) )
        println("!Warning! : Input matrix should be 3x3.")
        return
    end
    if( !((so3mat[1,1]==so3mat[2,2]==so3mat[3,3]==0)) )
        println("!Warning! : Diagonals should be zero.")
        return
    end
    if( so3mat[2,1]!=-so3mat[1,2] ||
        so3mat[3,1]!=-so3mat[1,3] ||
        so3mat[3,2]!=-so3mat[2,3] )
        println("!Warning! : Entered matrix isn't a skew symmetric matrix.")
        return
    end
    ret_vec = [so3mat[3,2];
               so3mat[1,3];
               so3mat[2,1] ]
        return ret_vec
end

function VecToso3(omg)
    if( size(omg)!=(3,) )
        println("!Warning! : Input matrix should be 3x1.")
        return
    end

    ret_ss_matrix = [0      -omg[3]   omg[2];
                     omg[3]    0     -omg[1];
                    -omg[2]  omg[1]      0];
    return ret_ss_matrix
end
#****************************************************************#
# VeysiADN 17 Apr 2021
# Advanced Robotics Homework 4 Codes
#****************************************************************#
using LinearAlgebra
const lAlgebra = LinearAlgebra
#****************************************************************#
# Returns normalized version of given matrix/array
function Normalize(N)
    return N/lAlgebra.norm(N)
end
#****************************************************************#
# Returns 4x4 twist matrix ‘se3mat’ (an element of Lie Algebra se3)
# from given twist vector (= 6x1 vector) ‘xi
function VecTose3( xi )
    w_skew = [ 0   -xi[3]   xi[2];
              xi[3]   0    -xi[1];
             -xi[2]  xi[1]    0 ]
    return vcat(hcat(w_skew,xi[4:6]), zeros(1, 4))
end
#****************************************************************#
# Returns twist vector ‘xi’ from given twist matrix ‘se3mat’
function se3ToVec( se3mat )
    omg = [se3mat[3, 2], se3mat[1, 3],se3mat[2, 1]]
    v = se3mat[1:3, 4]
    return vcat(omg,v)
end
#****************************************************************#
# Calculates and return rotation matrix ‘R’ by
# evaluating matrix exponential of 3x3 skew symmetric matrix ‘so3mat’

function MatrixExp3(so3mat)
        omg = [so3mat[3, 2], so3mat[1, 3],so3mat[2, 1]]
        theta = lAlgebra.norm(omg)
        omg_n = so3mat / theta
        return lAlgebra.I + sin(theta) * omg_n + (1 - cos(theta)) * omg_n * omg_n
end

#****************************************************************#
# Calculates and return transformation matrix ‘g’ by
# evaluating matrix exponential of 4x4 twist matrix ‘se3mat’
function MatrixExp6( se3mat )
    omg = [se3mat[3, 2], se3mat[1, 3],se3mat[2, 1]]
    theta = lAlgebra.norm(omg)
    omg_n = se3mat[1:3, 1:3] / theta
    temp  = hcat(MatrixExp3(se3mat[1:3, 1:3]),
                         (lAlgebra.I * theta +
                          (1 - cos(theta)) * omg_n +
                          (theta - sin(theta)) * omg_n * omg_n) *
                         se3mat[1:3, 4] / theta)
    return    vcat(temp,[0 0 0 1])
end

#****************************************************************#
# VeysiADN 12 May 2021
# Advanced Robotics Homework 5 Codes
#****************************************************************#
using LinearAlgebra
const lAlgebra = LinearAlgebra
#****************************************************************#
# Computes exponential coordinate ‘so3mat’
# (skew symmetric matrix) of the rotation matrix R

function MatrixLog3(R)
    acosinput = (lAlgebra.tr(R) - 1) / 2
    if acosinput >= 1
        return zeros(3, 3)
    else
        theta = acos(acosinput)
        return theta / 2 / sin(theta) * (R - R')
    end
end
#****************************************************************#
# Computes exponential coordinate ‘se3mat’
# (twist matrix) of the rigid body transformation matrix T
function MatrixLog6(T)
    if( size(T)!=(4,4) )
        println("!Warning! : Transformation matrix should be 4x4.")
        return
    end
    R = T[1:3,1:3]
    t = T[1:3,4]
    omg_h = MatrixLog3(R)
    w = [omg_h[3,2];omg_h[1,3];omg_h[2,1]]
    if omg_h == zeros(3, 3)
        return vcat(hcat(zeros(3, 3), T[1:3, 4]), [0 0 0 0])
    else
        theta = acos((lAlgebra.tr(R) - 1) / 2)
        return vcat(hcat(omg_h,
                         (I - omg_h / 2 +
                          (1 / theta - 1 / tan(theta / 2) / 2) *
                          omg_h * omg_h / theta) * T[1:3, 4]),
                    [0 0 0 0])
    end
end
#****************************************************************#

#****************************************************************#
# VeysiADN 12 May 2021
# Advanced Robotics Homework 5 Codes
#****************************************************************#
using LinearAlgebra
#****************************************************************#
# Returns Adjoint matrix with given rigid body
# transformation matrix g
function Adjoint(g)

    R=g[1:3,1:3];
    p=g[1:3,4];
    p_hat = [ 0       -p[3]     p[2];
             p[3]       0      -p[1];
            -p[2]      p[1]       0 ]

    return [R zeros(3,3);p_hat*R R]
end
#****************************************************************#
# VeysiADN 4 Apr 2021
# Advanced Robotics Homework 3 Codes
#****************************************************************#
using LinearAlgebra
#****************************************************************#
# Returns 3 x 3 rotation matrix representing
# rotation about unit vector w by angle theta.
# theta should be in radian
function Rodrigues(w,theta)
    if( length(w) != 3)
        println("!Warning! : W matrix should have 3 elements.")
        return
    end
    if(size(w)==(1,3))
        w=w';
    end
    w_hat = [0      -w[3]    w[2];
             w[3]     0     -w[1];
            -w[2]    w[1]      0];
    rotation_matrix = cos(theta)*Matrix(1I,3,3) + sin(theta)*w_hat+(1-cos(theta))*w*w';
    return rotation_matrix;
end
#****************************************************************#
# Builds rigid transformation matrix T from given
#rotation matrix R and translation t
function RpToTrans(R,t)
    if( size(R)!=(3,3) )
        println("!Warning! : Rotation matrix should be 3x3.")
        return
    end
    if( length(t) != 3 )
        println("!Warning! : Translation matrix should have 3 elements.")
        return
    end
    if(size(t)==(1,3))
        t=t';
    end
    R_B_T = hcat(R,t) ;
    R_B_T = vcat(R_B_T,[0 0 0 1]) ;
    return R_B_T
end
#****************************************************************#
#Extracts rotation matrix R and translation t from given /
# rigid transformation matrix T
function TransToRp(T)
    if( size(T)!=(4,4) )
        println("!Warning! : Transformation matrix should be 4x4.")
        return
    end
    R = T[1:3,1:3]
    t = T[1:3,4]
    return R,t;
end
#****************************************************************#
# Inverts rigid transformation matrix T
function TransInv(T)
    if( size(T)!=(4,4) )
        println("!Warning! : W matrix should be 4x4.")
        return
    end
    if( det(T)==0 )
        println("!Warning! : Inverse doesn't exist (Determinant is equal to zero).")
        return
    end
    R,t = TransToRp(T);
    r_inv =hcat(R',-(R'*t))
    r_inv = vcat(r_inv,[0 0 0 1]);
    return r_inv;
end
#****************************************************************#


# nuclear Norm (times a constant)

"""
  NuclearNorm(λ::Real=1.0)

Returns the function `λ∑σ_i(X)`, where `σ_i(X)` is i-th singular value of matrix X.
"""

immutable NuclearNorm{R <: Real} <: ProximableConvex
  lambda::R
  function NuclearNorm(lambda::R)
    if lambda < 0
      error("parameter λ must be nonnegative")
    else
      new(lambda)
    end
  end
end

NuclearNorm{R <: Real}(lambda::R=1.0) = NuclearNorm{R}(lambda)

function (f::NuclearNorm){T <: RealOrComplex}(X::AbstractArray{T,2})
  U, S, V = svd(X);
  return f.lambda * sum(S);
end

function prox!{T <: RealOrComplex}(f::NuclearNorm, X::AbstractArray{T,2}, Y::AbstractArray{T,2}, gamma::Real=1.0)
  U, S, V = svd(X)

  for i in eachindex(S)
    S[i] = max(0, S[i] - f.lambda*gamma);
  end

  Y[:] = U * diagm(S) * V'
  return f.lambda * sum(S);
end

fun_name(f::NuclearNorm) = "nuclear norm"
fun_dom(f::NuclearNorm) = "AbstractArray{Real,2}, AbstractArray{Complex,2}"
fun_expr(f::NuclearNorm) = "X ↦ λ∑σ_i(X)"
fun_params(f::NuclearNorm) = "λ = $(f.lambda)"

function prox_naive{T <: RealOrComplex}(f::NuclearNorm, X::AbstractArray{T,2}, gamma::Real=1.0)
  U,S,V = svd(X)
  ftemp = NormL1(1.0)
  S_γ, fS_γ =  prox(ftemp,S,f.lambda*gamma)
  Y = U * diagm(S_γ) * V'
  return Y, f.lambda * sum(S_γ)
end


using DifferentialEquations
using DiffEqCallbacks
using Distributions
using StatsBase
using Random
using DataFrames
using StatsPlots
using BenchmarkTools

function rate_to_proportion(r::Float64,t::Float64)
    1-exp(-r*t)
end;

@enum InfectionStatus Susceptible Infected Recovered

Base.zero(::Type{InfectionStatus}) = Infected

function sir_abm!(du,u,p,t)
    (β,c,γ,δt) = p
    N = length(u)
    # Initialize du to u
    for i in 1:N
        du[i] = u[i]
    end
    for i in 1:N # loop through agents
        # If recovered
        if u[i]==Recovered
            continue
        # If susceptible
        elseif u[i]==Susceptible
            ncontacts = rand(Poisson(c*δt))
            while ncontacts > 0
                j = sample(1:N)
                if j==i
                    continue
                end
                a = u[j]
                if a==Infected && rand() < β
                    du[i] = Infected
                    break
                end
                ncontacts -= 1
            end
        # If infected
        else u[i]==Infected
            if rand() < γ
                du[i] = Recovered
            end
        end
    end
    nothing
end;

δt = 0.1
tf = 40.0
tspan = (0.0,tf);

β = 0.05
c = 10.0
γ = rate_to_proportion(0.25,δt)
p = [β,c,γ,δt];

N = 1000
I0 = 10
u0 = Array{InfectionStatus}(undef,N)
for i in 1:N
    if i <= I0
        s = Infected
    else
        s = Susceptible
    end
    u0[i] = s
end

Random.seed!(1234);

susceptible(u) = count(i == Susceptible for i in u)
infected(u) = count(i == Infected for i in u)
recovered(u) = count(i == Recovered for i in u);

saved_values = SavedValues(Float64, Tuple{Int64,Int64,Int64})
cb = SavingCallback((u,t,integrator)->(susceptible(u),infected(u),recovered(u)),
    saved_values,
    saveat=0:δt:tf);

prob_abm = DiscreteProblem(sir_abm!,u0,tspan,p);

sol_abm = solve(prob_abm,
    solver = FunctionMap(),
    dt = δt,
    callback = cb,
    dense = false,
    save_on = false);

df_abm = DataFrame(saved_values.saveval)
rename!(df_abm,[:S,:I,:R])
df_abm[!,:t] = saved_values.t;

@df df_abm plot(:t,
    [:S :I :R],
    label=["S" "I" "R"],
    xlabel="Time",
    ylabel="Number")

@benchmark solve(prob_abm,
    solver=FunctionMap,
    dt=δt,
    callback=cb,
    dense=false,
    save_on=false)