function isinplace_jump(p,rj)
    if p isa DiscreteProblem && p.f === DiffEqBase.DISCRETE_INPLACE_DEFAULT && rj !== nothing
        # Just a default discrete problem f, so don't use it for iip
        DiffEqBase.isinplace(rj)
    else
        DiffEqBase.isinplace(p)
    end
end

"""
$(TYPEDEF)

Defines a collection of jump processes to associate with another problem type.
- [Documentation Page](https://diffeq.sciml.ai/stable/types/jump_types/)
- [Tutorial
  Page](https://diffeq.sciml.ai/stable/tutorials/discrete_stochastic_example/)
- [FAQ
  Page](https://diffeq.sciml.ai/stable/tutorials/discrete_stochastic_example/#FAQ)

### Constructors

`JumpProblem`s can be constructed by first building another problem type to
which the jumps will be associated. For example, to  simulate a collection of
jump processes for which the transition rates do not explicitly depend on time
(called `ConstantRateJump`s or `MassActionJump`s), we must first construct a
`DiscreteProblem`
```julia
prob = DiscreteProblem(u0, p, tspan)
```
where `u0` is the initial condition, `p` the parameters and `tspan` the time
span. If we wanted to have the jumps coupled with a system of ODEs, or have
transition rates with explicit time dependence, we would use an `ODEProblem`
instead that defines the ODE portion of the dynamics.

Given `prob` we define the jumps via
- `JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpSet ;
  kwargs...)`
- `JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps...;
  kwargs...)`

Here `aggregator` specifies the underlying algorithm for calculating next jump
times and types, for example `Direct`. The collection of different
`AbstractJump` types can then be passed within a single `JumpSet` or as
subsequent sequential arguments.

### Fields

$(FIELDS)

## Keyword Arguments
- `rng`, the random number generator to use. On 1.7 and up defaults to Julia's
  builtin generator, below 1.7 uses RandomNumbers.jl's
  `Xorshifts.Xoroshiro128Star(rand(UInt64))`.
- `save_positions=(true,true)`, specifies whether to save the system's state
  (before,after) the jump occurs.
- `spatial_system`, for spatial problems the underlying spatial structure.
- `hopping_constants`, for spatial problems the spatial transition rate
  coefficients.

Please see the [tutorial
page](https://diffeq.sciml.ai/stable/tutorials/discrete_stochastic_example/) in
the DifferentialEquations.jl [docs](https://diffeq.sciml.ai/stable/) for usage
examples and commonly asked questions.
"""
mutable struct JumpProblem{iip,P,A,C,J<:Union{Nothing,AbstractJumpAggregator},J2,J3,J4,R} <: DiffEqBase.AbstractJumpProblem{P,J}
  """The type of problem to couple the jumps to. For a pure jump process use `DiscreteProblem`, to couple to ODEs, `ODEProblem`, etc."""
  prob::P
  """The aggregator algorithm that determines the next jump times and types for `ConstantRateJump`s and `MassActionJump`s. Examples include `Direct`."""
  aggregator::A
  """The underlying state data associated with the chosen aggregator."""
  discrete_jump_aggregation::J
  """`CallBackSet` with the underlying `ConstantRate` and `VariableRate` jumps."""
  jump_callback::C
  """The `VariableRateJump`s."""
  variable_jumps::J2
  """The `RegularJump`s."""
  regular_jump::J3
  """The `MassActionJump`s."""
  massaction_jump::J4
  """The random number generator to use."""
  rng::R
end
function JumpProblem(p::P,a::A,dj::J,jc::C,vj::J2,rj::J3,mj::J4,rng::R) where {P,A,J,C,J2,J3,J4,R}
    iip = isinplace_jump(p,rj)
    JumpProblem{iip,P,A,C,J,J2,J3,J4,R}(p,a,dj,jc,vj,rj,mj,rng)
end

# for remaking
Base.@pure remaker_of(prob::T) where {T <: JumpProblem} = DiffEqBase.parameterless_type(T)
function DiffEqBase.remake(thing::JumpProblem; kwargs...)
  T = remaker_of(thing)

  errmesg = """
  JumpProblems can currently only be remade with new u0, p, tspan or prob fields. To change other fields create a new JumpProblem. Feel free to open an issue on DiffEqJump to discuss further.
  """
  !issubset(keys(kwargs),(:u0,:p,:tspan,:prob)) && error(errmesg)

  if :prob ∉ keys(kwargs)
    dprob = DiffEqBase.remake(thing.prob; kwargs...)

    # if the parameters were changed we must remake the MassActionJump too
    if (:p ∈ keys(kwargs)) && using_params(thing.massaction_jump)
      update_parameters!(thing.massaction_jump, dprob.p; kwargs...)
    end
  else
    any(k -> k in keys(kwargs), (:u0,:p,:tspan)) && error("If remaking a JumpProblem you can not pass both prob and any of u0, p, or tspan.")
    dprob = kwargs[:prob]

    # we can't know if p was changed, so we must remake the MassActionJump
    if using_params(thing.massaction_jump)
      update_parameters!(thing.massaction_jump, dprob.p; kwargs...)
    end
  end

  T(dprob, thing.aggregator, thing.discrete_jump_aggregation, thing.jump_callback,
     thing.variable_jumps, thing.regular_jump, thing.massaction_jump, thing.rng)
end

DiffEqBase.isinplace(::JumpProblem{iip}) where {iip} = iip
JumpProblem(prob::JumpProblem) = prob

JumpProblem(prob,jumps::ConstantRateJump;kwargs...) = JumpProblem(prob,JumpSet(jumps);kwargs...)
JumpProblem(prob,jumps::VariableRateJump;kwargs...) = JumpProblem(prob,JumpSet(jumps);kwargs...)
JumpProblem(prob,jumps::RegularJump;kwargs...) = JumpProblem(prob,JumpSet(jumps);kwargs...)
JumpProblem(prob,jumps::MassActionJump;kwargs...) = JumpProblem(prob,JumpSet(jumps);kwargs...)
JumpProblem(prob,jumps::AbstractJump...;kwargs...) = JumpProblem(prob,JumpSet(jumps...);kwargs...)

JumpProblem(prob,aggregator::AbstractAggregatorAlgorithm,jumps::ConstantRateJump;kwargs...) = JumpProblem(prob,aggregator,JumpSet(jumps);kwargs...)
JumpProblem(prob,aggregator::AbstractAggregatorAlgorithm,jumps::VariableRateJump;kwargs...) = JumpProblem(prob,aggregator,JumpSet(jumps);kwargs...)
JumpProblem(prob,aggregator::AbstractAggregatorAlgorithm,jumps::RegularJump;kwargs...) = JumpProblem(prob,aggregator,JumpSet(jumps);kwargs...)
JumpProblem(prob,aggregator::AbstractAggregatorAlgorithm,jumps::AbstractMassActionJump;kwargs...) = JumpProblem(prob,aggregator,JumpSet(jumps);kwargs...)
JumpProblem(prob,aggregator::AbstractAggregatorAlgorithm,jumps::AbstractJump...;kwargs...) = JumpProblem(prob,aggregator,JumpSet(jumps...);kwargs...)
JumpProblem(prob,jumps::JumpSet;kwargs...) = JumpProblem(prob,NullAggregator(),jumps;kwargs...)

function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpSet;
                     save_positions = typeof(prob) <: DiffEqBase.AbstractDiscreteProblem ? (false,true) : (true,true),
                     rng = DEFAULT_RNG, scale_rates = true, useiszero = true, spatial_system=nothing, hopping_constants=nothing, kwargs...)

  # initialize the MassActionJump rate constants with the user parameters
  if using_params(jumps.massaction_jump)
    rates = jumps.massaction_jump.param_mapper(prob.p)
    maj = MassActionJump(rates, jumps.massaction_jump.reactant_stoch, jumps.massaction_jump.net_stoch,
                         jumps.massaction_jump.param_mapper; scale_rates=scale_rates, useiszero=useiszero,
                         nocopy=true)
  else
    maj = jumps.massaction_jump
  end

  ## Spatial jumps handling
  if spatial_system !== nothing && hopping_constants !== nothing && !is_spatial(aggregator) # check if need to flatten
    prob, maj = flatten(maj, prob, spatial_system, hopping_constants; kwargs...)
  end
  ## Constant Rate Handling
  t,end_time,u = prob.tspan[1],prob.tspan[2],prob.u0
  if (typeof(jumps.constant_jumps) <: Tuple{}) && (maj === nothing) && !is_spatial(aggregator) # check if there are no jumps
    disc = nothing
    constant_jump_callback = CallbackSet()
  else
    disc = aggregate(aggregator,u,prob.p,t,end_time,jumps.constant_jumps,maj,save_positions,rng; spatial_system = spatial_system, hopping_constants = hopping_constants, kwargs...)
    constant_jump_callback = DiscreteCallback(disc)
  end

  iip = isinplace_jump(prob, jumps.regular_jump)

  ## Variable Rate Handling
  if typeof(jumps.variable_jumps) <: Tuple{}
    new_prob = prob
    variable_jump_callback = CallbackSet()
  else
    new_prob = extend_problem(prob,jumps; rng=rng)
    variable_jump_callback = build_variable_callback(CallbackSet(),0,jumps.variable_jumps...; rng=rng)
  end
  callbacks = CallbackSet(constant_jump_callback,variable_jump_callback)

  JumpProblem{iip,typeof(new_prob),typeof(aggregator),typeof(callbacks),
              typeof(disc),typeof(jumps.variable_jumps),
              typeof(jumps.regular_jump),typeof(maj),typeof(rng)}(
                        new_prob,aggregator,disc,
                        callbacks,
                        jumps.variable_jumps,
                        jumps.regular_jump, maj, rng)
end

function extend_problem(prob::DiffEqBase.AbstractDiscreteProblem, jumps; rng=DEFAULT_RNG)
  error("VariableRateJumps require a continuous problem, like an ODE/SDE/DDE/DAE problem.")
end

function extend_problem(prob::DiffEqBase.AbstractODEProblem,jumps; rng=DEFAULT_RNG)
  function jump_f(du::ExtendedJumpArray,u::ExtendedJumpArray,p,t)
    prob.f(du.u,u.u,p,t)
    update_jumps!(du,u,p,t,length(u.u),jumps.variable_jumps...)
  end
  ttype = eltype(prob.tspan)
  u0 = ExtendedJumpArray(prob.u0,[-randexp(rng,ttype) for i in 1:length(jumps.variable_jumps)])
  remake(prob,f=ODEFunction{true}(jump_f),u0=u0)
end

function extend_problem(prob::DiffEqBase.AbstractSDEProblem,jumps; rng=DEFAULT_RNG)
  function jump_f(du,u,p,t)
    prob.f(du.u,u.u,p,t)
    update_jumps!(du,u,p,t,length(u.u),jumps.variable_jumps...)
  end

  if prob.noise_rate_prototype === nothing
    jump_g = function (du,u,p,t)
      prob.g(du.u,u.u,p,t)
    end
  else
    jump_g = function (du,u,p,t)
      prob.g(du,u.u,p,t)
    end
  end

  ttype = eltype(prob.tspan)
  u0 = ExtendedJumpArray(prob.u0,[-randexp(rng,ttype) for i in 1:length(jumps.variable_jumps)])
  remake(prob,f=SDEFunction{true}(jump_f,jump_g),g=jump_g,u0=u0)
end

function extend_problem(prob::DiffEqBase.AbstractDDEProblem,jumps; rng=DEFAULT_RNG)
  jump_f = function (du,u,h,p,t)
    prob.f(du.u,u.u,h,p,t)
    update_jumps!(du,u,p,t,length(u.u),jumps.variable_jumps...)
  end
  ttype = eltype(prob.tspan)
  u0 = ExtendedJumpArray(prob.u0,[-randexp(rng,ttype) for i in 1:length(jumps.variable_jumps)])
  ramake(prob,f=DDEFunction{true}(jump_f),u0=u0)
end

# Not sure if the DAE one is correct: Should be a residual of sorts
function extend_problem(prob::DiffEqBase.AbstractDAEProblem,jumps; rng=DEFAULT_RNG)
  jump_f = function (out,du,u,p,t)
    prob.f(out.u,du.u,u.u,t)
    update_jumps!(du,u,t,length(u.u),jumps.variable_jumps...)
  end
  ttype = eltype(prob.tspan)
  u0 = ExtendedJumpArray(prob.u0,[-randexp(rng,ttype) for i in 1:length(jumps.variable_jumps)])
  remake(prob,f=DAEFunction{true}(jump_f),u0=u0)
end

function build_variable_callback(cb,idx,jump,jumps...; rng=DEFAULT_RNG)
  idx += 1
  condition = function (u,t,integrator)
    u.jump_u[idx]
  end
  affect! = function (integrator)
    jump.affect!(integrator)
    integrator.u.jump_u[idx] = -randexp(rng,typeof(integrator.t))
  end
  new_cb = ContinuousCallback(condition,affect!;
                      idxs = jump.idxs,
                      rootfind = jump.rootfind,
                      interp_points = jump.interp_points,
                      save_positions = jump.save_positions,
                      abstol = jump.abstol,
                      reltol = jump.reltol)
  build_variable_callback(CallbackSet(cb,new_cb),idx,jumps...; rng=DEFAULT_RNG)
end

function build_variable_callback(cb,idx,jump; rng=DEFAULT_RNG)
  idx += 1
  condition = function (u,t,integrator)
    u.jump_u[idx]
  end
  affect! = function (integrator)
    jump.affect!(integrator)
    integrator.u.jump_u[idx] = -randexp(rng,typeof(integrator.t))
  end
  new_cb = ContinuousCallback(condition,affect!;
                      idxs = jump.idxs,
                      rootfind = jump.rootfind,
                      interp_points = jump.interp_points,
                      save_positions = jump.save_positions,
                      abstol = jump.abstol,
                      reltol = jump.reltol)
  CallbackSet(cb,new_cb)
end

aggregator(jp::JumpProblem{P,A,C,J,J2}) where {P,A,C,J,J2} = A

@inline function extend_tstops!(tstops,jp::JumpProblem{P,A,C,J,J2}) where {P,A,C,J,J2}
  !(typeof(jp.jump_callback.discrete_callbacks) <: Tuple{}) && push!(tstops,jp.jump_callback.discrete_callbacks[1].condition.next_jump_time)
end

@inline function update_jumps!(du,u,p,t,idx,jump)
  idx += 1
  du[idx] = jump.rate(u.u,p,t)
end

@inline function update_jumps!(du,u,p,t,idx,jump,jumps...)
  idx += 1
  du[idx] = jump.rate(u.u,p,t)
  update_jumps!(du,u,p,t,idx,jumps...)
end

### Displays
num_constant_rate_jumps(aggregator::AbstractSSAJumpAggregator) = length(aggregator.rates)

Base.summary(io::IO, prob::JumpProblem) = string(DiffEqBase.parameterless_type(prob)," with problem ",DiffEqBase.parameterless_type(prob.prob)," and aggregator ",typeof(prob.aggregator))
function Base.show(io::IO, mime::MIME"text/plain", A::JumpProblem)
  println(io,summary(A))
  println(io,"Number of constant rate jumps: ",A.discrete_jump_aggregation === nothing ? 0 : num_constant_rate_jumps(A.discrete_jump_aggregation))
  println(io,"Number of variable rate jumps: ",length(A.variable_jumps))
  if A.regular_jump !== nothing
    println(io,"Have a regular jump")
  end
  if (A.massaction_jump !== nothing) && (get_num_majumps(A.massaction_jump) > 0)
    println(io,"Have a mass action jump")
  end
end

TreeViews.hastreeview(x::JumpProblem) = true


# These Symata functions (symbols) translate between Symata and Julia data and expressions

### JVar

@sjdoc JVar """
    JVar(x)

return the Julia value of the Symbol that `x` evaluates to.

For example, if `a = 1` in Julia and `b = a` in Symata, then `JVar(b)` evaluates to `1`.
"""

@sjseealso_group(Jxpr, JVar)
apprules(mx::Mxpr{:JVar}) = Core.eval(Main,symname(mx[1]))

### SetJ

@mkapprule SetJ nargs => 2  """
    SetJ(x,val)

sets the Julia symbol `x` to `val`.

Variables and functions in Symata are separate from those in Julia, ie, their table of bindings to symbols are separate.
"""

exportj(lhs::SymString) = exportj(lhs,doeval(lhs))
## FIXME: find out what we want on the next line and do it properly.
exportj(lhs::SymString, rhs) = Main.eval(Expr(:(=),symname(Symbol(lhs)),rhs))
exportj(lhs::SymString, rhs::Symbol) = exportj(lhs,QuoteNode(rhs))
@doap SetJ(lhs::SymString,rhs) = exportj(lhs,rhs)

### ExportJ

@mkapprule ExportJ """
    ExportJ(x,y,...)

sets the Julia symbols `x`,`y`, etc. to the Symata-value of `x`,`y`, etc.
`ExportJ(x)` is equivalent to `SetJ(x,x)`.
"""

@doap ExportJ(vars::SymString...) = foreach(exportj, vars)

### Jxpr

@sjdoc Jxpr """
    Jxpr

allows embedding Julia expressions. A Jxpr is entered like this `J( expr )`.

`expr` is interpreted as a Julia expression and it is wrapped expression with head `Jxpr`, which is then evaluated when
`Jxpr` is evaluated. You never see the head `Jxpr`. For example,
 `m = J( collect(1:10) )`  creates a Julia array and binds it to the Symata symbol `m`.
"""

@sjexamp( Jxpr,
         "This creates a Julia Array{Int,1} and \"binds\" it to the Symata symbol m.",
         ("m = J( collect(1:3) )",
          "3-element Array{Int64,1}:\n 1\n 2\n 3"))

@sjexamp( Jxpr,
         "Call a Julia function",
         ("tf = J( time )",""),
         ("tf()","1.424287593897437e9"))

# quote, i.e. :( expr ) is parsed as a Julia expression and is wrapped as  < -- we will remove this
# Mxpr with head Jxpr. It is evaluated here.
@mkapprule Jxpr :nodefault => true

@doap function Jxpr(args...)
    local res
    for x in args
        res = eval(x)
    end
    res
end

@sjdoc J """
    J(expr1,expr2,...)

Evaluate Julia expressions `expr1`, `expr2`, ... in Julia and return the final result.
"""

@sjseealso_group(J, Compile, SymataCall)

#### Unpack

@sjdoc Unpack """
    Unpack(a)

unpack a Julia typed `AbstractArray` into an Symata `List` expression.

Only `1`-d is supported. If `a` is a Julia `Dict`, then a `List` of `Lists` of
key,value pairs is returned.
"""

@sjexamp( Unpack,
         "This creates a List of three random Float64's.",
         ("Unpack( J(rand(3)) )", "[0.5548766917324894,0.034964001133465095,0.9122052258982192]"))

@mkapprule Unpack :nargs => 1

@doap Unpack(a::AbstractArray) = unpacktoList(a)

"""
    unpacktoList(obj::AbstractArray{T,1})

Converts a Julia array to a Symata list of type `ListT`. Only one-dimensional
arrays are supported.
"""
function unpacktoList(obj)
    mx = mxpr(:List, do_unpack(obj))
    setfixed(mx)
    setcanon(mx)
    return mx
end
do_unpack(obj) = copyto!(newargs(length(obj)),obj)

## We copy this and change it a bit to tolistoflists in mxpr_utils
function do_unpack(dict::Dict)
    args = newargs(length(dict))
    i = 0
    for (k,v) in dict
        i += 1
        args[i] = mxpr(:List,k,v)
    end
    return args
end

#### Pack

@sjdoc Pack """
    Pack(expr)

pack the arguments of the Symata expression `expr` into a Julia array,
whose element type is the minimum required to holds the arguments of `expr`.
"""

@sjexamp( Pack,
         "This returns a Julia array of element type Int [1,2,3].",
         ("ClearAll(f)",""),
         ("Pack(f(1,2,3))","3-element Array{Int64,1}: [1,2,3]"))

@sjseealso_group(Pack,Unpack)

"""
    typejoin_array(a::Array)

return the typejoin of all elements in `a`.
I saw this in Base somewhere. Probably more efficient there.
"""
function typejoin_array(a)
    length(a) == 0 && return Any
    T = typeof(a[1])
@inbounds  for i in 2:length(a)
        T = typejoin(T, typeof(a[i]))
    end
    T
end

# 1-d unpack
function apprules(mx::Mxpr{:Pack})
    a = margs(margs(mx)[1])
    T = typejoin_array(a)
    do_pack(T,a)
end

do_pack(T,sjobj) = copyto!(Array{T}(undef, length(sjobj)), sjobj)

#### Translate Symata to Julia

##
# Wrap Expr to prevent Symata from evaluating it.

abstract type AbstractMtoE end

mutable struct MtoECompile <: AbstractMtoE
end

mutable struct MtoEPlain <: AbstractMtoE
end

mxpr_to_expr(x, aux::AbstractMtoE) = x

# Don't really want to do this with everything !
function mxpr_to_expr(s::Symbol, aux::AbstractMtoE)
    s
#    Symbol(lowercase(string(s)))
end

function mxpr_to_expr(s::Symbol,aux::MtoEPlain)
    Symbol(lowercase(string(s)))  # only lowercase some
end

const MTOJSYM_COMPILE = Dict(
    :Times => :mmul,
    :Plus => :mplus,
    :Power => :mpow,
    :Abs => :mabs,
    :Exp => :Exp  # Is this necessary ?
)

mtojsym_compile(s::Symbol) =  get(MTOJSYM_COMPILE, s, mtojsym(s))

const MTOJSYM_PLAIN = Dict(
                             :Times => :(*),
                             :Plus => :(+),
                             :Power => :(^),
                             :Abs => :abs,
                             :E => :e
                             )

function mtojsym_plain(s::Symbol)
    Symbol(lowercase(string(get(MTOJSYM_PLAIN, s, mtojsym(s)))))
end

function mxpr_to_expr(mx::Mxpr,aux::MtoECompile)
    head = mtojsym_compile(mhead(mx))
    mxpr_to_expr_body(head,mx,aux)
end

function mxpr_to_expr(mx::Mxpr{:Power}, aux::MtoECompile)
    if mx[1] == :E
        #        return Expr(:call, :Exp, margs(mx)[2:end]...)
        return Expr(:call, :Exp, [mxpr_to_expr(x,aux) for x in margs(mx)[2:end]]...)
    else
#        return Expr(:call, :mpow, margs(mx)[1:end]...)
        return Expr(:call, :mpow, [mxpr_to_expr(x,aux) for x in margs(mx)]...)
    end
end

# convert List to Julia array
function mxpr_to_expr(mx::Mxpr{:List},aux::MtoECompile)
    Expr(:vect, [mxpr_to_expr(x,aux) for x in margs(mx)]...)
end

function mxpr_to_expr(mx::Mxpr,aux::MtoEPlain)
    head = mtojsym_plain(mhead(mx))
    mxpr_to_expr_body(head,mx,aux)
end

function mxpr_to_expr_body(head,mx::Mxpr,aux::AbstractMtoE)
    isempty(mx) && return :( $(head)() )
    a = margs(mx)
    a1 = mxpr_to_expr(a[1],aux)
    ex = :( $(head)($(a1)) )
    length(mx) == 1 && return ex
    for i in 2:length(a)
        push!(ex.args, mxpr_to_expr(a[i],aux))
    end
    ex
end

#### Compile

@mkapprule Compile """
    f = Compile(expr)

convert `expr` to a compiled function.

    f = Compile( [x,y,...], expr)

create an anonymous Julia function `(x,y,...) -> expr` from Symata expression `expr` and bind
the result to `f`.

```
symata> f = Compile(x^2 + y^2)
symata> f(3,4)
        25
```

Compile works by creating an anonymous Julia function from the Symata expression `expr`. This anonymous
function is just-in-time compiled for the arguments that it is called with.
If no arguments are specified, then the function signature is the sorted list of free symbols found in `expr`. Free symbols are all symbols
that are not bound in the Julia. More precisely, in the Symata module scope. This
excludes `e`, for instance. Alternatively, the arguments may be specified, for example `Compile( [x,y], expr)`.

By default `expr` is not evaluated. This works for literal expressions, such as `x^2`. If the expression must be evaluated, wrap it
in `Evaluate`. For example

```
symata 1> expr = x^2 + y^3
Out(1) = x^2 + y^3

symata 2> f = Compile(Evaluate(expr))
Out(2) = (::#1) (generic function with 1 method)

symata 3> f(2,3)
Out(3) = 31
```
"""
@doap function Compile(a::Mxpr{:List}, body)
    aux = MtoECompile()
    jexpr = Expr(:function, Expr(:tuple, [mxpr_to_expr(x, aux) for x in margs(a)]...) , mxpr_to_expr(body, aux))
    Core.eval(Main, jexpr)
end

# This is not reliable. It is not worth the complexity of fixing it, I think.
# We require an explicit list of variables, instead.
# We could look at Attributes, Protected, etc. and just use plain variables.
# @doap function Compile(body)
#     aux = MtoECompile()
#     jbody = mxpr_to_expr(body,aux)
#     Core.eval(Main, Expr(:function, Expr(:tuple, freesyms(jbody)...), jbody))
# end

## NOTE: freesyms is unrelated to getfreesyms and setfreesyms
"""
    freesyms(ex::Expr)

return a list of Symata-unbound symbols at any depth in expression `ex`, by
walking through the expression. This may not be useful anymore.
"""
function freesyms(x)
    syms = Dict()
    freesyms!(x, syms)
    sort(collect(keys(syms)))
end

function freesyms!(ex::Expr, syms::Dict)
    a = ( ex.head == :call ?  view(ex.args,2:length(ex.args)) : ex.args )
    foreach(x -> freesyms!(x,syms), a)
end

# We have been using isdefined for a long time,
# but this is incorrect. We want isbound, which
# checks for Symata binding.
# Neither is correct. For instance, E is not Symata bound.
# E is bound in Julia Main. (we did it). So, the fact
# that this was worked for several examples, is fortuitous.
function freesyms!(s::Symbol, syms)
#    if ! isdefined(Symata,s)
    if ! isbound(s)
        syms[s] = 1
    end
end

freesyms!(x, syms) = nothing

####  ToJuliaExpression

@mkapprule ToJuliaExpression

@doap function ToJuliaExpression(x)
    mxpr(:JuliaExpression, mxpr_to_expr(x, MtoEPlain()))
end

"""
    @symExpr(expr)

Symata-evaluates `expr`, translates the result to a Julia expression, and inserts it into the surrounding code. This works
just like any Julia macro for inserting code, except that the code is generated by evaluating a Symata expression.

The following evaluates Symata code, translates the result to Julia and uses it as the body of a Julia function.
```
julia> f1(z) = @symExpr Together(PolyLog(-1,z) + (1-z))
```
"""
macro symExpr(expr)
    mx = symataevaluate(expr, EvaluateJuliaSyntaxSimple())
    mx = doeval(mx)
    local ex
    if isa(mx,Mxpr{:JuliaExpression})
       ex = mx[1]
    else
       ex = mxpr_to_expr(mx, MtoECompile())
    end
    :(($(esc(ex))))  ## We need to escape this so that the code is evaluated in the correct module
end

### ToJuliaString

@sjdoc ToJuliaString """
    ToJuliaString(expr)

translate `expr` to Julia, returning a string.
If the option `NoSymata => False` is given, then translated code requires `using Symata`.
Currently, only arithemetic, trigonometric functions, and a few other things are translated.
"""
@mkapprule ToJuliaString :nodefault => true  :options => Dict( :NoSymata => true )

protect(:NoSymata)

function do_ToJuliaString(mx, x; NoSymata=true)
    aux = NoSymata ? MtoEPlain() : MtoECompile()
    string(mxpr_to_expr(x, aux))
end

@mkapprule SymataCall  """
    SymataCall(var, body)
    SymataCall(varlist, body)

return a Julia function that substitues `var` or variable in `varlist` into `body` and
submits the result to the Symata evaluation sequence.

The variables are localized before writing the Julia function.
"""
@doap SymataCall(var::Symbol, ex::Mxpr) = wrap_symata(doeval(ex),var)
@doap SymataCall(vars::Mxpr{:List}, ex::Mxpr) = wrap_symata(doeval(ex),vars...)

"""
    f = wrap_symata(expr::Mxpr,var)

return a Julia function that Symata-evaluates an expression when called.

The function corresponds to `f(var) = expr`.
This wraps `expr` in a single-argument Julia function.

First creates a lexical scope, that is a new gensym symbol `nvar` is generated and is substituted
everywhere in `expr` for `var`. Calling `f(x)`  Symata-binds `x` to `nvar` and `expr` is
Symata-evaluated and the result is returned.

`wrap_symata` is used by `NIntegrate` and `Plot`.
"""
function wrap_symata(var0,expr0::Mxpr)   # Why are arguments reversed here ? Is this ever called ?
    (var,expr) = localize_variable(var0,expr0)
#    deepunsetfixed(expr)
    function (x)
        setsymval(var,x)
        # deepunsetfixed(expr)
        # this seems wasteful. When called from NIntegrate, it is not needed. Don't know why.
        # because we needed HoldAll
        doeval(expr)
    end
end

function wrap_symata(expr0::Mxpr,vars0...)
    vars = Array{Symbol}(undef, length(vars0))
    expr = expr0
    for i in 1:length(vars0)
        (vars[i],expr) = localize_variable(vars0[i],expr)
    end
    deepunsetfixed(expr)
    function (args...)
        for i in 1:length(args)
            setsymval(vars[i],args[i])
        end
        deepunsetfixed(expr)
        doeval(expr)
    end
end

function wrap_symata(expr0::SJSym,var0)
    var0 == expr0 && return (x) -> x
    sym = get_localized_symbol(var0)
    return (sym,expr0)   # this does not appear to be useful
end

#### CodeNative

# This does not yet work. Symata evaluates the symbol `f` to
# the function object, so we lose its name. We probably need
# a robust way to associate the symbol with the function.
# ... Julia numbers anonymous functions and names others.
# we can get this information somehow
# ok, it is string(func::Function). Then we can keep a
# hash table of Symata symbols bound to functions.
# ... a PITA.
# Better to define a Julia type symfunction or something
# that stores the function and the name.
# we can make it callable and it passes the call to its function
# can also do this at the Symata level.

# FIXME. implement functions according to the description above.

# @mkapprule CodeNative
# @doap function CodeNative(f::Symbol, arg)
#     thetypes = (margs(args)...)
#     code_native(eval(f), thetypes)
# end


# ------------------------------------------------------------------------------------------
# # Julia for Data Science
# Prepared by [@nassarhuda](https://github.com/nassarhuda)! 😃
#
# `Last updated on 03/Jan/2020`
#
# In this tutorial, we will discuss why *Julia* is the tool you want to use for your data
# science applications.
#
# We will cover the following:
# * **Data**
# * Data processing
# * Visualization
#
# ### Data: Build a strong relationship with your data.
# Every data science task has one main ingredient, the _data_! Most likely, you want to use
# your data to learn something new. But before the _new_ part, what about the data you
# already have? Let's make sure you can **read** it, **store** it, and **understand** it
# before you start using it.
#
# Julia makes this step really easy with data structures and packages to process the data,
# as well as, existing functions that are readily usable on your data.
#
# The goal of this first part is get you acquainted with some Julia's tools to manage your
# data.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# First, let's download a csv file from github that we can work with.
#
# Note: `download` depends on external tools such as curl, wget or fetch. So you must have
# one of these.
# ------------------------------------------------------------------------------------------

P = download("https://raw.githubusercontent.com/nassarhuda/easy_data/master/programming_languages.csv","programminglanguages.csv")

# ------------------------------------------------------------------------------------------
# We can use shell commands like `ls` in Julia by preceding them with a semicolon.
# ------------------------------------------------------------------------------------------

;ls

# ------------------------------------------------------------------------------------------
# And there's the *.csv file we downloaded!
#
# Now let's load it into Julia
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# We'll use the `DelimitedFiles` standard library package and its `readdlm()` function as
# shown
# below.
#
# (Today, the [CSV.jl](https://juliadata.github.io/CSV.jl/stable/) package is the
# recommended way to load CSVs in Julia. We can install it via `Pkg.add()`, and load .csv
# files using `CSV.read()`. This tutorial hasn't been updated to use CSV.jl yet.)
# ------------------------------------------------------------------------------------------

# using Pkg
# Pkg.add("CSV") # for CSV.read()
# Pkg.add("DelimitedFiles") # for readdlm

# using CSV
# P = CSV.read("programminglanguages.csv",header=true)
# or
using DelimitedFiles  # Standard library in Base

# ------------------------------------------------------------------------------------------
# By default, `readdlm` will fill an array with the data stored in the input .csv file. If
# we set the keyword argument `header` to `true`, we'll get a second output array for just
# the headers.
# ------------------------------------------------------------------------------------------

P,H = readdlm("programminglanguages.csv", ',', header=true)

P # stores the dataset

H # stores the header names

# ------------------------------------------------------------------------------------------
# Here we write our first small function.

# Now you can answer questions such as, "when was language X created?"
# ------------------------------------------------------------------------------------------

function language_created_year(P,language::String)
    loc = findfirst(P[:,2].==language)
    return P[loc,1]
end

language_created_year(P,"Julia")

language_created_year(P,"julia")

# ------------------------------------------------------------------------------------------
# As expected, this will not return what you want, but thankfully, string manipulation is
# really easy in Julia!
# ------------------------------------------------------------------------------------------

function language_created_year_v2(P,language::String)
    loc = findfirst(lowercase.(P[:,2]).==lowercase.(language))
    return P[loc,1]
end
language_created_year_v2(P,"julia")

# ------------------------------------------------------------------------------------------
# **Reading and writing to files is really easy in Julia.**

#
# You can use different delimiters with the function `readdlm`, from the `DelimitedFiles`
# package.

#
# To write to files, we can use `writedlm`.

#
# Let's write this same data to a file with a different delimiter.
# ------------------------------------------------------------------------------------------

writedlm("programming_languages_data.txt", P, '-')

# ------------------------------------------------------------------------------------------
# We can now check that this worked using a shell command to glance at the file,
# ------------------------------------------------------------------------------------------

;head -10 programming_languages_data.txt

# ------------------------------------------------------------------------------------------
# and also check that we can use `readdlm` to read our new text file correctly.
# ------------------------------------------------------------------------------------------

P_new_delim = readdlm("programming_languages_data.txt", '-');
P == P_new_delim

# ------------------------------------------------------------------------------------------
# ### Dictionaries
# Let's try to store the above data in a dictionary format!
#
# First, let's initialize an empty dictionary
# ------------------------------------------------------------------------------------------

dict = Dict{Integer,Vector{String}}()

# ------------------------------------------------------------------------------------------
# Here we told Julia that we want `dict` to only accept integers as keys and vectors of
# strings as values.
#
# However, we could have initialized an empty dictionary without providing this information
# (depending on our application).
# ------------------------------------------------------------------------------------------

dict2 = Dict()

# ------------------------------------------------------------------------------------------
# This dictionary takes keys and values of any type!
#
# Now, let's populate the dictionary with years as keys and vectors that hold all the
# programming languages created in each year as their values.
# ------------------------------------------------------------------------------------------

for i = 1:size(P,1)
    year,lang = P[i,:]

    if year in keys(dict)
        dict[year] = push!(dict[year],lang)
    else
        dict[year] = [lang]
    end
end

# ------------------------------------------------------------------------------------------
# Now you can pick whichever year you want and find what programming languages were invented
# in that year
# ------------------------------------------------------------------------------------------

dict[2003]

# ------------------------------------------------------------------------------------------
# ### DataFrames!
# *Shout out to R fans!*
# One other way to play around with data in Julia is to use a DataFrame.
#
# This requires loading the `DataFrames` package. Thankfully, this tutorial came with a
# Project.toml file that specifies exactly which version of DataFrames to install...
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### Project.toml files
#
# For this tutorial (Julia for Data Science), you may have noticed that there are files in
# this folder called [`Project.toml`](/edit/introductory-tutorials/broader-topics-and-
# ecosystem/intro-to-julia-for-data-science/Project.toml) and
# [`Manifest.toml`](/edit/introductory-tutorials/broader-topics-and-ecosystem/intro-to-
# julia-for-data-science/Manifest.toml). These are files autogenerated by Julia's package
# manager, `Pkg`, that describe the _exact set of packages_ installed for a julia project,
# allowing you to share your work in a perfectly reproducible way.
#
# Jupyter was able to detect those `.toml` files, and so this notebook was automatically
# started with _this project activated!_ Note that this means any packages you add or remove
# inside this notebook will only affect this "Julia for Data Science" _project_.
#
# To install all of the package dependencies used in the rest of the tutorial, you only need
# to run this next cell (commands that start with `]` are package repl commands):
# ------------------------------------------------------------------------------------------

] instantiate

# ------------------------------------------------------------------------------------------
# You can read more about package manager commands, here:
# https://docs.julialang.org/en/v1/stdlib/Pkg/index.html
#
# **Now back to DataFrames!**
# ------------------------------------------------------------------------------------------

using DataFrames
df = DataFrame(year = P[:,1], language = P[:,2])

# ------------------------------------------------------------------------------------------
# You can access columns by header name, or column index.
#
# In this case, `df[1]` is equivalent to `df.year` or `df[!, :year]`.
#
# Note that if we want to index columns by header name, we precede the header name with a
# colon. In Julia, this means that the header names are treated as *symbols*.
# ------------------------------------------------------------------------------------------

df.year

# ------------------------------------------------------------------------------------------
# **`DataFrames` provides some handy features when dealing with data**
#
# First, it uses julia's "missing" type.
# ------------------------------------------------------------------------------------------

a = missing
typeof(a)

# ------------------------------------------------------------------------------------------
# Let's see what happens when we try to add a "missing" type to a number.
# ------------------------------------------------------------------------------------------

a + 1

# ------------------------------------------------------------------------------------------
# `DataFrames` provides the `describe` function, which can give you quick statistics about
# each column in your dataframe
# ------------------------------------------------------------------------------------------

describe(df)

# ------------------------------------------------------------------------------------------
# ### RDatasets
#
# We can use RDatasets to play around with pre-existing datasets
# ------------------------------------------------------------------------------------------

using RDatasets
iris = dataset("datasets", "iris")

# ------------------------------------------------------------------------------------------
# Note that data loaded with `dataset` is stored as a DataFrame. 😃
# ------------------------------------------------------------------------------------------

typeof(iris)

# ------------------------------------------------------------------------------------------
# The summary we get from `describe` on `iris` gives us a lot more information than the
# summary on `df`!
# ------------------------------------------------------------------------------------------

describe(iris)

# ------------------------------------------------------------------------------------------
# ### More on Missing Values
#
# Julia 1.0 and beyond has native support for `missing` values. (Before Julia 1.0, this was
# done via the DataArrays.jl package.)
# More information on using arrays with missing values can be found
# [in the Julia documentation](https://docs.julialang.org/en/v1/manual/missing/#Arrays-With-
# Missing-Values-1).
# ------------------------------------------------------------------------------------------

foods = ["apple", "cucumber", "tomato", "banana"]
calories = [missing,47,22,105]
typeof(calories)

using Statistics  # julia's standard library for stats
mean(calories)

# ------------------------------------------------------------------------------------------
# Missing values ruin everything! 😑
#
# Luckily we can ignore them with `skipmissing`!
# ------------------------------------------------------------------------------------------

mean(skipmissing(calories))

# ------------------------------------------------------------------------------------------
# Oh WAIT! Detour. How did I get the emoji there?
#
# Try this out:
#
# ```
# \:expressionless: + <TAB>
# ```
# ------------------------------------------------------------------------------------------

😑 = 0 # expressionless
😀 = 1
😞 = -1

# ------------------------------------------------------------------------------------------
# *Back to missing values*
# ------------------------------------------------------------------------------------------

prices = [0.85,1.6,0.8,0.6,]

dataframe_calories = DataFrame(item=foods,calories=calories)

dataframe_prices = DataFrame(item=foods,price=prices)

# ------------------------------------------------------------------------------------------
# We can also `join` two dataframes together
# ------------------------------------------------------------------------------------------

DF = join(dataframe_calories,dataframe_prices,on=:item)

# ------------------------------------------------------------------------------------------
# ### FileIO
# ------------------------------------------------------------------------------------------

using FileIO
julialogo = download("https://avatars0.githubusercontent.com/u/743164?s=200&v=4","julialogo.png")

# ------------------------------------------------------------------------------------------
# Again, let's check that this download worked!
# ------------------------------------------------------------------------------------------

;ls

# ------------------------------------------------------------------------------------------
#
# Next, let's load the Julia logo, stored as a .png file
#
# **NOTE: You may see errors below, that certain Image packages could not be found. If so:**
#  - This is because these packages are specific to your OS, so aren't installed by default.
#  - Simply run the suggested commands to install the packages, then try again.
# ------------------------------------------------------------------------------------------

# These commands may vary depending on your operating system.
#import Pkg; Pkg.add("QuartzImageIO")
#import Pkg; Pkg.add("ImageMagick")

X1 = load("julialogo.png")

# ------------------------------------------------------------------------------------------
# We see here that Julia stores this logo as an array of colors.
# ------------------------------------------------------------------------------------------

@show typeof(X1);
@show size(X1);

# ------------------------------------------------------------------------------------------
# And if we load the Images package, it will display in Jupyter as an image:
# ------------------------------------------------------------------------------------------

using Images

X1

# ------------------------------------------------------------------------------------------
# ### File types
# In Julia, many file types are supported so you do not have to transfer a file you from
# another language to a text file before you read it.
#
# *Some packages that achieve this:*
# MAT CSV NPZ JLD FASTAIO
#
#
# Let's try using MAT to write a file that stores a matrix.
# ------------------------------------------------------------------------------------------

using MAT

A = rand(5,5)
matfile = matopen("densematrix.mat", "w")
write(matfile, "A", A)
close(matfile)

# ------------------------------------------------------------------------------------------
# Now try opening densematrix.mat with MATLAB!
# ------------------------------------------------------------------------------------------

newfile = matopen("densematrix.mat")
read(newfile,"A")

names(newfile)

close(newfile)

.nbexports/introductory-tutorials/broader-topics-and-ecosystem/intro-to-julia-for-data-science/1. Julia for Data Science - Data.jl


### A Pluto.jl notebook ###
# v0.12.7

using Markdown
using InteractiveUtils

# ╔═╡ e077ab16-22e5-11eb-15ed-5f0654bf7328
using Pkg, DrWatson

# ╔═╡ ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
md"## Listing4.01"

# ╔═╡ e75a9a06-22e5-11eb-2e36-8d4f62b830ed
begin
	@quickactivate "StatisticsWithJuliaPlutoNotebooks"
end

# ╔═╡ fa68607e-22e5-11eb-0558-c9a4d9f77426
begin
	f(z::Int) = begin z = 0 end
	f(z::Array{Int}) = begin z[1] = 0 end
end

# ╔═╡ 572972b2-22e6-11eb-1ec4-2d53fc807d03
begin
	x1 = 1
	typeof(x1)
end

# ╔═╡ 5729a994-22e6-11eb-00b0-e525c9e271cb
isimmutable(x1)

# ╔═╡ 572a4444-22e6-11eb-351f-65750fcd0d5e
Text("Before call by value: $x1")

# ╔═╡ 57305f50-22e6-11eb-1aef-61c42e0c6363
f(x1)

# ╔═╡ 573d81bc-22e6-11eb-3b89-010224cc8027
Text("After call by value: $x1")

# ╔═╡ 573e105a-22e6-11eb-32cb-859417a95d32
begin
	x2 = [1]
	typeof(x2)
end

# ╔═╡ 574411da-22e6-11eb-2e9e-739f51ea9fc6
isimmutable(x2)

# ╔═╡ 574ff9c8-22e6-11eb-3866-817010612a7e
Text("Before call by reference: $x2")

# ╔═╡ 575094b4-22e6-11eb-25b6-fb702eec82a2
f(x2)

# ╔═╡ 575b02be-22e6-11eb-29ad-3f48f1b6eae6
Text("After call by reference: $x2")

# ╔═╡ 475ff888-22e6-11eb-2354-09f8f40a8e12
md"## End of listing4.01"

# ╔═╡ Cell order:
# ╟─ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
# ╠═e077ab16-22e5-11eb-15ed-5f0654bf7328
# ╠═e75a9a06-22e5-11eb-2e36-8d4f62b830ed
# ╠═fa68607e-22e5-11eb-0558-c9a4d9f77426
# ╠═572972b2-22e6-11eb-1ec4-2d53fc807d03
# ╠═5729a994-22e6-11eb-00b0-e525c9e271cb
# ╠═572a4444-22e6-11eb-351f-65750fcd0d5e
# ╠═57305f50-22e6-11eb-1aef-61c42e0c6363
# ╠═573d81bc-22e6-11eb-3b89-010224cc8027
# ╠═573e105a-22e6-11eb-32cb-859417a95d32
# ╠═574411da-22e6-11eb-2e9e-739f51ea9fc6
# ╠═574ff9c8-22e6-11eb-3866-817010612a7e
# ╠═575094b4-22e6-11eb-25b6-fb702eec82a2
# ╠═575b02be-22e6-11eb-29ad-3f48f1b6eae6
# ╟─475ff888-22e6-11eb-2354-09f8f40a8e12


# # Example 3: Pulse Parametrization

#md # !!! tip
#md #     This example is also available as a Jupyter notebook:
#md #     [`state_to_state_parametrizations.ipynb`](@__NBVIEWER_ROOT_URL__/examples/state_to_state_parametrizations.ipynb).

#md # ``\gdef\op#1{\hat{#1}}``
#md # ``\gdef\init{\text{init}}``
#md # ``\gdef\tgt{\text{tgt}}``

#nb # $
#nb # \newcommand{tr}[0]{\operatorname{tr}}
#nb # \newcommand{diag}[0]{\operatorname{diag}}
#nb # \newcommand{abs}[0]{\operatorname{abs}}
#nb # \newcommand{pop}[0]{\operatorname{pop}}
#nb # \newcommand{aux}[0]{\text{aux}}
#nb # \newcommand{opt}[0]{\text{opt}}
#nb # \newcommand{tgt}[0]{\text{tgt}}
#nb # \newcommand{init}[0]{\text{init}}
#nb # \newcommand{lab}[0]{\text{lab}}
#nb # \newcommand{rwa}[0]{\text{rwa}}
#nb # \newcommand{bra}[1]{\langle#1\vert}
#nb # \newcommand{ket}[1]{\vert#1\rangle}
#nb # \newcommand{Bra}[1]{\left\langle#1\right\vert}
#nb # \newcommand{Ket}[1]{\left\vert#1\right\rangle}
#nb # \newcommand{Braket}[2]{\left\langle #1\vphantom{#2}\mid{#2}\vphantom{#1}\right\rangle}
#nb # \newcommand{op}[1]{\hat{#1}}
#nb # \newcommand{Op}[1]{\hat{#1}}
#nb # \newcommand{dd}[0]{\,\text{d}}
#nb # \newcommand{Liouville}[0]{\mathcal{L}}
#nb # \newcommand{DynMap}[0]{\mathcal{E}}
#nb # \newcommand{identity}[0]{\mathbf{1}}
#nb # \newcommand{Norm}[1]{\lVert#1\rVert}
#nb # \newcommand{Abs}[1]{\left\vert#1\right\vert}
#nb # \newcommand{avg}[1]{\langle#1\rangle}
#nb # \newcommand{Avg}[1]{\left\langle#1\right\rangle}
#nb # \newcommand{AbsSq}[1]{\left\vert#1\right\vert^2}
#nb # \newcommand{Re}[0]{\operatorname{Re}}
#nb # \newcommand{Im}[0]{\operatorname{Im}}
#nb # $

# This example illustrates the parametrization of control pulses as a
# form of amplitude constraint.

using DrWatson
@quickactivate "KrotovTests"
#-
using QuantumControl
using QuantumControl.Shapes: flattop
using Krotov:
    SquareParametrization,
    TanhParametrization,
    TanhSqParametrization,
    LogisticParametrization,
    LogisticSqParametrization
using LinearAlgebra

#-
#jl using Test; println("")
using Plots
Plots.default(
    linewidth               = 3,
    size                    = (550, 300),
    legend                  = :top,
    foreground_color_legend = nothing,
    background_color_legend = RGBA(1, 1, 1, 0.8),
)
#-

# ## Parametrizations

# ## Symmetric Bounded Controls

#-
include(joinpath(@__DIR__, "plots", "symmetric_parametrization_comparison.jl"))  # hide
fig = plot_symmetric_parametrization_comparison()  # hide
#jl display(fig)

# ## Positive (Bounded) Controls

#-
include(joinpath(@__DIR__, "plots", "positive_parametrization_comparison.jl"))  # hide
fig = plot_positive_parametrization_comparison()  # hide
#jl display(fig)

# ## Two-level Hamiltonian

# We consider the Hamiltonian $\op{H}_{0} = - \frac{\omega}{2} \op{\sigma}_{z}$, representing
# a simple qubit with energy level splitting $\omega$ in the basis
# $\{\ket{0},\ket{1}\}$. The control field $\epsilon(t)$ is assumed to couple via
# the Hamiltonian $\op{H}_{1}(t) = \epsilon(t) \op{\sigma}_{x}$ to the qubit,
# i.e., the control field effectively drives transitions between both qubit
# states.
#
# We we will use

ϵ(t) = 0.2 * flattop(t, T=5, t_rise=0.3, func=:blackman);

#-
"""Two-level-system Hamiltonian."""
function hamiltonian(Ω=1.0, ϵ=ϵ)
    σ̂_z = ComplexF64[
        1  0
        0 -1
    ]
    σ̂_x = ComplexF64[
        0  1
        1  0
    ]
    Ĥ₀ = -0.5 * Ω * σ̂_z
    Ĥ₁ = σ̂_x
    return (Ĥ₀, (Ĥ₁, ϵ))
end;
#-

H = hamiltonian();
#jl @test length(H) == 2

# The control field here switches on from zero at $t=0$ to it's maximum amplitude
# 0.2 within the time period 0.3 (the switch-on shape is half a [Blackman pulse](https://en.wikipedia.org/wiki/Window_function#Blackman_window)).
# It switches off again in the time period 0.3 before the
# final time $T=5$). We use a time grid with 500 time steps between 0 and $T$:

tlist = collect(range(0, 5, length=500));

#-
function plot_control(pulse::Vector, tlist)
    plot(tlist, pulse, xlabel="time", ylabel="amplitude", legend=false)
end

plot_control(ϵ::T, tlist) where {T<:Function} = plot_control([ϵ(t) for t in tlist], tlist)

plot_control(H[2][2], tlist)

# ## Optimization target

# The `krotov` package requires the goal of the optimization to be described by a
# list of `Objective` instances. In this example, there is only a single
# objective: the state-to-state transfer from initial state $\ket{\Psi_{\init}} =
# \ket{0}$ to the target state $\ket{\Psi_{\tgt}} = \ket{1}$, under the dynamics
# of the Hamiltonian $\op{H}(t)$:

function ket(label)
    result = Dict("0" => Vector{ComplexF64}([1, 0]), "1" => Vector{ComplexF64}([0, 1]),)
    return result[string(label)]
end;

#-
#jl @test dot(ket(0), ket(1)) ≈ 0
#-

objectives = [Objective(initial_state=ket(0), generator=H, target_state=ket(1))]

#-
#jl @test length(objectives) == 1
#-

# ## Square-parametrization for positive pulses

problem = ControlProblem(
    objectives=objectives,
    pulse_options=IdDict(
        ϵ => Dict(
            :lambda_a => 5,
            :update_shape => t -> flattop(t, T=5, t_rise=0.3, func=:blackman),
            :parametrization => SquareParametrization(),
        )
    ),
    tlist=tlist,
    iter_stop=50,
    J_T=QuantumControl.Functionals.J_T_ss,
    check_convergence=res -> begin
        ((res.J_T < 1e-3) && (res.converged = true) && (res.message = "J_T < 10⁻³"))
    end
);
#-
opt_result_positive, file = @optimize_or_load(
    datadir(),
    problem;
    method=:krotov,
    filename="parametrization#opt_result_positive.jld2"
);
#-
opt_result_positive

# We can plot the optimized field:

#-
#!jl plot_control(opt_result_positive.optimized_controls[1], tlist)
#-

#-
#jl @test minimum(opt_result_positive.optimized_controls[1]) ≥ 0.0
#jl @test minimum(opt_result_positive.optimized_controls[1]) < 1e-16
#jl @test maximum(opt_result_positive.optimized_controls[1]) > 0.0
#-

# ## Tanh-Square-Parametrization for positive amplitude-constrained pulses

problem_tanhsq = ControlProblem(
    objectives=objectives,
    pulse_options=IdDict(
        ϵ => Dict(
            :lambda_a => 10,
            :update_shape => t -> flattop(t, T=5, t_rise=0.3, func=:blackman),
            :parametrization => TanhSqParametrization(3),
        )
    ),
    tlist=tlist,
    iter_stop=50,
    J_T=QuantumControl.Functionals.J_T_ss,
    check_convergence=res -> begin
        ((res.J_T < 1e-3) && (res.converged = true) && (res.message = "J_T < 10⁻³"))
    end
);
#-
opt_result_tanhsq, file = @optimize_or_load(
    datadir(),
    problem_tanhsq;
    method=:krotov,
    filename="parametrization#opt_result_tanhsq.jld2"
);
#-
opt_result_tanhsq

# We can plot the optimized field:

#-
#!jl plot_control(opt_result_tanhsq.optimized_controls[1], tlist)
#-

#-
#jl @test minimum(opt_result_tanhsq.optimized_controls[1]) ≥ 0.0
#jl @test minimum(opt_result_tanhsq.optimized_controls[1]) < 1e-16
#jl @test maximum(opt_result_tanhsq.optimized_controls[1]) > 0.0
#jl @test maximum(opt_result_tanhsq.optimized_controls[1]) < 3.0
#-

# ## Logistic-Square-Parametrization for positive amplitude-constrained pulses

problem_logisticsq = ControlProblem(
    objectives=objectives,
    pulse_options=IdDict(
        ϵ => Dict(
            :lambda_a => 1,
            :update_shape => t -> flattop(t, T=5, t_rise=0.3, func=:blackman),
            :parametrization => LogisticSqParametrization(3, k=1.0),
        )
    ),
    tlist=tlist,
    iter_stop=50,
    J_T=QuantumControl.Functionals.J_T_ss,
    check_convergence=res -> begin
        ((res.J_T < 1e-3) && (res.converged = true) && (res.message = "J_T < 10⁻³"))
    end
);
#-
opt_result_logisticsq, file = @optimize_or_load(
    datadir(),
    problem_logisticsq;
    method=:krotov,
    filename="parametrization#opt_result_logisticsq.jld2"
);
# We can plot the optimized field:

#-
#!jl plot_control(opt_result_logisticsq.optimized_controls[1], tlist)
#-

#-
#jl @test minimum(opt_result_logisticsq.optimized_controls[1]) ≥ 0.0
#jl @test minimum(opt_result_logisticsq.optimized_controls[1]) < 1e-16
#jl @test maximum(opt_result_logisticsq.optimized_controls[1]) > 0.0
#jl @test maximum(opt_result_logisticsq.optimized_controls[1]) < 3.0
#-
# ## Tanh-parametrization for amplitude-constrained pulses

problem_tanh = ControlProblem(
    objectives=objectives,
    pulse_options=IdDict(
        ϵ => Dict(
            :lambda_a => 1,
            :update_shape => t -> flattop(t, T=5, t_rise=0.3, func=:blackman),
            :parametrization => TanhParametrization(-0.5, 0.5),
        )
    ),
    tlist=tlist,
    iter_stop=50,
    J_T=QuantumControl.Functionals.J_T_ss,
    check_convergence=res -> begin
        ((res.J_T < 1e-3) && (res.converged = true) && (res.message = "J_T < 10⁻³"))
    end
);
#-
opt_result_tanh, file = @optimize_or_load(
    datadir(),
    problem_tanh;
    method=:krotov,
    filename="parametrization#opt_result_tanh.jld2"
);
#-
#!jl plot_control(opt_result_tanh.optimized_controls[1], tlist)
#-
#jl @test minimum(opt_result_tanh.optimized_controls[1]) > -0.5
#jl @test maximum(opt_result_tanh.optimized_controls[1]) < 0.5
#-


### A Pluto.jl notebook ###
# v0.12.16

using Markdown
using InteractiveUtils

# ╔═╡ 9dfa6a82-3b9d-11eb-2557-3bb72249910a
using PaddedViews

# ╔═╡ 97dcb6ca-3b9b-11eb-2c96-0904d9a90046
s="""L.LL.LL.LL
LLLLLLL.LL
L.L.L..L..
LLLL.LL.LL
L.LL.LL.LL
L.LLLLL.LL
..L.L.....
LLLLLLLLLL
L.LLLLLL.L
L.LLLLL.LL"""

# ╔═╡ 1501f7ee-3ba3-11eb-2b57-7b10a8d1d5d4
sbig="""LLLLLLLLLLLLLLLLLLLLLLL.LLLLLLL.L.LLLLL.LLLL.LLLLLLLLL..LLL.LLLLLLLLLLLLLL.LLLLLLLLL.LLL.LLLLLL
LLLLLLLLLL.L.LLLLLLL.LL.LLLLLLL.LLLLLLLL.LLLLLLLLLLLLL.LLLLLLLLLLL.LLLLLLLL.LLLLLLLLLLLL.LLLLLL
LLLLLLLL.L.LLLLLLLLLLLL.LLLLLLL.LLLLLL.LLLLL.LLLLLLLLL.LLLLLLLLLLL.LLLLLLLLLLLLLLLL.LLLL.LLLLLL
.LLLLLLLLLLLLLLLLL.LLLL.LLLLLLL.LLLLLL.LLLLL.LLL.LLLLLLLLLL.LLLLLL.LLLLLLLL.LLLLLL.LLLLL.LLLLLL
LLLLLLLLLLLLLLLLLLLLLLL.LLLLLLL.LLL.LLLLL.LL.LLLLLLLLL.LLLL.LLLLLL.LLLLLLLL.LLLLLLL.LLLL.LLLLLL
LLL.LL..L..L.LL.L.L........L.............LLL....LL...L..L.L.....L..L.L.......L..L......LLLL.L.L
LLLLLLLLLLLLLLLLL..LLLL.LLLLLLL.LLLLLL.LLLLL.LLLLLLLLL.LLLL.LLLLLLLLLLLLLLL.LLLLLLL.LLLLL.LLLLL
LLLLLLLLLL.LLLL.LL.LLLL.LLLLLLLLLLLLLLLLLL.L.LLLLLLLLL.LLLL.L.L.LL.LLLLLLLLLLLLL.LL.LLLLLLLLLLL
LLLLLLLLLLLLLLLLL.LLLLL.LLLLLLL.LLLLLLLLLLLL.LLLLLLLLL.LLLL.LLLLLLLLLLLLLLL.LL.LLLLLLLLL.LLLLL.
LLLLLLLLLLLLLLLLL..LLL.LLLLLLLLLLLLLLL.LLLLL.LLLLLLLLL.LLLL.LLLLLL.LLLLLLLL.LLLLLLLLLLLL.LLLLLL
LLLLLLLLLL.LLLLLLLLLLLL.LLLLLLLLLLLLLLLLLLLL.LLLLLLLLLLLLLL.LLLL.LLLLLLLLLLLLL.LLLL.LLLLLLLLLLL
LLLLLLLLLL.LLLLLLL.LLLLLLLLLLLL.LLLLLL.LLL.L..LLLLLLLL.LLLL.LLLLLL..LLLLLLLLLLLLLLL.LLLL.LLLLLL
L.LLLLLLLL..LLLLLL.LLLL.LLLLLLLLLLLLLL.LLLLL.LLLLLLLLLLLLLL.LLLLLLLLL.LLLLL.LLLLLLL.LLLL.LLLLLL
.LLLLLLLLL.LLLLLLLLLLLL.LLLLLLLLLLLLLLLLLLLLLLLLLLLLLL.LLLLLLLLLLLLLLLLLLLL.LLLLLLL.LLLLLLLLLLL
LL.L..LL...LL..L..L.L.L.....L..LL...L.LLL......L.L...L.LL...L.L.L.........L.LL....L.LL.LL...LL.
LLLLLLLLLLLLLLLLLLLLLLL.LLLLLLL.LLLLLLLLLLLLLLLLLLLLLL.LLLL.LLLLLL.L.LLLLLLLLLLLLLL.LLL..LLLLLL
LLLLL.LLLL.LLLL.LLLLLLLLLLLLLLL.LLLLLL.L.LLL.LLLLLLLLL.LLLLLLLLLLLLLLLLLLLLLLLLLLLL.LLL.LLLLLLL
LLL.LLLLLL.LLLLLLL.LLLL.LLL.LLL.LLLL.L.LL.LLLLLLLLLLLL.LLLL.LLL.LL.LLLLLLLLLLLLLLLLLLLLL.LL.LLL
LLLLLLLLLL.LLL.LLLLLLLL.LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL.LLLLLL.LLLLLLLLLLLLLLLL.LLLL.LLLLLL
...L...L..L.L...L..L......L.......L.L...L...L...L.....L...L...L....L...LL..L..L.............LL.
LLL..LLLLL.LLLLLLLLLLLL.LLLLLLL.LLLLLL.LLLLLLLLLLL.LLL.LLLL.LLLLLL.LLLLLLLLLLLLLLLL.LLLLL.LLLLL
LLLLLLLLLL.LLL..LLLLLLL.L.LLLLL.LLLLLLLLLLLL.LLLLLLLLL.LLLLLLLLLLL.LLL.LLLLLLLLLLLLLLLLLLLLLLLL
LLLLLL.L.L.L.LLLLLLLLLL.LLLLLLL.LLLLLL.LLLLL.LLLLLLLLLLLLLL.LLLLLL.LLLLLLLL.LLLLLLL.LLLL.LLLLL.
LLLLLL.LLL.LLLLLLLLLLLL.LLLLLLL.L.LLLLLLLLLL.LLLLLLLLL.LLLL.LLLLLL.LLL.LLLL.LLLLLLL.LLLL.LLLLL.
LLLLLLLLLL.LLLLLLL.LLLL.LLLLLLL.LLLLLL.LLLLL.LLL.LLLLL.L.LL.LLLLLL.LLLLLLLL..LLLLLLLLLLL.LLLLLL
LLLLLL.LLL.LLLLLLL.LLLL.LLLLLLL.LLLLLLLLLLLLLLLLLLLLLL.LLLLLLLLLLL.LLLLLLLL.LLLLLLL.LLLLLLLLLLL
LLLLLLLLLL.LLLLLLL.LLLL.LLLLLLLLLLLLLLLLLLLL.LLLLLLLLL.LLLL.LLLLLL.LLLLLLL..LLLLLLLLL.LL.LLLLLL
LLLLLLLLLLLLLLLLLLLLLL..LLLLLLL.LLLLLL.LLLLL.LL.LLLLLL.LLLLLLLLLLL.LLLLL.L.LLLLLLLLLLLLL.LLLLLL
LLLLLLLLLL.LLLLLLLLLLLL.LLLLLLL.LLLLLLL.LLLL.LLLLLLLLLLLLLL.LLLLLLLLLLLLLLL.LL.LLLL.LLLL.LLLLLL
.L......L..L........LL..LLL...LL.L.....L..L.L..LLL..LLL..LL.LL...LLL.......L...LL...........LLL
LLLLLLLLLL.LL.LLLL.LLLL.LLLLLLL.LLLLL.LLLLLL.LLLLLLLLL.LLLLLLLLLLL.LLLLLLLL.LLLLLLLLLLLL.LLLLLL
LLLLLLLLLLLLLLLLLL.LLLLLLLLLLLLLLLLLLLLLLLLL.LLLLL.LLL.LLLLLLLLLLL.LLLLLLLLLLLLLLLL.LLLL.LLLLLL
LLLLLLLLLL.LLLLLLL.LLLL.LLLLLL..LLLLLL.LLLLLLLLLLLLLLL.LL.L.L.LLLLLLLLLLLLL.LLLLLLL.LLL..LLLLLL
LLLLLL.LLL.LLLLLLL.LLLLLLLLL.LL.LLLLLL.LLLLLLLLLLLLLLL.LLLL.LLLLLL.LLL.LLLL.LLLLLLL.LLLL.LLLLLL
LLLLLLLLLLLLLLLL.L.LLLL.LLLLLL..LLLLLLLLLLLL.LLLLLLL.L.LLLLLLLLLLL.LLLLLLLL.LLLLLLL.LLLL..LLLLL
LLLLLLLLLL.LLLLLLL.LLLL.LLLLLLL.LLLLLLLLLLLL.LLLLLLLLLLLLLL.LLLLLL.LL.LLLLL.LL.L.LL.LLLLLLLLLLL
LLLLLL.LLL.LLLLLLL.LLLLLLL.LLLL.LLLLLLLLLLLL.LLLLLLLLL.LLL.LLLLLLL.LLLLLLLLLLLLLLL..LLLL.LLLLLL
LL.LLLLLLL.LLLLLLL.LLLLLLLLLLLL.LLLLLLLLLLLLL.LLLLLLLLLLLLLLLL.LLL.LLLLLLLL.LLLLLLLLLLLLLL.LLLL
LLLLLLLLLL.LLLLLLL.LLLL.LLL.LLL.LLLLLL.LLLL..LLLLLLLLLLLLLL.LLLLLLLLLLLLLLL.LLLLLLL.LLLL.LLLLLL
........LL.L..LLLL..L..LLLL..LLL.........L.L......LL.LL..L........L...L..LL..LLLL.L.L.....L.LL.
LL.LLLLLLL.LLLLLLL.LLLL.LLLL.LL.LLLLLLLLLLLLLLLLLLLLLL.LLLL.LLLLL..LLLLLLLLLLLLLLLL.LLLLL.L..LL
LLLLLLLLL.LLLLLLLL.LLL..LLLLLLL.LLLLLL.LLLLL.LLLLLLLLL.LLLL.LLLLLLLLLL.LLLL.LLLL.LLLLLLLLLLLLLL
LL.LLLLLLL.LLLLLLL.LLLL.LLLLLLLLLLLLLL.LLLLLLLL.LLLLLLLLLLL.LLLLLL.LLLLLLL..LLL.LLLLLLLLLL.LLLL
LLLLLLLLLLLLLLLLLLLLLLL.LLLLLLL.LLLLLLLLLLLL.LLLLLLLLL.LLLL.LLLLLL.LLL.LLL..LLLLLLL.LLLL.LLLLLL
LLL.LLLLLL.LLLLLLLLLLLLLLLLLLLL.LLLLLL..LLL.LLLLLLLLLL.LLLL.LLLLLL.LLLLLLLLLLLLLLLL.LLLL.LLLL.L
LLLLLLLLLL.LLLLLLL.LLLLLLLLLLLL.LLLLLL.LLLLLLLL.LLLLLLLLLLL.LLLLLL.LL.LLLL..LLLLLLLLLLLLLLLLLLL
..LL..LLL.....LL......L..L.L.LLL........LL......LLL...L......L.LL..L.LL.LL.L.......LL....LL...L
.LL.LLLLLLLLLLLLLL.L.LL.LLLLLLL.LLL.LL...L.L.LLLLLLLLL.LLLLLLLLLLLLL.LLLLLL.LLLLLLL.LLLL.LLLLLL
L.LLLLLLLLLLLL.LLL.LLLL.LLLLLLL.LLLLLLLLLLLL.LLLLLLLLLLLL.LLLLLLLL.LLL.LLLL.LLL.LLL.LLLL.LLLLLL
LLLLLLLLLL.LLLLLLL.LLLL.LLLLLLL.LLLLLLLLLLLL.LLLLLLLLL.LLL..L.LLLL.LLLLLLLL..LLL.LLLLLLL.LLLLLL
.LLLLLLLLL.LLL.LLL.LLLL.LLL..LLLLLLLLLLLLLLLLLLLLLLLLLL.LLL.LLLLLL.LLLLLLLL.LLLLLLL.LLLL.LLLLLL
L..L...LL...L.LL....L.LL.L..LL.L.........L.........LLLL..L.L..LL...L.L.L....LLL.....L..L.L...LL
.LLLLLLLLL.LLLL.LL.LLLL.LLLLLLLLL.LLLL.LLLLL.LLL.LLLLL.LLLLLLLLLLL.LLLLLLLLLLLLLLLLLLLLL.LLLLLL
LLLLLLLLLL.L..LLLL.LLLL.LLLLLLL..LLLLLLLLLLL.LLLLLLLLL.LLLL.LLLLLLLLLLLLLLL.LLLLLLLLLLLL.LLLLLL
LLLLLLLLLL.LLLLLLL.LLLL.LLLLLLL.LLLLLL.LLLLLLLL.LLLLL.LLLLLLLLLLLL.LLLLLLLL.LLLLLLL.LL.LLLLL.LL
LLLLLLLLLL.LLLLLLL.LLLL.LLLLLLL.LLLLLL.LLLLLLLLLLLLLLLLLLLLLLLLLLL.LLLLLLLL.LLLLLLL.LLLLLLLLLLL
L.LLLLLLLL.LLLLLLLLLLLL.LLLL.LL..LLLLL.LLLL.LLLLLL.LLLLLLLL.LLL.LLLLLLLLLLLLLLLLL.L.LLLL.LLLLLL
LLLLLLLLLL..LLLLLL.LLLLLLLLLLLL.L.LLLL.LLLLLLLLLLLLLLL.LLLL.LLLLLL..LL.LLLL.LLLLLLL.LLLL.LLLLLL
LLL.LLLLLLLLLLLLLL.LLLLLLLLLLLL.LL.LLL.LLLLL.LLLLL.LLL.LLLL.LLLLLL.LL.LLLL..LLLLLLL.LLLLLLLLLLL
LLLLLLLLLL.LLLLLLLLLLLL.LLLLLLL.LLLLLL.LLLLL.LLLLLLLL..LLLL.LLLLLLLLLLLLLLL.LLLLL.LLLLLL.LLLLLL
L.L...L...L..L..L....L...L..LLL..L....L..LL...LLL.LL.L...L.L..L.LLL..L....L.L...LL..L..L.L..L.L
LLLLLLLLLLLLLLLLLLLLLLLLLLLL.LLLLLLLLLLLLLLL.LLLLLLLLL.LLLL.LLLLLL.LLLLLLLL.LLLLLLL.LLLL.LLLLLL
LLLLLLLLLLLLLLLLLL.LLLLLLLLLLLLLLLLLLLLLLLLL.LLLLLLLLL.LLLL.LLLLLLLLLLLLLLLLLLLLLLL.LLLLL.LLL.L
LLLLLLLLLL.LLLLL.L.LLLL.LLLLLLL.LLLLLLLLLLLL.LLLLLLLLL.LLLL.LLLLLL.LLLLLLLLLLLLL.LLLL.LLLLLLLLL
L..LLLLLLLLL.LLLLL.L.LLLLLL.LLL.LLLLLL.LLLLLLLLLLLLLLL.LLLL.LLLLLL.LL.LLLLLLLLLLLLL.LLLLLLLLLLL
LLLLL.LLLL.LLLL.LL.LLLL.LLLLLLLLLLLLLLLLLLLL.LLLLLLLLL.LLLLLL.LL.L.LLL.LLLLLLLLL.LL.LLLL.LLLLLL
....L....L..L.....L....L..LL....L.L..L..LL.LLL...L.....L.LLL.L.LL.....L....L.L........L.LLL....
L.L.LL.LLL.LLLLL.L.LLLL.LLLLLLL.LLLLLL.LLLLL.LLLLLLLLL.LLLL.LLLLLLLL.LLLLLLLLLLL.LL.LLLL.LLLLLL
LLLLLLLLL.LLLLLLLL.LLLL.LLLLLLL.LLLLLL.LLLLL.LL.LLLLLL.LLLL.LLLLLL.LLLLLLL.LLLLLLLL.LLLL.LLLLLL
LLLLLLLLL.LL.LLLLLLLLLL.LLLL.LLLL.LLLLLLLLLLLLLLLLLLLL..LLL.L..LLL.LLL.L.LL.LLLLLL..LLLL.LLLLLL
LLLLLLLLLL.LLLLLLL.LLLL.LLLLLLL.LLLLLL.LLLLLLLLLLLLLLLLLLLLLLLLLLL.LLLLLLLLLLLLLLLL.LLLLLLLLLLL
LLLLLLLLLLLLLLLLLLLLLLL.LLLLLLL.LLLLLLLLLLLL.LLLLLLLLL.LLLL.LLLLLLLLLLLL.LL.LLLLLLL.LLLL.LLLLLL
.....LL.LL.L.....LL.LL..L...L...LLLLL......L.L....LL...LLL...LLLLL..LL..L....L......L.......L.L
LLLLLLLLLL.LLLL.LL.LLLL.L.LLLLL.LLLLLL.LL.LL.LLLLLLLLL.LLLL.LLLLLLLLLLLLLLL.LLLLLLLLLLLLLLLLLLL
LLLLLLLLLL.LLLLLLL.LLLL.LLLLLLL.LLL.LL.LLLLL.LLLLLLLLLLLLLLLLLLLLL.LLLLLLLLLLLLLLLL.LLLLLLLLL.L
LLLL.LLLLL.LLLLLLL.LLLL.LLLLLLL.LLLLLL.LLLLLLL.LLLLLLLLLLLLLLLLLLL.LLLLLLLL.LLLLLLLLLLLL.LLLLLL
LLLLLLLLL..LLLLLLL.L.LL..LLLLLLLLLLLLL.LLLLL.LLLLLLLLLLLLLL.LLL.LL.LLLLLLLL.LLLL.LLLLLLLLLLLLLL
LLLLLLLLLLLLL.LLLLLLLLL.LLLLLL..LLLLLL.LLLLL.LLLLLLLLLLL.LLLLLLLLLL.LLLLLLL.LLLLLLL.LLLLLLLLLLL
..LLL..L..L...L..L..L.L.....L............L.....L..L....L.LL......L.....L..LLL..L.L..L..........
LLL.LLLLLL.LLLLLLLLLLLL.LL.LLLLLLLLLLL.LLLLLL.LLLLLLL..LLLL.LLLL.L.L.LLLL.LLLLLLLLLLLLLLLLLLLLL
LLLLLLLLLL.LLLLLLL.LLLL.LLLLLLL.LLLLLL.LL.LL.LLLLLLLLLL.LLL.LL.LLLLLLLLLLLL.LLLLLLLLLLLL.LLLLLL
.LLLLLLLLL.LLLL.LLLLLLLLLLLLL...LL.L.LLLLLLL.LLLLLLLLLLLLLLLLLLLLL.LLLLLLLL.LLLLLLLLLL.L.LLLLLL
LLLL.LLLLLLLLLLLLL.LLLL.LLLLLLLLLLLLLL.LLLLL.LLLLL.LLL.LLLL.LLLLLLLLLLLL.LLLLL.LLLLLLLLLLLLLLLL
LLLLLLLLLL.LLLLLLL.LLLLLLLLLLLL.LLLLLL.LLLLL.LLLLLLLLL.LLLLL.LLL.L.LLLLLLLL.LLLLLL..LLLLLLLLLLL
.LLLLLLLLL.LLLLLLL.LLLL.LLLLLL..LLLLLL.LLLLL.LLLLLLLLLLLLLLLLLLLLL.LLLLLLLL.LLLLLLL.LLLL.LLLLLL
.L.....LL......L........L.L....LLL......L...L.LL...L...L..LL................L...L..L...........
LLLLLLLLLL.LLLLLLL.LLLL.LLLL.LL.LLLLLL.LLLL..LLLLLLLLLLLLLLLLLLLLL.LLLLLLLL.LLLL.LL.LLL.LLLLLLL
LLLLLL.LLL.LLLLLL..LLL..LLLLLL..LLLLLL.LLLL..LLLLLLLLLLLLLLLLLLLLL.LLLLLLLL.LLLLLLL..LLLLLLLLLL
LLLLLL..LL.LLLLLLL.LLLL.LLLL.LL.LLL.LL.LLLLLLLLLLLLLLLLLLLL.LLLLLL.LLLLLLLLLLLLLLLL.LLLLLLLLLLL
LLLLLLLLLL.LLLLLLL.LLLLLLLLLLLL.LLLLLL.LLLLLLLLLLLLLLL.LL.L.LLLLLL.L.LLLLLL.LLLLLLLLLLLLLLLL.LL
LLLLLLLLLL.LLLLLL.LLLLLLL.LLLLL.LLLLL.LLLLLL.L.LLLLLLL.LL.LLLLLLLLLLL.LLLLLLLLLLLLL.LLLLLLLLLLL
LLLLLLLLLL.LLLLLLL.LLLL.LLLLLLLLLLLLLL.LLLL..LLLLLLLLL.LLLLLLLLLLLLL.LLLLLL.LLLLLLL.LL.L.LLLLLL
LLLLL.LLLL.LLLLLLL..LLLLLLLLLLL.LLLLLL.LLL.L.L.LLLLLLLLLLLL.LLLL.L.LLLLLLLL.LLLLLLL.LLLLLLLLLLL
....LLL...LLL.....L.L...LL...L..L....LL...L.L.LLLLL....L......LLLL......L..LLLLL.L.LL...LL..LLL
LLLLLLLLLLLLLLLLLLLLLLL.LLLLLLL.LL.LLL.LLLLL.LLLLLLLLL.LLLL.LLLLLL.LLLLLLLL.LLLLLLLLLLLL.LLLLLL
LLLLLLLLLLLLLLLLLL.LLLLLLLLLLLL.LLLLLL.LLLLL.LLLLLLLLL.LLLL.LLLLLL.LLLLLLLLLLLLLLLL.LLLLLLLLLLL
.LLLLLLLLL.L..LLLLLLLLLLLLLLLLL.LLLLLL.LLLL..LLLLLLLLL.LLLLLLLLLLL.LLLLLLLLLLLLLLLL.LLLLLLLLLLL
LLLLLLLLLL.LLLLLLL.LLLL.L.L.LLLLLLLLLL.LLLLLLLLLLLLLLLLLLLL.LLLLLL.LLLLLLLL.LLL.LLLL.LLL.LLLLLL
LLLLLLLLLLLLLLLLLL.LLLL.LLLLLLLLLLLLLLLLLLLLLLLLLLLLLL.LLLL.LLLLLL.LLLLLLLL..LLLLLL.LLLL.L.LLLL"""

# ╔═╡ c06dfe28-3b9b-11eb-0e90-73b965f01cef
res = collect.(split(sbig))

# ╔═╡ f2078fda-3b9b-11eb-02f6-69faf6d6ce6b
parsedict = Dict('L'=>0, '.'=>-1, '#'=>1)

# ╔═╡ 74450896-3ba2-11eb-27f9-5f052f8bf906
printdict = Dict(-1 => '.', 0 => 'L', 1 => '#')

# ╔═╡ 39aeaf84-3b9d-11eb-209a-15a19031a709
state = ((ch -> parsedict[ch]).(hcat(res...)))'

# ╔═╡ 85e5c7d2-3b9d-11eb-2a71-9fff27469aa8
function nextstate(state, countfn, occthreshold)
	emptyseat = state .== 0
	occupied = state .> 0
	occneigh = countfn(state)
	update1 = (emptyseat) .& (occneigh .== 0)  # 0 -> 1
	update2 = (occupied) .& (occneigh .>= occthreshold)  # 1 -> 0
	anyupdate = (sum(update1) + sum(update2)) > 0
	state + update1 - update2, anyupdate
end

# ╔═╡ 716c36ee-3b9d-11eb-02cd-59c13327fbb4
function run(countfn, occthreshold; maxiter=100)
	mystate = state
	for i=1:maxiter
		mystate, anyupdate = nextstate(mystate, countfn, occthreshold)
		if anyupdate
			println("Updated")
		else
			println("No change after $i steps")
			println("Sum:", sum(mystate.==1))
			break
		end
		#printstate(mystate)
	end
	mystate
end

# ╔═╡ 837a6fae-3ba2-11eb-3dd4-a971f8334552
function printstate(state)
	for col = 1:size(state, 1)
		for row = 1:size(state, 2)
			print(printdict[state[col,row]])
		end
		println()
	end
	println()
end

# ╔═╡ eb8e5ce6-3ba6-11eb-34e1-c541043ea307
"""
Part 2: for each seat, count the occupancy of the next seats in each of the eight cardinal directions.
"""
function countoccupiedneighbours2(state)
	accum = zeros(Int64, size(state))
	row, col = size(state)
	for i=1:row
		for j=1:col
			if state[i, j] < 0
				continue
			end
			for delta in (
					[0,1], [0,-1], [1,0], [-1,0],
					[1,1], [1,-1], [-1,1], [-1,-1])
				pos = [i,j] + delta
				while ((1 <= pos[1] <= row)
						&& (1 <= pos[2] <= col)
						&& (state[pos...] < 0))
					pos += delta
				end
				if ((1 <= pos[1] <= row) && (1 <= pos[2] <= col))
					if state[pos...] == 1
						accum[i,j] += 1
					end
				end
			end
		end
	end
	accum
end

# ╔═╡ f89e53ea-3c7c-11eb-323b-576f7f77da00
sum(run(countoccupiedneighbours2, 5) .== 1)

# ╔═╡ 19ad6462-3b9f-11eb-056e-1b99dc5b3979
"""
Shifts an array left or right and/or up or down and pads the empty row/column.
"""
function shiftpad(a, leftright, topbottom; v=0)
	row, col = size(a)
	if leftright == "l"
		yold = 1:col-1
		ynew = 2:col
	elseif leftright == "r"
		yold = 2:col
		ynew = 1:col-1
	else
		yold = ynew = 1:col
	end
	if topbottom == "t"
		xold = 1:row-1
		xnew = 2:row
	elseif topbottom == "b"
		xold = 2:row
		xnew = 1:row-1
	else
		xold = xnew = 1:row
	end

	PaddedView(v, convert(Array{Int64,2}, a)[xold, yold], (1:row, 1:col), (xnew, ynew))
end

# ╔═╡ 6100b904-3b9f-11eb-1d2e-d5f8ed748767
"""
Sums up the values in the eight neighbouring cells for a 2D array.
"""
function neighboursum(a)
	accum = zeros(Int64, size(a))
	for lr in ("", "l", "r")
		for tb in ("", "t", "b")
			if lr == "" && tb == ""
				continue
			end
			accum += shiftpad(a, lr, tb)
		end
	end
	accum
end

# ╔═╡ 222c5fc0-3c7c-11eb-3f59-095768dc679b
"""
Part 1: for each seat, count the occupied seats amongst the eight neighbouring spaces.
"""
function countoccupiedneighbours(state)
	neighboursum(state .> 0)
end

# ╔═╡ b5b08618-3c7c-11eb-1a95-25275bbf81af
sum(run(countoccupiedneighbours, 4) .== 1)

# ╔═╡ Cell order:
# ╠═97dcb6ca-3b9b-11eb-2c96-0904d9a90046
# ╟─1501f7ee-3ba3-11eb-2b57-7b10a8d1d5d4
# ╠═c06dfe28-3b9b-11eb-0e90-73b965f01cef
# ╠═f2078fda-3b9b-11eb-02f6-69faf6d6ce6b
# ╟─74450896-3ba2-11eb-27f9-5f052f8bf906
# ╠═39aeaf84-3b9d-11eb-209a-15a19031a709
# ╠═85e5c7d2-3b9d-11eb-2a71-9fff27469aa8
# ╠═716c36ee-3b9d-11eb-02cd-59c13327fbb4
# ╠═b5b08618-3c7c-11eb-1a95-25275bbf81af
# ╠═f89e53ea-3c7c-11eb-323b-576f7f77da00
# ╠═837a6fae-3ba2-11eb-3dd4-a971f8334552
# ╠═eb8e5ce6-3ba6-11eb-34e1-c541043ea307
# ╠═222c5fc0-3c7c-11eb-3f59-095768dc679b
# ╠═6100b904-3b9f-11eb-1d2e-d5f8ed748767
# ╠═9dfa6a82-3b9d-11eb-2557-3bb72249910a
# ╠═19ad6462-3b9f-11eb-056e-1b99dc5b3979


"""
    frule([::RuleConfig,] (Δf, Δx...), f, x...)

Expressing the output of `f(x...)` as `Ω`, return the tuple:

    (Ω, ΔΩ)

The second return value is the differential w.r.t. the output.

If no method matching `frule((Δf, Δx...), f, x...)` has been defined, then return `nothing`.

Examples:

unary input, unary output scalar function:

```jldoctest frule
julia> dself = NoTangent();

julia> x = rand()
0.8236475079774124

julia> sinx, Δsinx = frule((dself, 1), sin, x)
(0.7336293678134624, 0.6795498147167869)

julia> sinx == sin(x)
true

julia> Δsinx == cos(x)
true
```

Unary input, binary output scalar function:

```jldoctest frule
julia> sincosx, Δsincosx = frule((dself, 1), sincos, x);

julia> sincosx == sincos(x)
true

julia> Δsincosx[1] == cos(x)
true

julia> Δsincosx[2] == -sin(x)
true
```

Note that techically speaking julia does not have multiple output functions, just functions
that return a single output that is iterable, like a `Tuple`.
So this is actually a [`Tangent`](@ref):
```jldoctest frule
julia> Δsincosx
Tangent{Tuple{Float64, Float64}}(0.6795498147167869, -0.7336293678134624)
```

The optional [`RuleConfig`](@ref) option allows specifying frules only for AD systems that
support given features. If not needed, then it can be omitted and the `frule` without it
will be hit as a fallback. This is the case for most rules.

See also: [`rrule`](@ref), [`@scalar_rule`](@ref), [`RuleConfig`](@ref)
"""
frule(ȧrgs, f, ::Vararg{Any}) = nothing

# if no config is present then fallback to config-less rules
frule(::RuleConfig, args...) = frule(args...)

# Manual fallback for keyword arguments. Usually this would be generated by
#
#   frule(::Any, ::Vararg{Any}; kwargs...) = nothing
#
# However - the fallback method is so hot that we want to avoid any extra code
# that would be required to have the automatically generated method package up
# the keyword arguments (which the optimizer will throw away, but the compiler
# still has to manually analyze). Manually declare this method with an
# explicitly empty body to save the compiler that work.
const frule_kwfunc = Core.kwftype(typeof(frule)).instance
(::typeof(frule_kwfunc))(::Any, ::typeof(frule), ȧrgs, f, ::Vararg{Any}) = nothing
function (::typeof(frule_kwfunc))(kws::Any, ::typeof(frule), ::RuleConfig, args...)
    return frule_kwfunc(kws, frule, args...)
end

"""
    rrule([::RuleConfig,] f, x...)

Expressing `x` as the tuple `(x₁, x₂, ...)` and the output tuple of `f(x...)`
as `Ω`, return the tuple:

    (Ω, (Ω̄₁, Ω̄₂, ...) -> (s̄elf, x̄₁, x̄₂, ...))

Where the second return value is the the propagation rule or pullback.
It takes in differentials corresponding to the outputs (`x̄₁, x̄₂, ...`),
and `s̄elf`, the internal values of the function itself (for closures)

If no method matching `rrule(f, xs...)` has been defined, then return `nothing`.

Examples:

unary input, unary output scalar function:

```jldoctest
julia> x = rand();

julia> sinx, sin_pullback = rrule(sin, x);

julia> sinx == sin(x)
true

julia> sin_pullback(1) == (NoTangent(), cos(x))
true
```

binary input, unary output scalar function:

```jldoctest
julia> x, y = rand(2);

julia> hypotxy, hypot_pullback = rrule(hypot, x, y);

julia> hypotxy == hypot(x, y)
true

julia> hypot_pullback(1) == (NoTangent(), (x / hypot(x, y)), (y / hypot(x, y)))
true
```

The optional [`RuleConfig`](@ref) option allows specifying rrules only for AD systems that
support given features. If not needed, then it can be omitted and the `rrule` without it
will be hit as a fallback. This is the case for most rules.

See also: [`frule`](@ref), [`@scalar_rule`](@ref), [`RuleConfig`](@ref)
"""
rrule(::Any, ::Vararg{Any}) = nothing

# if no config is present then fallback to config-less rules
rrule(::RuleConfig, args...) = rrule(args...)

# Manual fallback for keyword arguments. See above
const rrule_kwfunc = Core.kwftype(typeof(rrule)).instance
(::typeof(rrule_kwfunc))(::Any, ::typeof(rrule), ::Any, ::Vararg{Any}) = nothing
function (::typeof(rrule_kwfunc))(kws::Any, ::typeof(rrule), ::RuleConfig, args...)
    return rrule_kwfunc(kws, rrule, args...)
end


@eval struct SiteType{T}
  (f::Type{<:SiteType})() = $(Expr(:new, :f))
end

# Note that the complicated definition of
# SiteType above is a workaround for performance
# issues when creating parameterized types
# in Julia 1.4 and 1.5-beta. Ideally we
# can just use the following in the future:
# struct SiteType{T}
# end

"""
SiteType is a parameterized type which allows
making Index tags into Julia types. Use cases
include overloading functions such as `op`,
`siteinds`, and `state` which generate custom
operators, Index arrays, and IndexVals associated
with Index objects having a certain tag.

To make a SiteType type, you can use the string
macro notation: `SiteType"MyTag"`

To make a SiteType value or object, you can use
the notation: `SiteType("MyTag")`

There are currently a few built-in site types
recognized by `ITensors.jl`. The system is easily extensible
by users. To add new operators to an existing site type,
you can follow the instructions [here](http://itensor.org/docs.cgi?vers=julia&page=formulas/sitetype_extending).
To create new site types, you can follow the instructions
[here](https://itensor.org/docs.cgi?vers=julia&page=formulas/sitetype_basic) and
[here](https://itensor.org/docs.cgi?vers=julia&page=formulas/sitetype_qns).

The current built-in site types are:

- `SiteType"S=1/2"` (or `SiteType"S=½"`)
- `SiteType"S=1"`
- `SiteType"Fermion"`
- `SiteType"tJ"`
- `SiteType"Electron"`

# Examples

Tags on indices get turned into SiteTypes internally, and then
we search for overloads of functions like `op` and `siteind`.
For example:
```julia
julia> s = siteind("S=1/2")
(dim=2|id=862|"S=1/2,Site")

julia> @show op("Sz", s);
op(s, "Sz") = ITensor ord=2
Dim 1: (dim=2|id=862|"S=1/2,Site")'
Dim 2: (dim=2|id=862|"S=1/2,Site")
NDTensors.Dense{Float64,Array{Float64,1}}
 2×2
 0.5   0.0
 0.0  -0.5

julia> @show op("Sx", s);
op(s, "Sx") = ITensor ord=2
Dim 1: (dim=2|id=862|"S=1/2,Site")'
Dim 2: (dim=2|id=862|"S=1/2,Site")
NDTensors.Dense{Float64,Array{Float64,1}}
 2×2
 0.0  0.5
 0.5  0.0

julia> @show op("Sy", s);
op(s, "Sy") = ITensor ord=2
Dim 1: (dim=2|id=862|"S=1/2,Site")'
Dim 2: (dim=2|id=862|"S=1/2,Site")
NDTensors.Dense{Complex{Float64},Array{Complex{Float64},1}}
 2×2
 0.0 + 0.0im  -0.0 - 0.5im
 0.0 + 0.5im   0.0 + 0.0im

julia> s = siteind("Electron")
(dim=4|id=734|"Electron,Site")

julia> @show op("Nup", s);
op(s, "Nup") = ITensor ord=2
Dim 1: (dim=4|id=734|"Electron,Site")'
Dim 2: (dim=4|id=734|"Electron,Site")
NDTensors.Dense{Float64,Array{Float64,1}}
 4×4
 0.0  0.0  0.0  0.0
 0.0  1.0  0.0  0.0
 0.0  0.0  0.0  0.0
 0.0  0.0  0.0  1.0
```

Many operators are available, for example:

- `SiteType"S=1/2"`: `"Sz"`, `"Sx"`, `"Sy"`, `"S+"`, `"S-"`, ...
- `SiteType"Electron"`: `"Nup"`, `"Ndn"`, `"Nupdn"`, `"Ntot"`, `"Cup"`, `"Cdagup"`, `"Cdn"`, `"Cdagup"`, `"Sz"`, `"Sx"`, `"Sy"`, `"S+"`, `"S-"`, ...
- ...

You can view the source code for the internal SiteType definitions
and operators that are defined [here](https://github.com/ITensor/ITensors.jl/tree/master/src/physics/site_types).
"""
SiteType(s::AbstractString) = SiteType{Tag(s)}()

SiteType(t::Tag) = SiteType{t}()

tag(::SiteType{T}) where {T} = T

macro SiteType_str(s)
  SiteType{Tag(s)}
end

# Keep TagType defined for backwards
# compatibility; will be deprecated later
const TagType = SiteType
macro TagType_str(s)
  TagType{Tag(s)}
end

#---------------------------------------
#
# op system
#
#---------------------------------------

@eval struct OpName{Name}
  (f::Type{<:OpName})() = $(Expr(:new, :f))
end

# Note that the complicated definition of
# OpName above is a workaround for performance
# issues when creating parameterized types
# in Julia 1.4 and 1.5-beta. Ideally we
# can just use the following in the future:
# struct OpName{Name}
# end

"""
OpName is a parameterized type which allows
making strings into Julia types for the purpose
of representing operator names.
The main use of OpName is overloading the
`ITensors.op!` method which generates operators
for indices with certain tags such as "S=1/2".

To make a OpName type, you can use the string
macro notation: `OpName"MyTag"`.

To make an OpName value or object, you can use
the notation: `OpName("myop")`
"""
OpName(s::AbstractString) = OpName{Symbol(s)}()
OpName(s::Symbol) = OpName{s}()
name(::OpName{N}) where {N} = N

macro OpName_str(s)
  OpName{Symbol(s)}
end

# Default implementations of op and op!
op(::OpName, ::SiteType; kwargs...) = nothing
op(::OpName, ::SiteType, ::Index...; kwargs...) = nothing
op(::OpName, ::SiteType, ::SiteType,
   sitetypes_inds::Union{SiteType, Index}...; kwargs...) = nothing
op!(::ITensor, ::OpName, ::SiteType, ::Index...; kwargs...) = nothing
op!(::ITensor, ::OpName, ::SiteType, ::SiteType,
    sitetypes_inds::Union{SiteType, Index}...; kwargs...) = nothing

# Deprecated version, for backwards compatibility
op(::SiteType, ::Index, ::AbstractString; kwargs...) = nothing

function _sitetypes(ts::TagSet)
  Ntags = length(ts)
  return SiteType[SiteType(ts[n]) for n in 1:Ntags]
end

_sitetypes(i::Index) = _sitetypes(tags(i))

"""
    op(opname::String, s::Index; kwargs...)

Return an ITensor corresponding to the operator
named `opname` for the Index `s`. The operator
is constructed by calling an overload of either
the `op` or `op!` methods which take a `SiteType`
argument that corresponds to one of the tags of
the Index `s` and an `OpName"opname"` argument
that corresponds to the input operator name.

Operator names can be combined using the `"*"`
symbol, for example `"S+*S-"` or `"Sz*Sz*Sz"`.
The result is an ITensor made by forming each operator
then contracting them together in a way corresponding
to the usual operator product or matrix multiplication.

The `op` system is used by the AutoMPO
system to convert operator names into ITensors,
and can be used directly such as for applying
operators to MPS.

# Example
```julia
s = Index(2, "Site,S=1/2")
Sz = op("Sz", s)
```
"""
function op(name::AbstractString,
            s::Index...;
            kwargs...)

  name = strip(name)

  # TODO: filter out only commons tags
  # if there are multiple indices
  commontags_s = commontags(s...)

  # Interpret operator names joined by *
  # as acting sequentially on the same site
  starpos = findfirst("*", name)
  if !isnothing(starpos)
    op1 = name[1:starpos.start-1]
    op2 = name[starpos.start+1:end]
    return product(op(op1, s...; kwargs...),
                   op(op2, s...; kwargs...))
  end

  common_stypes  = _sitetypes(commontags_s)
  push!(common_stypes,SiteType("Generic"))
  opn = OpName(name)

  #
  # Try calling a function of the form:
  #    op(::OpName, ::SiteType, ::Index; kwargs...)
  #
  for st in common_stypes
    res = op(opn, st, s...; kwargs...)
    if !isnothing(res)
      return res
    end
  end

  # otherwise try calling a function of the form:
  #    op!(::ITensor, ::OpName, ::SiteType, ::Index; kwargs...)
  #
  Op = emptyITensor(prime.(s)..., dag.(s)...)
  for st in common_stypes
    op!(Op, opn, st, s...; kwargs...)
    if !isempty(Op)
      return Op
    end
  end

  #
  # otherwise try calling a function of the form:
  #    op(::OpName, ::SiteType; kwargs...)
  # which returns a Julia matrix
  #
  for st in common_stypes
    op_mat = op(opn, st; kwargs...)
    if !isnothing(op_mat)
      rs = reverse(s)
      return itensor(op_mat, prime.(rs)..., dag.(rs)...)
    end
  end

  if length(s) > 1
    # No overloads for common tags found. It might be a
    # case of making an operator with mixed site types,
    # searching for overloads like:
    #   op(::OpName,
    #      ::SiteType...,
    #      ::Index...;
    #      kwargs...)
    #   op!(::ITensor, ::OpName,
    #       ::SiteType...,
    #       ::Index...;
    #       kwargs...)
    stypes = _sitetypes.(s)

    for st in Iterators.product(stypes...)
      res = op(opn, st..., s...; kwargs...)
      if !isnothing(res)
        return res
      end
    end

    Op = emptyITensor(prime.(s)..., dag.(s)...)
    for st in Iterators.product(stypes...)
      op!(Op, opn, st..., s...; kwargs...)
      if !isempty(Op)
        return Op
      end
    end
    error("Older op interface does not support multiple indices with mixed site types. You may want to overload `op(::OpName, ::SiteType..., ::Index...)` or `op!(::ITensor, ::OpName, ::SiteType..., ::Index...) for the operator \"$name\" and Index tags $(tags.(s)).")
  end

  #
  # otherwise try calling a function of the form:
  #   op(::SiteType, ::Index, ::AbstractString)
  #
  # (Note: this version is for backwards compatibility
  #  after version 0.1.10, and may be eventually
  #  deprecated)
  #
  for st in common_stypes
    res = op(st, s[1], name; kwargs...)
    if !isnothing(res)
      return res
    end
  end

  throw(ArgumentError("Overload of \"op\" or \"op!\" functions not found for operator name \"$name\" and Index tags: $(commontags_s))"))
end

# For backwards compatibility, version of `op`
# taking the arguments in the other order:
op(s::Index,
   opname::AbstractString;
   kwargs...) = op(opname, s; kwargs...)

# To ease calling of other op overloads,
# allow passing a string as the op name
op(opname::AbstractString,t::SiteType) = op(OpName(opname),t)

"""
    op(opname::String,sites::Vector{<:Index},n::Int; kwargs...)

Return an ITensor corresponding to the operator
named `opname` for the n'th Index in the array
`sites`.

# Example

```julia
s = siteinds("S=1/2", 4)
Sz2 = op("Sz", s, 2)
```
"""
op(opname::AbstractString, s::Vector{<:Index},
   ns::Vararg{Int, N}; kwargs...) where {N} =
  op(opname, ntuple(n -> s[ns[n]], Val(N))...; kwargs...)

op(s::Vector{ <: Index}, opname::AbstractString,
   ns::Int...; kwargs...) =
  op(opname, s, ns...; kwargs...)

op(s::Vector{ <: Index}, opname::AbstractString,
   ns::Tuple{Vararg{Int}}, kwargs::NamedTuple) =
  op(opname, s, ns...; kwargs...)

op(s::Vector{ <: Index}, opname::AbstractString,
   ns::Int, kwargs::NamedTuple) =
  op(opname, s, ns; kwargs...)

# This version helps with call like `op.(Ref(s), os)` where `os`
# is a vector of tuples.
op(s::Vector{ <: Index}, os::Tuple{String, Vararg}) =
  op(s, os...)

# Here, Ref is used to not broadcast over the vector of indices
# TODO: consider overloading broadcast for `op` with the example
# here: https://discourse.julialang.org/t/how-to-broadcast-over-only-certain-function-arguments/19274/5
# so that `Ref` isn't needed.
ops(s::Vector{ <: Index},
    os::AbstractArray{ <: Tuple{String, Vararg}}) =
  op.(Ref(s), os)

ops(os::Vector{ <: Tuple{String, Vararg}}, s::Vector{ <: Index}) =
  op.(Ref(s), os)

#---------------------------------------
#
# state system
#
#---------------------------------------

@eval struct StateName{Name}
  (f::Type{<:StateName})() = $(Expr(:new, :f))
end

StateName(s::AbstractString) = StateName{SmallString(s)}()
StateName(s::SmallString) = StateName{s}()
name(::StateName{N}) where {N} = N

macro StateName_str(s)
  StateName{SmallString(s)}
end

state(::SiteType, ::StateName) = nothing
state(::SiteType, ::AbstractString) = nothing

function state(s::Index,
               name::AbstractString)::IndexVal
  stypes  = _sitetypes(s)
  sname = StateName(name)

  # Try calling state(::SiteType"Tag",::StateName"Name")
  for st in stypes
    res = state(st,sname)
    !isnothing(res) && return s(res)
  end

  # Try calling state(::SiteType"Tag","Name")
  for st in stypes
    res = state(st,name)
    !isnothing(res) && return s(res)
  end

  throw(ArgumentError("Overload of \"state\" function not found for Index tags $(tags(s))"))
end

state(s::Index,n::Integer) = s[n]

state(sset::Vector{<:Index},j::Integer,st) = state(sset[j],st)

#---------------------------------------
#
# siteind system
#
#---------------------------------------

space(st::SiteType; kwargs...) = nothing

space(st::SiteType, n::Int; kwargs...) =
  space(st; kwargs...)

function space_error_message(st::SiteType)
  return "Overload of \"space\",\"siteind\", or \"siteinds\" functions not found for Index tag: $(tag(st))"
end

function siteind(st::SiteType; addtags = "", kwargs...)
  sp = space(st; kwargs...)
  isnothing(sp) && return nothing
  return Index(sp, "Site, $(tag(st)), $addtags")
end

function siteind(st::SiteType, n; kwargs...)
  s = siteind(st; kwargs...)
  !isnothing(s) && return addtags(s, "n=$n")
  sp = space(st, n; kwargs...)
  isnothing(sp) && error(space_error_message(st))
  return Index(sp, "Site, $(tag(st)), n=$n")
end

siteind(tag::String; kwargs...) =
  siteind(SiteType(tag); kwargs...)

siteind(tag::String, n; kwargs...) =
  siteind(SiteType(tag), n; kwargs...)

# Special case of `siteind` where integer (dim) provided
# instead of a tag string
siteind(d::Integer,n::Integer; kwargs...) = Index(d,"Site,n=$n")

#---------------------------------------
#
# siteinds system
#
#---------------------------------------

siteinds(::SiteType, N; kwargs...) = nothing

"""
  siteinds(tag::String, N::Integer; kwargs...)

Create an array of `N` physical site indices of type `tag`.
Keyword arguments can be used to specify quantum number conservation,
see the `space` function corresponding to the site type `tag` for
supported keyword arguments.
"""
function siteinds(tag::String,
                  N::Integer; kwargs...)
  st = SiteType(tag)

  si = siteinds(st,N;kwargs...)
  if !isnothing(si)
    return si
  end

  return [siteind(st,j; kwargs...) for j=1:N]
end

"""
  siteinds(f::Function, N::Integer; kwargs...)

Create an array of `N` physical site indices where the site type at site `n` is given
by `f(n)` (`f` should return a string).
"""
function siteinds(f::Function,
                  N::Integer; kwargs...)
  [siteind(f(n),n; kwargs...) for n=1:N]
end

# Special case of `siteinds` where integer (dim)
# provided instead of a tag string
function siteinds(d::Integer,
                  N::Integer; kwargs...)
  return [siteind(d,n; kwargs...) for n=1:N]
end

#---------------------------------------
#
# has_fermion_string system
#
#---------------------------------------

has_fermion_string(::OpName, ::SiteType) = nothing

function has_fermion_string(opname::AbstractString,
                            s::Index;
                            kwargs...)::Bool
  opname = strip(opname)

  # Interpret operator names joined by *
  # as acting sequentially on the same site
  starpos = findfirst("*", opname)
  if !isnothing(starpos)
    op1 = opname[1:starpos.start-1]
    op2 = opname[starpos.start+1:end]
    return xor(has_fermion_string(op1,s; kwargs...),
               has_fermion_string(op2,s; kwargs...))
  end

  Ntags = length(tags(s))
  stypes = _sitetypes(s)
  opn = OpName(opname)
  for st in stypes
    res = has_fermion_string(opn, st)
    !isnothing(res) && return res
  end
  return false
end


# # GARTEUR SM-AG19 Testbed: Construction of the geometry

# Source code: [`garteur_geometry_vis_tut.jl`](garteur_geometry_vis_tut.jl)

# ## Description

# This virtual test application is based on the test article
# used by the GARTEUR Structures & Materials Action Group 19
# which organized a Round Robin exercise where 12 European laboratories
# tested a single structure between 1995 and 1997. The benchmark structure
# was a laboratory structure built to simulate the dynamic behaviour
# of an aeroplane. The structure was initially built for a benchmark
# study on experimental modal analysis conducted by the
# Structures and Materials Action Group (SM-AG19) of the Group
# for Aeronautical Research and Technology in EURope (GARTEUR).
# The test-bed was designed and manufactured by ONERA, France.

# ![](IMAC97photo.png)

# ### References

# - [GARTEUR] Ground Vibration Test Techniques, compiled by A Gravelle, GARTEUR
#   Structures & Materials Action Group 19 Technical report TP-115, 1999.
# - [BW] Etienne Balmes, Jan R. Wright, GARTEUR GROUP ON GROUND VIBRATION
#   TESTING | RESULTS FROM THE TEST OF A SINGLE STRUCTURE BY 12 LABORATORIES IN
#   EUROPE, Proceedings of DETC'97, 1997 ASME Design Engineering Technical
#   Conferences, September 14-17, 1997, Sacramento, California.

# ## Goals

# - Show how to construct model from multiple connected beams.
# - Demonstrate the use of massless connectors.
# - Visualize the structure interactively.
#

##
# ## Geometry of the testbed airplane.

# The aluminum testbed was a rather simple structure which was reasonably
# dynamically representative of a simple airplane structure [GARTEUR](@ref
# References). It was composed of several beams simulating a fuselage with wings
# and a tail. Wing tip drums allowed to adjust bending and torsion frequencies
# similarly to airplane ones, with some very close modal frequencies.

# ![](garteur-geom.png)

# The script included below defines the geometry of the structure, the
# cross-sectional properties, the connectivity, and the location of the nodes.

include("garteur_geometry_tut.jl")

##
# ## Basic visualization

# Here we use the `PlotlyJS` plotting library, with some simple utilities for
# generating geometry of beams.
using PlotlyJS
using FinEtoolsFlexBeams.VisUtilModule: plot_solid, plot_space_box, render, default_layout_3d, save_to_json

# The colors are used to help distinguish between the individual parts of the model.
colors = [
"rgb(125, 155, 155)",  # 1 body
"rgb(125, 155, 155)",  # 2 wing
"rgb(125, 155, 155)",  # 3 drums
"rgb(125, 155, 155)",  # 4 vertical tail
"rgb(125, 155, 155)",  # 5 horizontal tail
"rgb(125, 155, 155)",  # 6 constraining plate
"rgb(15, 15, 155)",  # 7 massless connectors: wing beam to plate
"rgb(125, 15, 15)",  # 8 massless connectors: wing beam to drums
"rgb(125, 175, 15)",  # 9 massless connectors: fuselage to wing beam
"rgb(125, 175, 15)",  # 10 massless connectors: fuselage to tail
"rgb(15, 155, 155)",  # 11 massless connectors: sensors, point masses
]

# The geometry is defined in terms of "traces" (a trace is in the `PlotlyJS`
# parlance a graphical object; it could be a curve, surface, and a lot of other
# things). It is the name for a graphical object in `PlotlyJS`. The two points
# below define a box, which is helpful when setting the extents of the graphics
# display.
tbox = plot_space_box([[-1.2 * L -1.2 * L -1.2 * L]; [+1.2 * L +1.2 * L +1.2 * L]])
# For each finite element set in the array `fesa`, generate the graphics to
# represent that object.
traces = let traces = tbox
    for fes in fesa
        labl  = fes.label[1]
        tm = plot_solid(fens, fes; facecolor=colors[labl]);
        traces = cat(traces, tm; dims = 1)
    end
    traces
end
# The layout of the plot is defined with simple defaults.
layout = default_layout_3d(;width=900, height=900)
# Next, the graphics is rendered, and may be interacted with by zooming,
# panning, etc.
pl = render(traces; layout = layout)

##
# ## Visualizing the nodes

# In order to be able to discern the nodes we will reduce the opacity of the
# surfaces representing the beams, otherwise the nodes would be hidden by these
# surfaces. Otherwise the geometries defined in the same way as above.

tbox = plot_space_box([[-1.2 * L -1.2 * L -1.2 * L]; [+1.2 * L +1.2 * L +1.2 * L]])
traces = let traces = tbox
    for fes in fesa
        labl  = fes.label[1]
        tm = plot_solid(fens, fes; facecolor=colors[labl], opacity = 0.3);
        traces = cat(traces, tm; dims = 1)
    end
    traces
end
# Next we add to the "traces" the graphics representing all the nodes in the
# model as bright red dots.
using FinEtoolsFlexBeams.VisUtilModule: plot_nodes
traces = cat(traces, plot_nodes(fens; color = "rgb(255, 15, 5)"); dims = 1)

# Finally, the graphics is presented.
layout = default_layout_3d(;width=900, height=900)
pl = render(traces; layout = layout)

true