function actions_mdp_fixture()
    dmodel = NoCrashIDMMOBILModel(3, PhysicalParam(3, lane_length=100.0))
    rmodel = NoCrashRewardModel()
    return NoCrashMDP{typeof(rmodel)}(dmodel, rmodel, 1.0)
end

function test_iteration()
    println("\t\tTesting Action iteration.")
    mdp = actions_mdp_fixture()
    s = MLState(0.0,0.0, CarState[CarState(50.0, 2.0, 30.0, 0.0, Multilane.NORMAL, 0)])
    as = actions(mdp)
    as = actions(mdp, s, as)
    @test length(collect(as)) == length(as.acceptable)+1
end

function test_off_road()
    println("\t\tTesting No Offroad Actions...")
    mdp = actions_mdp_fixture()
    as = actions(mdp)
    right = MLState(0.0,0.0, CarState[CarState(50.0, 1.0, 30.0, 0.0, Multilane.NORMAL, 0)])
    ar = collect(actions(mdp, right, as))
    @test length(ar) > 1
    @test all(a -> a.lane_change >= 0., ar)
    left = MLState(0.0,0.0, CarState[CarState(50.0, 3.0, 30.0, 0.0, Multilane.NORMAL, 0)])
    al = collect(actions(mdp, left, as))
    @test length(al) > 1
    @test all(a -> a.lane_change <= 0., al)
end

function test_all_safe()
    println("\t\tTesting case where all actions are safe")
    mdp = actions_mdp_fixture()
    as = actions(mdp)
    allsafe = MLState(0.0,0.0, CarState[CarState(50.0, 2.0, 30.0, 0.0, Multilane.NORMAL, 0)])
    @test length(collect(actions(mdp, allsafe, as))) == 10
end

function failure_4()
    println("\t\tTesting special failure case changing into new lane")
    mdp = Multilane.MLMDP{Multilane.MLState,Multilane.MLAction,Multilane.NoCrashIDMMOBILModel,Multilane.NoCrashRewardModel}(Multilane.NoCrashIDMMOBILModel(10,Multilane.PhysicalParam(0.75,2.0,4.0,31.0,4.0,35.0,27.0,31.0,35.0,27.0,20.0,4,100.0),Multilane.BehaviorModel[Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.0,1.0,2.0,27.5,4.0,4.0),Multilane.MOBILParam(0.5,4.0,0.2),1),Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.5,1.5,1.4,27.5,4.0,4.0),Multilane.MOBILParam(0.25,4.0,0.2),2),Multilane.IDMMOBILBehavior(Multilane.IDMParam(2.0,2.0,0.8,27.5,4.0,4.0),Multilane.MOBILParam(0.0,4.0,0.2),3),Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.0,1.0,2.0,31.0,4.0,4.0),Multilane.MOBILParam(0.5,4.0,0.2),4),Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.5,1.5,1.4,31.0,4.0,4.0),Multilane.MOBILParam(0.25,4.0,0.2),5),Multilane.IDMMOBILBehavior(Multilane.IDMParam(2.0,2.0,0.8,31.0,4.0,4.0),Multilane.MOBILParam(0.0,4.0,0.2),6),Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.0,1.0,2.0,35.0,4.0,4.0),Multilane.MOBILParam(0.5,4.0,0.2),7),Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.5,1.5,1.4,35.0,4.0,4.0),Multilane.MOBILParam(0.25,4.0,0.2),8),Multilane.IDMMOBILBehavior(Multilane.IDMParam(2.0,2.0,0.8,35.0,4.0,4.0),Multilane.MOBILParam(0.0,4.0,0.2),9)],StatsBase.WeightVec{Float64,Array{Float64,1}}([1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],9.0),1.0,1.3333333333333333,0.5,20.0,0.5,[1.0,1.0,1.0,1.0],2.0,false),Multilane.NoCrashRewardModel(100.0,10.0,5.0,4),1.0)

    s = MLState(false,0.0,0.0, CarState[CarState(50.0,3.0,31.22970781020354,0.0,Multilane.NORMAL,1),CarState(99.24797069990433,4.0,27.167643727404155,0.0,Multilane.IDMMOBILBehavior(Multilane.IDMParam(2.0,2.0,0.8,27.5,4.0,4.0),Multilane.MOBILParam(0.0,4.0,0.2),3),2)])

    @test Multilane.is_safe(mdp, s, MLAction(0.0, 1.0))
end

function failure_5()
    println("\t\tTesting special failure case allowing dangerous lane change when abs(lane_change) > 1")
    mdp = Multilane.MLMDP{Multilane.MLState,Multilane.MLAction,Multilane.NoCrashIDMMOBILModel,Multilane.NoCrashRewardModel}(Multilane.NoCrashIDMMOBILModel(10,Multilane.PhysicalParam(0.75,2.0,4.0,31.0,4.0,35.0,27.0,31.0,35.0,27.0,20.0,4,100.0),Multilane.BehaviorModel[Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.0,1.0,2.0,27.5,4.0,4.0),Multilane.MOBILParam(0.5,4.0,0.2),1),Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.5,1.5,1.4,27.5,4.0,4.0),Multilane.MOBILParam(0.25,4.0,0.2),2),Multilane.IDMMOBILBehavior(Multilane.IDMParam(2.0,2.0,0.8,27.5,4.0,4.0),Multilane.MOBILParam(0.0,4.0,0.2),3),Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.0,1.0,2.0,31.0,4.0,4.0),Multilane.MOBILParam(0.5,4.0,0.2),4),Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.5,1.5,1.4,31.0,4.0,4.0),Multilane.MOBILParam(0.25,4.0,0.2),5),Multilane.IDMMOBILBehavior(Multilane.IDMParam(2.0,2.0,0.8,31.0,4.0,4.0),Multilane.MOBILParam(0.0,4.0,0.2),6),Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.0,1.0,2.0,35.0,4.0,4.0),Multilane.MOBILParam(0.5,4.0,0.2),7),Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.5,1.5,1.4,35.0,4.0,4.0),Multilane.MOBILParam(0.25,4.0,0.2),8),Multilane.IDMMOBILBehavior(Multilane.IDMParam(2.0,2.0,0.8,35.0,4.0,4.0),Multilane.MOBILParam(0.0,4.0,0.2),9)],StatsBase.WeightVec{Float64,Array{Float64,1}}([1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],9.0),1.0,2.6666666666666665,0.5,20.0,0.5,[1.0,1.0,1.0,1.0],2.0,false),Multilane.NoCrashRewardModel(100.0,10.0,8.0,1),1.0)
    state = MLState(false,0.0,0.0, CarState[CarState(50.0,2.0,27.0,0.0,Multilane.NORMAL,1),CarState(52.934068808019056,1.0,32.940581978786135,0.0,Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.5,1.5,1.4,35.0,4.0,4.0),Multilane.MOBILParam(0.25,4.0,0.2),8),3),CarState(7.329881331042117,1.5,27.127595226858716,-0.6666666666666666,Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.0,1.0,2.0,35.0,4.0,4.0),Multilane.MOBILParam(0.5,4.0,0.2),7),8),CarState(1.3070886107784006,3.0,27.306814286745862,0.0,Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.0,1.0,2.0,31.0,4.0,4.0),Multilane.MOBILParam(0.5,4.0,0.2),4),10)])
    as = actions(mdp, state, actions(mdp))
    @test all([a.lane_change >= 0.0 for a in as])
end

function failure_6()
    println("\t\tTesting special failure case changing into a car slightly behind.")
    mdp = Multilane.MLMDP{Multilane.MLState,Multilane.MLAction,Multilane.NoCrashIDMMOBILModel,Multilane.TargetLaneReward}(Multilane.NoCrashIDMMOBILModel(10,Multilane.PhysicalParam(0.75,2.0,4.0,31.0,4.0,35.0,27.0,31.0,35.0,27.0,8.0,4,100.0),Multilane.CopulaIDMMOBIL(Multilane.IDMParam(0.7999999999999999,1.0,1.0,27.669999999999998,0.0,4.0),Multilane.IDMParam(2.0,3.0,2.0,38.93,4.0,4.0),Multilane.MOBILParam(0.0,1.0,0.0),Multilane.MOBILParam(1.0,3.0,0.2),Multilane.GaussianCopula([1.0 0.25 0.25 0.25 0.25 0.25 0.25 0.25; 0.25 1.0 0.25 0.25 0.25 0.25 0.25 0.25; 0.25 0.25 1.0 0.25 0.25 0.25 0.25 0.25; 0.25 0.25 0.25 1.0 0.25 0.25 0.25 0.25; 0.25 0.25 0.25 0.25 1.0 0.25 0.25 0.25; 0.25 0.25 0.25 0.25 0.25 1.0 0.25 0.25; 0.25 0.25 0.25 0.25 0.25 0.25 1.0 0.25; 0.25 0.25 0.25 0.25 0.25 0.25 0.25 1.0],[1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0; 0.25 0.968246 0.0 0.0 0.0 0.0 0.0 0.0; 0.25 0.193649 0.948683 0.0 0.0 0.0 0.0 0.0; 0.25 0.193649 0.158114 0.935414 0.0 0.0 0.0 0.0; 0.25 0.193649 0.158114 0.133631 0.92582 0.0 0.0 0.0; 0.25 0.193649 0.158114 0.133631 0.115728 0.918559 0.0 0.0; 0.25 0.193649 0.158114 0.133631 0.115728 0.102062 0.912871 0.0; 0.25 0.193649 0.158114 0.133631 0.115728 0.102062 0.0912871 0.908295]),377605),1.0,0.6666666666666666,1.0,35.0,0.25,false,4.0,Inf),Multilane.TargetLaneReward(4),1.0)
    state = Multilane.MLState(0.0,0.0,Multilane.CarState[Multilane.CarState(50.0,2.0,30.9774,0.0,Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.86615,2.77692,1.11154,37.674,0.446166,4.0),Multilane.MOBILParam(0.111542,2.77692,0.0223083),371338),1),Multilane.CarState(45.9956,3.0,30.9545,0.0,Multilane.IDMMOBILBehavior(Multilane.IDMParam(1.27863,1.79771,1.60114,32.1611,2.40458,4.0),Multilane.MOBILParam(0.601144,1.79771,0.120229),371341),16)],Nullable{Any}())
    as = actions(mdp, state, actions(mdp))
    # if !all([a.lane_change <= 0.0 for a in as])
    #     Gallium.@enter actions(mdp, state)
    # end
    @test all([a.lane_change <= 0.0 for a in as])
end

function test_actions()
    println("\tTesting Action Space...")
    test_iteration()
    test_off_road()
    test_all_safe()
    # failure_4() # removed because of behavior change
    # failure_5() # removed because of behavior change
    failure_6()
end


module Soss

import Base.rand
using Random
using Reexport: @reexport

@reexport using StatsFuns
using MeasureTheory

export For

using NamedTupleTools
using SampleChains
using SymbolicCodegen

using SymbolicUtils: Symbolic
const MaybeSym{T} = Union{T, Symbolic{T}}

# MeasureTheory.For(f, dims::MaybeSym{<: Integer}...) = ProductMeasure(mappedarray(i -> f(Tuple(i)...), CartesianIndices(dims)))
# MeasureTheory.For(f, dims::MaybeSym{<: AbstractUnitRange}...) = ProductMeasure(mappedarray(i -> f(Tuple(i)...), CartesianIndices(dims)))

import MacroTools: prewalk, postwalk, @q, striplines, replace, @capture
import MacroTools
import MLStyle
# import MonteCarloMeasurements
# using MonteCarloMeasurements: Particles, StaticParticles, AbstractParticles

using Requires
using ArrayInterface: StaticInt

using TransformVariables: as, asℝ, as𝕀, asℝ₊
import TransformVariables
const TV = TransformVariables

using SimplePosets: SimplePoset

using RuntimeGeneratedFunctions
RuntimeGeneratedFunctions.init(@__MODULE__)

const ∞ = MeasureTheory.∞

"""
we use this to avoid introduce static type parameters
for generated functions
"""
_unwrap_type(a::Type{<:Type}) = a.parameters[1]

include("noted.jl")
include("core/models/abstractmodel.jl")
include("core/statement.jl")
include("core/models/model.jl")
# include("core/models/jointdistribution.jl")
include("core/canonical.jl")
include("core/dependencies.jl")
include("core/toposort.jl")
include("core/weighted.jl")
include("core/utils.jl")
include("core/models/conditional.jl")

# include("distributions/dist.jl")
# include("distributions/for.jl")
include("distributions/iid.jl")
# include("distributions/mix.jl")
# include("distributions/flat.jl")
# include("distributions/markovchain.jl")

include("primitives/rand.jl")
include("primitives/simulate.jl")
include("primitives/logdensity.jl")
include("primitives/xform.jl")
include("primitives/likelihood-weighting.jl")
# include("primitives/gg.jl")
# @init @require Bijectors="76274a88-744f-5084-9051-94815aaf08c4" begin
#     include("primitives/bijectors.jl")
# end

include("primitives/basemeasure.jl")
include("primitives/testvalue.jl")
# include("primitives/entropy.jl")

include("transforms/predict.jl")
include("transforms/markovblanket.jl")
include("transforms/utils.jl")
include("transforms/basictransforms.jl")
include("transforms/withmeasures.jl")

include("symbolic/symcall.jl")
include("symbolic/symify.jl")
include("symbolic/rules.jl")
include("symbolic/symbolic.jl")
include("symbolic/codegen.jl")

# include("particles.jl")
include("plots.jl")

include("inference/rejection.jl")
# include("inference/dynamicHMC.jl")
# include("inference/advancedhmc.jl")
include("inference/power-posterior.jl")
# include("inference/Δlogdensity.jl")

#
# # include("graph.jl")
# # # include("optim.jl")
include("importance.jl")
#

function __init__()
    @require SampleChainsDynamicHMC = "6d9fd711-e8b2-4778-9c70-c1dfb499d4c4" begin
        include("samplechains/dynamichmc.jl")
    end
end

# # # include("sobols.jl")
# # # include("fromcube.jl")
# # # include("tocube.jl")
end # module


@testset "DC Power Flow Models Monitored Line Flow Constraints and Static Unbounded" begin
    system = PSB.build_system(PSITestSystems, "c_sys5_ml")
    limits = PSY.get_flow_limits(PSY.get_component(MonitoredLine, system, "1"))
    for model in [DCPPowerModel, StandardPTDFModel]
        template = get_thermal_dispatch_template_network(
            NetworkModel(model; PTDF=PSY.PTDF(system)),
        )
        model_m = DecisionModel(template, system; optimizer=HiGHS_optimizer)
        @test build!(model_m; output_dir=mktempdir(cleanup=true)) == PSI.BuildStatus.BUILT
        @test check_variable_bounded(model_m, FlowActivePowerVariable, MonitoredLine)
        @test check_variable_unbounded(model_m, FlowActivePowerVariable, Line)

        @test solve!(model_m) == RunStatus.SUCCESSFUL
        @test check_flow_variable_values(
            model_m,
            FlowActivePowerVariable,
            MonitoredLine,
            "1",
            limits.from_to,
        )
    end
end

@testset "DC Power Flow Models Monitored Line Flow Constraints and Static with Bounds" begin
    system = PSB.build_system(PSITestSystems, "c_sys5_ml")
    set_rate!(PSY.get_component(Line, system, "2"), 1.5)
    for model in [DCPPowerModel, StandardPTDFModel]
        template = get_thermal_dispatch_template_network(
            NetworkModel(model; PTDF=PSY.PTDF(system)),
        )
        set_device_model!(template, DeviceModel(Line, StaticBranch))
        set_device_model!(template, DeviceModel(MonitoredLine, StaticBranchUnbounded))
        model_m = DecisionModel(template, system; optimizer=HiGHS_optimizer)
        @test build!(model_m; output_dir=mktempdir(cleanup=true)) == PSI.BuildStatus.BUILT

        @test check_variable_unbounded(model_m, FlowActivePowerVariable, MonitoredLine)
        # Broken
        # @test check_variable_bounded(model_m, FlowActivePowerVariable, Line)

        @test solve!(model_m) == RunStatus.SUCCESSFUL
        @test check_flow_variable_values(model_m, FlowActivePowerVariable, Line, "2", 1.5)
    end
end

@testset "AC Power Flow Monitored Line Flow Constraints" begin
    system = PSB.build_system(PSITestSystems, "c_sys5_ml")
    limits = PSY.get_flow_limits(PSY.get_component(MonitoredLine, system, "1"))
    template = get_thermal_dispatch_template_network(ACPPowerModel)
    model_m = DecisionModel(template, system; optimizer=ipopt_optimizer)
    @test build!(model_m; output_dir=mktempdir(cleanup=true)) == PSI.BuildStatus.BUILT

    @test check_variable_bounded(model_m, FlowActivePowerFromToVariable, MonitoredLine)
    @test check_variable_unbounded(model_m, FlowReactivePowerFromToVariable, MonitoredLine)

    @test solve!(model_m) == RunStatus.SUCCESSFUL
    @test check_flow_variable_values(
        model_m,
        FlowActivePowerFromToVariable,
        FlowReactivePowerFromToVariable,
        MonitoredLine,
        "1",
        0.0,
        limits.from_to,
    )
end

@testset "DC Power Flow Models for HVDCLine with with Line Flow Constraints, TapTransformer & Transformer2W Unbounded" begin
    ratelimit_constraint_keys = [
        PSI.ConstraintKey(RateLimitConstraint, Transformer2W, "ub"),
        PSI.ConstraintKey(RateLimitConstraint, Transformer2W, "lb"),
        PSI.ConstraintKey(RateLimitConstraint, TapTransformer, "ub"),
        PSI.ConstraintKey(RateLimitConstraint, TapTransformer, "lb"),
    ]

    system = PSB.build_system(PSITestSystems, "c_sys14_dc")
    hvdc_line = PSY.get_component(HVDCLine, system, "DCLine3")
    limits_from = PSY.get_active_power_limits_from(hvdc_line)
    limits_to = PSY.get_active_power_limits_to(hvdc_line)
    limits_min = min(limits_from.min, limits_to.min)
    limits_max = min(limits_from.max, limits_to.max)

    tap_transformer = PSY.get_component(TapTransformer, system, "Trans3")
    rate_limit = PSY.get_rate(tap_transformer)

    transformer = PSY.get_component(Transformer2W, system, "Trans4")
    rate_limit2w = PSY.get_rate(tap_transformer)

    for model in [DCPPowerModel, StandardPTDFModel],
        hvdc_model in [HVDCDispatch, HVDCLossless]

        template =
            get_template_dispatch_with_network(NetworkModel(model; PTDF=PSY.PTDF(system)))
        set_device_model!(template, HVDCLine, hvdc_model)
        model_m = DecisionModel(template, system; optimizer=ipopt_optimizer)
        @test build!(model_m; output_dir=mktempdir(cleanup=true)) == PSI.BuildStatus.BUILT

        @test check_variable_bounded(model_m, FlowActivePowerVariable, HVDCLine)
        @test check_variable_unbounded(model_m, FlowActivePowerVariable, TapTransformer)
        @test check_variable_unbounded(model_m, FlowActivePowerVariable, Transformer2W)

        psi_constraint_test(model_m, ratelimit_constraint_keys)

        @test solve!(model_m) == RunStatus.SUCCESSFUL

        @test check_flow_variable_values(
            model_m,
            FlowActivePowerVariable,
            HVDCLine,
            "DCLine3",
            limits_min,
            limits_max,
        )
        @test check_flow_variable_values(
            model_m,
            FlowActivePowerVariable,
            TapTransformer,
            "Trans3",
            -rate_limit,
            rate_limit,
        )
        @test check_flow_variable_values(
            model_m,
            FlowActivePowerVariable,
            Transformer2W,
            "Trans4",
            -rate_limit2w,
            rate_limit2w,
        )
    end
end

@testset "DC Power Flow Models for Unbounded HVDCLine, and StaticBranchBounds for TapTransformer & Transformer2W" begin
    system = PSB.build_system(PSITestSystems, "c_sys14_dc")
    hvdc_line = PSY.get_component(HVDCLine, system, "DCLine3")
    limits_from = PSY.get_active_power_limits_from(hvdc_line)
    limits_to = PSY.get_active_power_limits_to(hvdc_line)
    limits_min = min(limits_from.min, limits_to.min)
    limits_max = min(limits_from.max, limits_to.max)

    tap_transformer = PSY.get_component(TapTransformer, system, "Trans3")
    rate_limit = PSY.get_rate(tap_transformer)

    transformer = PSY.get_component(Transformer2W, system, "Trans4")
    rate_limit2w = PSY.get_rate(tap_transformer)

    for model in [DCPPowerModel, StandardPTDFModel]
        template =
            get_template_dispatch_with_network(NetworkModel(model; PTDF=PSY.PTDF(system)))
        set_device_model!(template, DeviceModel(HVDCLine, HVDCUnbounded))
        set_device_model!(template, DeviceModel(TapTransformer, StaticBranchBounds))
        set_device_model!(template, DeviceModel(Transformer2W, StaticBranchBounds))
        model_m = DecisionModel(template, system; optimizer=ipopt_optimizer)
        @test build!(model_m; output_dir=mktempdir(cleanup=true)) == PSI.BuildStatus.BUILT

        if model == DCPPowerModel
            @test check_variable_unbounded(model_m, FlowActivePowerVariable, HVDCLine)
            @test check_variable_bounded(model_m, FlowActivePowerVariable, TapTransformer)
            @test check_variable_bounded(model_m, FlowActivePowerVariable, TapTransformer)
        end

        @test solve!(model_m) == RunStatus.SUCCESSFUL

        @test check_flow_variable_values(
            model_m,
            FlowActivePowerVariable,
            HVDCLine,
            "DCLine3",
            limits_min,
            limits_max,
        )
        @test check_flow_variable_values(
            model_m,
            FlowActivePowerVariable,
            TapTransformer,
            "Trans3",
            -rate_limit,
            rate_limit,
        )
        @test check_flow_variable_values(
            model_m,
            FlowActivePowerVariable,
            Transformer2W,
            "Trans4",
            -rate_limit2w,
            rate_limit2w,
        )
    end
end

@testset "AC Power Flow Models for HVDCLine Flow Constraints and TapTransformer & Transformer2W Unbounded" begin
    ratelimit_constraint_keys = [
        PSI.ConstraintKey(RateLimitConstraintFromTo, Transformer2W),
        PSI.ConstraintKey(RateLimitConstraintToFrom, Transformer2W),
        PSI.ConstraintKey(RateLimitConstraintFromTo, TapTransformer),
        PSI.ConstraintKey(RateLimitConstraintToFrom, TapTransformer),
        PSI.ConstraintKey(FlowRateConstraintFromTo, HVDCLine),
        PSI.ConstraintKey(FlowRateConstraintToFrom, HVDCLine),
        PSI.ConstraintKey(HVDCPowerBalance, HVDCLine),
    ]

    system = PSB.build_system(PSITestSystems, "c_sys14_dc")

    hvdc_line = PSY.get_component(HVDCLine, system, "DCLine3")
    limits_from = PSY.get_active_power_limits_from(hvdc_line)
    limits_to = PSY.get_active_power_limits_to(hvdc_line)
    limits_min = min(limits_from.min, limits_to.min)
    limits_max = min(limits_from.max, limits_to.max)

    tap_transformer = PSY.get_component(TapTransformer, system, "Trans3")
    rate_limit = PSY.get_rate(tap_transformer)

    transformer = PSY.get_component(Transformer2W, system, "Trans4")
    rate_limit2w = PSY.get_rate(tap_transformer)

    template = get_template_dispatch_with_network(ACPPowerModel)
    set_device_model!(template, DeviceModel(HVDCLine, HVDCDispatch))
    model_m = DecisionModel(template, system; optimizer=ipopt_optimizer)
    @test build!(model_m; output_dir=mktempdir(cleanup=true)) == PSI.BuildStatus.BUILT

    check_variable_bounded(model_m, FlowReactivePowerToFromVariable, HVDCLine)
    check_variable_bounded(model_m, FlowActivePowerVariable, HVDCLine)
    @test check_variable_bounded(model_m, FlowActivePowerFromToVariable, TapTransformer)
    @test check_variable_unbounded(model_m, FlowReactivePowerFromToVariable, TapTransformer)
    @test check_variable_bounded(model_m, FlowActivePowerToFromVariable, Transformer2W)
    @test check_variable_unbounded(model_m, FlowReactivePowerToFromVariable, Transformer2W)

    psi_constraint_test(model_m, ratelimit_constraint_keys)

    @test solve!(model_m) == RunStatus.SUCCESSFUL

    @test check_flow_variable_values(
        model_m,
        FlowActivePowerVariable,
        FlowReactivePowerToFromVariable,
        HVDCLine,
        "DCLine3",
        limits_max,
    )
    @test check_flow_variable_values(
        model_m,
        FlowActivePowerFromToVariable,
        FlowReactivePowerFromToVariable,
        TapTransformer,
        "Trans3",
        rate_limit,
    )
    @test check_flow_variable_values(
        model_m,
        FlowActivePowerToFromVariable,
        FlowReactivePowerToFromVariable,
        Transformer2W,
        "Trans4",
        rate_limit2w,
    )
end

@testset "DC Power Flow Models for HVDCLine Dispatch and TapTransformer & Transformer2W Unbounded" begin
    ratelimit_constraint_keys = [
        PSI.ConstraintKey(RateLimitConstraint, Transformer2W, "ub"),
        PSI.ConstraintKey(RateLimitConstraint, Line, "ub"),
        PSI.ConstraintKey(RateLimitConstraint, Line, "lb"),
        PSI.ConstraintKey(RateLimitConstraint, TapTransformer, "ub"),
        PSI.ConstraintKey(RateLimitConstraint, Transformer2W, "lb"),
        PSI.ConstraintKey(RateLimitConstraint, TapTransformer, "lb"),
        PSI.ConstraintKey(FlowRateConstraint, HVDCLine),
    ]

    system = PSB.build_system(PSITestSystems, "c_sys14_dc")

    hvdc_line = PSY.get_component(HVDCLine, system, "DCLine3")
    limits_from = PSY.get_active_power_limits_from(hvdc_line)
    limits_to = PSY.get_active_power_limits_to(hvdc_line)
    limits_min = min(limits_from.min, limits_to.min)
    limits_max = min(limits_from.max, limits_to.max)

    tap_transformer = PSY.get_component(TapTransformer, system, "Trans3")
    rate_limit = PSY.get_rate(tap_transformer)

    transformer = PSY.get_component(Transformer2W, system, "Trans4")
    rate_limit2w = PSY.get_rate(tap_transformer)

    template = get_template_dispatch_with_network(
        NetworkModel(StandardPTDFModel; PTDF=PSY.PTDF(system)),
    )
    set_device_model!(template, DeviceModel(HVDCLine, HVDCDispatch))
    model_m = DecisionModel(template, system; optimizer=ipopt_optimizer)
    @test build!(model_m; output_dir=mktempdir(cleanup=true)) == PSI.BuildStatus.BUILT

    @test check_variable_bounded(model_m, FlowActivePowerVariable, HVDCLine)
    @test !check_variable_bounded(model_m, FlowActivePowerVariable, TapTransformer)
    @test !check_variable_bounded(model_m, FlowActivePowerVariable, Transformer2W)
    @test check_variable_unbounded(model_m, FlowActivePowerVariable, Line)

    psi_constraint_test(model_m, ratelimit_constraint_keys)

    @test solve!(model_m) == RunStatus.SUCCESSFUL

    @test check_flow_variable_values(
        model_m,
        FlowActivePowerVariable,
        HVDCLine,
        "DCLine3",
        limits_max,
    )
    @test check_flow_variable_values(
        model_m,
        FlowActivePowerVariable,
        TapTransformer,
        "Trans3",
        rate_limit,
    )
    @test check_flow_variable_values(
        model_m,
        FlowActivePowerVariable,
        Transformer2W,
        "Trans4",
        rate_limit2w,
    )
end


# Model detector efficiency (Window +  Detector Crystal)

struct DetectorEfficiency
    name::String
    window::AbstractWindow
    surface::Vector{Film}
    active::Film
end

Base.show(io::IO, de::DetectorEfficiency) = print(io, "$(de.name)[$(de.window)]")

SDDEfficiency(
    window::AbstractWindow;
    thickness = 0.0370,
    deadlayer = 30.0e-7,
    entrance = Film(pure(n"Al"), 10.0e-7),
) = DetectorEfficiency(
    "SDD",
    window,
    [entrance, Film(pure(n"Si"), deadlayer)],
    Film(pure(n"Si"), thickness),
)

SiLiEfficiency(
    window::AbstractWindow;
    thickness = 0.250,
    deadlayer = 30.0e-7,
    entrance = Film(pure(n"Al"), 10.0e-7),
) = DetectorEfficiency(
    "Si(Li)",
    window,
    [entrance, Film(pure(n"Si"), deadlayer)],
    Film(pure(n"Si"), thickness),
)

efficiency(aa::DetectorEfficiency, energy::Float64, angle::Float64 = π / 2) =
    transmission(aa.window, energy, angle) *
    (1.0 - transmission(aa.active, energy, angle)) *
    mapreduce(lyr -> transmission(lyr, energy, angle), *, aa.surface)

"""
    detectorresponse(det::EDSDetector, eff::DetectorEfficiency, incidence::Float64=π/2)::AbstractMatrix

Build a matrix which models the detector response including aspects like the detector efficiency, the resolution, the
escape peaks.  All the warts that can be modeled within a linear model but not things like coincidence peaks that
are non-linear.  This function can (!should!) be specialized for more sophisticated detector models that include more
warts.

Example:

    genint = computegeneratedintensity(....) # Either characteristic or Bremsstrahlung...
    det = simpleEDS(4096, 5.0, 0.0, 132.0, 10)
    eff = SDDEfficiency(AP33Tabulation(); thickness=0.0370, deadlayer=30.0e-7, entrance=Film(pure(n"Al"), 10.0e-7))
    resp = detectorresponse(det, eff)
    # finally compute the measured signal
    measured = genint*resp
"""
function detectorresponse(
    det::EDSDetector,
    eff::DetectorEfficiency,
    incidence::Float64 = π / 2,
)::AbstractMatrix
    res = zeros(Float64, (channelcount(det), channelcount(det)))
    # An x-ray with energy in ch will be dispersed among a range of channels about ch
    for ch = lld(det):channelcount(det) # X-ray energy by channel
        el, eh = energy(ch, det), energy(ch + 1, det)  # X-ray energy
        effic, fwhm =
            0.5 * (efficiency(eff, eh, incidence) + efficiency(eff, el, incidence)),
            resolution(0.5 * (eh + el), det)
        roc =
            max(
                lld(det),
                channel(el - 3.0 * fwhm, det),
            ):min(channelcount(det), channel(el + 3.0 * fwhm, det))
        res[ch, roc] = map(ch2 -> effic * profile(ch2, 0.5 * (eh + el), det), roc)
    end
    return res
end


using DICOM
function read_dicom_dir(dicomdir, filename_base="image_")
    datap = Dict()
    filename_base = "image_"
    dicomdir="C:\\Users\\Jirik/data/medical/orig/3Dircadb1.1/MASKS_DICOM/portalvein/"
    files = readdir(dicomdir)
    files_filtered = [fn for fn in files if findfirst(".", fn) == nothing ]

    function get_int(fn)
        return parse(Int, fn[length(filename_base)+1:end])
    end
    files_sorted = sort(files_filtered, by=get_int)

    dcmData = dcm_parse(dicomdir * files_sorted[1])
    image = dcmData[tag"Pixel Data"]
    pxsp = dcmData[tag"Pixel Spacing"]
    slth = dcmData[tag"Slice Thickness"]
    datap["voxelsize_mm"] = [slth, pxsp[1], pxsp[2]]
    # slloc1 = dcmData[tag"Slice Location"]
    # if length(files_sorted) > 1
    #     dcmData2 = dcm_parse(dicomdir * files_sorted[2])
    #     # slloc2 = dcmData2[tag"Slice Location"]
    #     datap["voxelsize_mm"] = [abs(slloc2 - slloc1), pxsp[1], pxsp[2]]
    #
    # else
    #     datap["voxelsize_mm"] = [pxsp[1], pxsp[2]]
    # end

    newdata = Array{eltype(image), 3}(undef, length(files_sorted), size(image,1), size(image,2))
    for i=1:length(files_sorted)
        dcmData = dcm_parse(dicomdir * files_sorted[i])
        image = dcmData[tag"Pixel Data"]
        newdata[i, :, :] = image
    end
    datap["data3d"] = newdata
    return datap
end


using RHEOS
using SpecialFunctions
using Test

const tol = (eps(RHEOS.RheoFloat))^(0.125)

include("definitions.jl")
include("base.jl")
include("datagen.jl")
include("IO.jl")
include("processing.jl")

include(joinpath(@__DIR__, "models", "elements.jl"))
include(joinpath(@__DIR__, "models", "maxwell.jl"))
include(joinpath(@__DIR__, "models", "kelvinvoigt.jl"))
include(joinpath(@__DIR__, "models", "zener.jl"))
include(joinpath(@__DIR__, "models", "poynting-thomson.jl"))


function DateDetector(arg0::AbsoluteDate)
    return DateDetector((AbsoluteDate,), arg0)
end

function DateDetector(arg0::jdouble, arg1::jdouble, arg2::Vector{TimeStamped})
    return DateDetector((jdouble, jdouble, Vector{TimeStamped}), arg0, arg1, arg2)
end

function add_event_date(obj::DateDetector, arg0::AbsoluteDate)
    return jcall(obj, "addEventDate", void, (AbsoluteDate,), arg0)
end

function g(obj::DateDetector, arg0::SpacecraftState)
    return jcall(obj, "g", jdouble, (SpacecraftState,), arg0)
end

function get_date(obj::DateDetector)
    return jcall(obj, "getDate", AbsoluteDate, ())
end


"""
This is the module where the actual interaction with typescript happens.

It's mostly wrapped up in the Semagrams struct; see the documentation there.
"""
module UI

export Semagram, get_acset, serve_semagram, save, load, close!, export_html, export_pdf

using JSExpr
using WebIO
using Mux
import JSON
import DefaultApplication
using ..Schema
using ..Muesli
using ..Data
using Catlab.CSetDataStructures

"""
Basically the "control wires" for a semagram.

Contains:
- `divid`, the DOM ID of the div where the semagram should be anchored.
- `scope`, the WebIO scope in which the semagram lives
- `handle`, the WebIO handle in which data is passed *back* from the semagram.
   Note: there is currently no way of passing data *to* the semagram, but when
   we add this, it should be a parameter here.
- `ws`, the schema of the semagram. This gets passed to typescript, and also
   tells us how to interpret the data that we are getting back from typescript.
"""
struct Semagram{T <: ACSet}
  scope::Scope
  receiving::Observable{Dict{String,Any}}
  sending::Observable{Dict{String,Any}}
  exported::Observable{String}
  function Semagram{T}(ls::LocatedSemagramData) where {T <: ACSet}
    try
      T()
    catch e
      error("""
        error: all type parameters to the acset type must be fully instantiated.

        Right: Semagram{WeightedGraph{Int}}(WeightedGraphSema)
        Wrong: Semagram{WeightedGraph}(WeightedGraphSema)
      """)
    end
    deps = [
      "semagrams" => joinpath(@__DIR__, "..", "deps", "bundles", "app.bundle.js")
    ]
    scope = Scope(imports=deps, dom=dom"div"())
    ls_json = to_json(ls)
    receiving = Observable(scope, "receiving", ls_json)
    sending = Observable(scope, "sending", ls_json)
    exported = Observable(scope, "exported", "")
    onjs(sending, @js function (newls)
           this.state.resetWith(newls)
         end)
    on((newls) -> receiving[] = newls, sending)
    mountfn = @js function ()
      @var semagrams = System.registry.get(System.resolveSync("semagrams"))
      @var scopeobj = this
      setTimeout(() ->
        semagrams.main(
          $receiving[],
          scopeobj,
          (x) -> $receiving[] = x,
          (x) -> $exported[] = x), 20)
    end
    onmount(scope, mountfn)
    new{T}(scope, receiving, sending, exported)
  end
end

function Semagram{T}(s::SemagramSchema) where {T <: ACSet}
  ls = LocatedSemagramData(s)
  Semagram{T}(ls)
end

@WebIO.register_renderable(Semagram) do sg
    return WebIO.render(sg.scope)
end

function get_acset(sema::Semagram{T}) where {T}
  to_acset(save(sema), T)
end

function save(sema::Semagram)
  from_json(sema.receiving[], LocatedSemagramData)
end

function save(sema::Semagram, fn::String)
  open(fn, "w") do f
    write(f, JSON.json(sema.receiving[]))
  end
end

function load(sema::Semagram, fn::String)
  load(sema, load(fn))
end

function load(sema::Semagram, ls::LocatedSemagramData)
  sema.sending[] = to_json(ls)
end

function load(fn::String)
  v = Dict()
  open(fn, "r") do f
    v = JSON.parse(read(f, String))
  end
  from_json(v, LocatedSemagramData)
end

# NOTE: Not sure how to catch all the errors associated with WebSockets

function close!(sema_task::Task)
  try
    schedule(sema_task,ErrorException("stop"),error=true)
  catch e
    @warn "Semagram server closed"
  end
end

HTML_WRAPPING = """
<!doctype html>

<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
  @SVG@
</body>
</html>
"""

function export_html(sema::Semagram;
                     standalone::Union{Bool,Nothing}=nothing,
                     file::Union{String,Nothing}=nothing)
  # standalone defaults to true if we are writing to a file,
  # false otherwise
  standalone = standalone == nothing ? file != nothing : standalone
  body = sema.exported[]
  html = if standalone
    replace(HTML_WRAPPING, "@SVG@" => body)
  else
    body
  end
  if file == nothing
    HTML(html)
  else
    open(file, "w") do f
      write(f, html)
    end;
  end
end

function export_pdf(sema::Semagram, file::String)
  htmlfile = tempname() * ".html"
  export_html(sema; file=htmlfile)
  run(`wkhtmltopdf -s A6 -O Landscape $htmlfile $file`)
end

"""
This serves a semagram in a standalone page (as opposed to having it displayed
inline in a Jupyter notebook).

TODO: I currently don't know how to kill a task spawned by this...
"""
function serve_semagram(s::Semagram; port=8000)
  task = webio_serve(page("/", req -> node(:div, s)), port)
  sleep(1)
  DefaultApplication.open("http://localhost:$port")
  task
end

end # module UI


# Generates the Codata module automatically from NIST data available at
# http://physics.nist.gov/cuu/Constants/Table/allascii.txt
module CODATAgen

export GenCodataDefault,GenCodataSIUnits,autogen_codata_module
const units_match=r"(mol|kg|m|Pa|W|MeV|GeV|eV|J|u|ohm|C|F|S|N|T)(\^\-?[0-9])?"
const ignore_unit=r"E_h|C_90"

function parse_to_codata(name,value,units,unc)
    if name == "bohr_magneton_in_ev_per_t"
        "# FIXME $name\n"
    elseif name == "boltzmann_constant_in_ev_per_k"
        "# FIXME $name\n"
    elseif name == "natural_unit_of_action_in_ev_s"
        "# FIXME $name\n"
    elseif name == "nuclear_magneton_in_ev_per_t"
        "# FIXME $name\n"
    elseif name == "planck_constant_in_ev_s"
        "# FIXME $name\n"
    elseif name == "planck_constant_over_2_pi_in_ev_s"
        "# FIXME $name\n"
    else
        if contains(units, "E_h")
            "# FIXME $name ($units)\n"
        elseif contains(units, "sr")
            "# FIXME $name ($units)\n"
        elseif contains(units, "Wb")
            "# FIXME $name ($units)\n"
        elseif contains(units, "/c")
            "# FIXME $name ($units)\n"
        elseif units == "ohm"
            units = "Ohm"
            "const $name = PhysicalConstant(\"$name\",$value$units,$unc$units,\"$units\")\n"
        elseif contains(units, "C_90")
            "# FIXME $name ($units)\n"
        else
            "const $name = PhysicalConstant(\"$name\",$value$units,$unc$units,\"$units\")\n"
        end
    end
end
function getTmpName()
    let makeTmpName=x->ccall((:tmpnam,"libc"),Ptr{Cchar},(Ptr{Uint8},),x)
        tmpname=makeTmpName(0)
        bytestring(tmpname)
    end
end
function get_codata_and_gen_module(write_exports=true,
                                   gen_name=parse_to_codata,
                                   write_header=write_header_default,
                                   name="CODATA.jl")
    tmpName=getTmpName()
    download("http://physics.nist.gov/cuu/Constants/Table/allascii.txt",tmpName)
    autogen_codata_module(tmpName,write_exports,gen_name,write_header,name)
end
function autogen_codata_module(filename :: String,write_exports=true,
                               gen_name=parse_to_codata,
                               write_header=write_header_default,
                               name="Codata.jl")
    # open the input file file and get all the lines.
    ifile = open(filename)
    lines = readlines(ifile)
    close(ifile)

    # now open the target for writing.
    exports = IOBuffer()
    consts = IOBuffer()
    ofile = open(name,false, true, true, true, false)
    # write the header and footer on the file
    write_header(ofile)

    # first 10 lines are the header, who cares.
    data = lines[11:end]

    # now we just iterate over the dataset.  a few times, unfortunately.
    for l in data
        name = strip(l[1:58])
        value = strip(l[59:83])
        unc = strip(l[84:107])
        units = strip(l[108:end])

        # replace spaces in name with underscores, and a bunch of other stuff.
        name = replace(name, ' ', '_')
        name = replace(name, ',', '_')
        name = replace(name, '-', '_')
        name = replace(name, '/', "_per_")
        name = replace(name, '.', "")
        name = replace(name, "{220}_lattice_spacing_of_silicon",
                       "si_220_lattice_spacing")
        name = replace(name, '(', '_')
        name = replace(name, ')', "")
        name = lowercase(name)

        # replace space in values and uncertainties with nothing
        value, unc = replace(value, ' ', ""), replace(unc, ' ', "")
        unc = replace(unc, "(exact)", "0.0")
        value = replace(value,"...","")
        units = replace(replace(units, "  ", ' '), ' ', '*')
        if(gen_name(name,value,units,unc) != Nothing())
            write(consts, gen_name(name,value,units,unc))
        end
    end
    if(write_exports)
    write(ofile,takebuf_string(exports))
    end
    close(exports)
    write(ofile,takebuf_string(consts))
    close(consts)
    # write the footer
    write_footer(ofile)

    # all done
    close(ofile)
end

function write_header_default(hndl :: IOStream)
    write(hndl, "# Codata.jl\n")
    write(hndl, "# This file has been automatically generated.\n\n")
    write(hndl, "module CODATA\n")
    write(hndl, "using SIUnits\n\n")
    write(hndl, "using SIUnits.ShortUnits\n\n")
    write(hndl, "# The PhysicalConstant type encapsulates all of the\n")
    write(hndl, "# information about a particular constant - the value,\n")
    write(hndl, "# the uncertainty in that value, the units, and so on.\n")
    write(hndl, "type PhysicalConstant\n")
    write(hndl, "\tname :: String\n")
    write(hndl, "\tvalue\n")
    write(hndl, "\tuncertainty\n")
    write(hndl, "\tunits_string :: String\n")
    write(hndl, "end\n\n")
    write(hndl, "# autogenerated non-SI units\n")
    write(hndl, "const MeV = SIUnits.Mega*eV # Mega eV\n")
    write(hndl, "const GeV = SIUnits.Giga*eV # Giga eV\n")
    write(hndl, "const fm = SIUnits.Femto*m # femtometers\n")
    write(hndl, "export AtomicMassUnit\n")
    write(hndl, "const u = SIUnits.NonSIUnit{typeof(kg), :u}() # AMU\n")
    write(hndl, "convert(::Type{SIUnits.SIQuantity},::typeof(u))=9.1093826kg\n ")
    write(hndl, "const T = V*s*m^2\n # Tesla")
    write(hndl, "# begin constants\n\n")
end

function write_footer(hndl :: IOStream)
    write(hndl, "end # end of module definition\n")
end
function GenCodataDefault()
    autogen_codata_module("allascii.txt")
end
function GenCodataSIUnits()
    autogen_codata_module("allascii.txt",true,parse_to_SIUnits,write_header_SIUnits)
end
end


using Documenter, GeoPhyInv

#makedocs(modules=[GeoPhyInv],
#	         doctest=true)
#makedocs()

makedocs(
	 format = Documenter.HTML(
		  prettyurls = get(ENV, "CI", nothing) == "true"),
   sitename = "Toolbox for Geophysical Inversion",
   pages = ["Home" => "index.md",
	    "SeisForwExpt" => Any[
				  "Introduction" => "Fdtd/intro.md",
	#			  "Basic usage" => "Fdtd/reuse_expt.md",
				  "Generate snaps" => "Fdtd/create_snaps.md",
				  ],
	    "PoissonExpt" => Any[
				  "Introduction" => "Poisson/intro.md",
				 "Record data" => "Poisson/forw.md",
				 "Born map" => "Poisson/test_born.md",
				 ],
	    ]
#	    "Seismic Born Modeling" => "FWI/born_map.md",
#	    "Seismic Full Waveform Inversion" => "FWI/gradient_accuracy.md",
#    modules = []
)

deploydocs(
    repo   = "github.com/pawbz/GeoPhyInv.jl.git",
)
#=
deploydocs(
    deps   = Deps.pip("mkdocs", "python-markdown-math"),
    repo   = "github.com/pawbz/GeoPhyInv.jl.git",
    julia  = "1.0",
    osname = "linux",
    target = "build",
    make   = nothing
)
=#


SensitivityAlg(args...;kwargs...) = @error("The SensitivtyAlg choice mechanism was completely overhauled. Please consult the local sensitivity documentation for more information")

abstract type AbstractForwardSensitivityAlgorithm{CS,AD,FDT} <: DiffEqBase.AbstractSensitivityAlgorithm{CS,AD,FDT} end
abstract type AbstractAdjointSensitivityAlgorithm{CS,AD,FDT} <: DiffEqBase.AbstractSensitivityAlgorithm{CS,AD,FDT} end
abstract type AbstractSecondOrderSensitivityAlgorithm{CS,AD,FDT} <: DiffEqBase.AbstractSensitivityAlgorithm{CS,AD,FDT} end
abstract type AbstractShadowingSensitivityAlgorithm{CS,AD,FDT} <: DiffEqBase.AbstractSensitivityAlgorithm{CS,AD,FDT} end

"""
ForwardSensitivity{CS,AD,FDT} <: AbstractForwardSensitivityAlgorithm{CS,AD,FDT}

An implementation of continuous forward sensitivity analysis for propagating
derivatives by solving the extended ODE. When used within adjoint differentiation
(i.e. via Zygote), this will cause forward differentiation of the `solve` call
within the reverse-mode automatic differentiation environment.

## Constructor

```julia
function ForwardSensitivity(;
                            chunk_size=0,autodiff=true,
                            diff_type=Val{:central},
                            autojacvec=autodiff,
                            autojacmat=false)
```

## Keyword Arguments

* `autodiff`: Use automatic differentiation in the internal sensitivity algorithm
  computations. Default is `true`.
* `chunk_size`: Chunk size for forward mode differentiation if full Jacobians are
  built (`autojacvec=false` and `autodiff=true`). Default is `0` for automatic
  choice of chunk size.
* `autojacvec`: Calculate the Jacobian-vector product via automatic
  differentiation with special seeding.
* `diff_type`: The method used by FiniteDiff.jl for constructing the Jacobian
  if the full Jacobian is required with `autodiff=false`.

Further details:

- If `autodiff=true` and `autojacvec=true`, then the one chunk `J*v` forward-mode
  directional derivative calculation trick is used to compute the product without
  constructing the Jacobian (via ForwardDiff.jl).
- If `autodiff=false` and `autojacvec=true`, then the numerical direction derivative
  trick `(f(x+epsilon*v)-f(x))/epsilon` is used to compute `J*v` without constructing
  the Jacobian.
- If `autodiff=true` and `autojacvec=false`, then the Jacobian is constructed via
  chunked forward-mode automatic differentiation (via ForwardDiff.jl).
- If `autodiff=false` and `autojacvec=false`, then the Jacobian is constructed via
  finite differences via FiniteDiff.jl.

## SciMLProblem Support

This `sensealg` only supports `ODEProblem`s without callbacks (events).
"""
struct ForwardSensitivity{CS,AD,FDT} <: AbstractForwardSensitivityAlgorithm{CS,AD,FDT}
  autojacvec::Bool
  autojacmat::Bool
end
Base.@pure function ForwardSensitivity(;
                                       chunk_size=0,autodiff=true,
                                       diff_type=Val{:central},
                                       autojacvec=autodiff,
                                       autojacmat=false)
  autojacvec && autojacmat && error("Choose either Jacobian matrix products or Jacobian vector products,
                                      autojacmat and autojacvec cannot both be true")
  ForwardSensitivity{chunk_size,autodiff,diff_type}(autojacvec,autojacmat)
end

"""
ForwardDiffSensitivity{CS,CTS} <: AbstractForwardSensitivityAlgorithm{CS,Nothing,Nothing}

An implementation of discrete forward sensitivity analysis through ForwardDiff.jl.
When used within adjoint differentiation (i.e. via Zygote), this will cause forward
differentiation of the `solve` call within the reverse-mode automatic differentiation
environment.

## Constructor

```julia
ForwardDiffSensitivity(;chunk_size=0,convert_tspan=nothing)
```

## Keyword Arguments

* `chunk_size`: the chunk size used by ForwardDiff for computing the Jacobian, i.e. the
  number of simultaneous columns computed.
* `convert_tspan`: whether to convert time to also be `Dual` valued. By default this is
  `nothing` which will only convert if callbacks are found. Conversion is required in order
  to accurately differentiate callbacks (hybrid equations).

## SciMLProblem Support

This `sensealg` supports any `SciMLProblem`s, provided that the solver algorithms is
`SciMLBase.isautodifferentiable`. Note that `ForwardDiffSensitivity` can
accurately differentiate code with callbacks only when `convert_tspan=true`.
"""
struct ForwardDiffSensitivity{CS,CTS} <: AbstractForwardSensitivityAlgorithm{CS,Nothing,Nothing}
end
Base.@pure function ForwardDiffSensitivity(;chunk_size=0,convert_tspan=nothing)
  ForwardDiffSensitivity{chunk_size,convert_tspan}()
end

"""
BacksolveAdjoint{CS,AD,FDT,VJP,NOISE} <: AbstractAdjointSensitivityAlgorithm{CS,AD,FDT}

An implementation of adjoint sensitivity analysis using a backwards solution of the ODE.
By default this algorithm will use the values from the forward pass to perturb the
backwards solution to the correct spot, allowing reduced memory (O(1) memory). Checkpointing
stabilization is included for additional numerical stability over the naive implementation.

## Constructor

```julia
BacksolveAdjoint(;chunk_size=0,autodiff=true,
                  diff_type=Val{:central},
                  autojacvec=autodiff,
                  checkpointing=true, noise=true, noisemixing=false)
```

## Keyword Arguments

* `autodiff`: Use automatic differentiation for constructing the Jacobian
  if the Jacobian needs to be constructed.  Defaults to `true`.
* `chunk_size`: Chunk size for forward-mode differentiation if full Jacobians are
  built (`autojacvec=false` and `autodiff=true`). Default is `0` for automatic
  choice of chunk size.
* `diff_type`: The method used by FiniteDiff.jl for constructing the Jacobian
  if the full Jacobian is required with `autodiff=false`.
* `autojacvec`: Calculate the vector-Jacobian product (`J'*v`) via automatic
  differentiation with special seeding. The default is `true`. The total set
  of choices are:
    - `false`: the Jacobian is constructed via FiniteDiff.jl
    - `true`: the Jacobian is constructed via ForwardDiff.jl
    - `TrackerVJP`: Uses Tracker.jl for the vjp.
    - `ZygoteVJP`: Uses Zygote.jl for the vjp.
    - `EnzymeVJP`: Uses Enzyme.jl for the vjp.
    - `ReverseDiffVJP(compile=false)`: Uses ReverseDiff.jl for the vjp. `compile`
      is a boolean for whether to precompile the tape, which should only be done
      if there are no branches (`if` or `while` statements) in the `f` function.
* `checkpointing`: whether checkpointing is enabled for the reverse pass. Defaults
  to `true`.
* `noise`: Calculate the vector-Jacobian product (`J'*v`) of the diffusion term
  of an SDE via automatic differentiation with special seeding. The default is `true`.
  The total set of choices are:
    - `false`: the Jacobian is constructed via FiniteDiff.jl
    - `true`: the Jacobian is constructed via ForwardDiff.jl
    - `DiffEqSensitivity.ZygoteNoise()`: Uses Zygote.jl for the vjp.
    - `DiffEqSensitivity.ReverseDiffNoise(compile=false)`: Uses ReverseDiff.jl for
      the vjp. `compile` is a boolean for whether to precompile the tape, which
      should only be done if there are no branches (`if` or `while` statements) in
      the `f` function.
* `noisemixing`: Handle noise processes that are not of the form `du[i] = f(u[i])`.
  For example, to compute the sensitivities of an SDE with diagonal diffusion
  ```julia
  function g_mixing!(du,u,p,t)
    du[1] = p[3]*u[1] + p[4]*u[2]
    du[2] = p[3]*u[1] + p[4]*u[2]
    nothing
  end
  ```
  correctly, `noisemixing=true` must be enabled. The default is `false`.

For more details on the vjp choices, please consult the sensitivity algorithms
documentation page or the docstrings of the vjp types.

## Applicability of Backsolve and Caution

When `BacksolveAdjoint` is applicable, it is a fast method and requires the least memory.
However, one must be cautious because not all ODEs are stable under backwards integration
by the majority of ODE solvers. An example of such an equation is the Lorenz equation.
Notice that if one solves the Lorenz equation forward and then in reverse with any
adaptive time step and non-reversible integrator, then the backwards solution diverges
from the forward solution. As a quick demonstration:

```julia
using Sundials
function lorenz(du,u,p,t)
 du[1] = 10.0*(u[2]-u[1])
 du[2] = u[1]*(28.0-u[3]) - u[2]
 du[3] = u[1]*u[2] - (8/3)*u[3]
end
u0 = [1.0;0.0;0.0]
tspan = (0.0,100.0)
prob = ODEProblem(lorenz,u0,tspan)
sol = solve(prob,Tsit5(),reltol=1e-12,abstol=1e-12)
prob2 = ODEProblem(lorenz,sol[end],(100.0,0.0))
sol = solve(prob,Tsit5(),reltol=1e-12,abstol=1e-12)
@show sol[end]-u0 #[-3.22091, -1.49394, 21.3435]
```

Thus one should check the stability of the backsolve on their type of problem before
enabling this method. Additionally, using checkpointing with backsolve can be a
low memory way to stabilize it.

For more details on this topic, see
[Stiff Neural Ordinary Differential Equations](https://aip.scitation.org/doi/10.1063/5.0060697).

## Checkpointing

To improve the numerical stability of the reverse pass, `BacksolveAdjoint` includes a checkpointing
feature. If `sol.u` is a time series, then whenever a time `sol.t` is hit while reversing, a callback
will replace the reversing ODE portion with `sol.u[i]`. This nudges the solution back onto the appropriate
trajectory and reduces the numerical caused by drift.

## SciMLProblem Support

This `sensealg` only supports `ODEProblem`s, `SDEProblem`s, and `RODEProblem`s. This `sensealg` supports
callback functions (events).

## References

ODE:
 Rackauckas, C. and Ma, Y. and Martensen, J. and Warner, C. and Zubov, K. and Supekar,
 R. and Skinner, D. and Ramadhana, A. and Edelman, A., Universal Differential Equations
 for Scientific Machine Learning,	arXiv:2001.04385

 Hindmarsh, A. C. and Brown, P. N. and Grant, K. E. and Lee, S. L. and Serban, R.
 and Shumaker, D. E. and Woodward, C. S., SUNDIALS: Suite of nonlinear and
 differential/algebraic equation solvers, ACM Transactions on Mathematical
 Software (TOMS), 31, pp:363–396 (2005)

 Chen, R.T.Q. and Rubanova, Y. and Bettencourt, J. and Duvenaud, D. K.,
 Neural ordinary differential equations. In Advances in neural information processing
 systems, pp. 6571–6583 (2018)

 Pontryagin, L. S. and Mishchenko, E.F. and Boltyanskii, V.G. and Gamkrelidze, R.V.
 The mathematical theory of optimal processes. Routledge, (1962)

 Rackauckas, C. and Ma, Y. and Dixit, V. and Guo, X. and Innes, M. and Revels, J.
 and Nyberg, J. and Ivaturi, V., A comparison of automatic differentiation and
 continuous sensitivity analysis for derivatives of differential equation solutions,
 arXiv:1812.01892

DAE:
 Cao, Y. and Li, S. and Petzold, L. and Serban, R., Adjoint sensitivity analysis
 for differential-algebraic equations: The adjoint DAE system and its numerical
 solution, SIAM journal on scientific computing 24 pp: 1076-1089 (2003)

SDE:
 Gobet, E. and Munos, R., Sensitivity Analysis Using Ito-Malliavin Calculus and
 Martingales, and Application to Stochastic Optimal Control,
 SIAM Journal on control and optimization, 43, pp. 1676-1713 (2005)

 Li, X. and Wong, T.-K. L.and Chen, R. T. Q. and Duvenaud, D.,
 Scalable Gradients for Stochastic Differential Equations,
 PMLR 108, pp. 3870-3882 (2020), http://proceedings.mlr.press/v108/li20i.html
"""
struct BacksolveAdjoint{CS,AD,FDT,VJP,NOISE} <: AbstractAdjointSensitivityAlgorithm{CS,AD,FDT}
  autojacvec::VJP
  checkpointing::Bool
  noise::NOISE
  noisemixing::Bool
end
Base.@pure function BacksolveAdjoint(;chunk_size=0,autodiff=true,
                                      diff_type=Val{:central},
                                      autojacvec=autodiff,
                                      checkpointing=true, noise=true,noisemixing=false)
  BacksolveAdjoint{chunk_size,autodiff,diff_type,typeof(autojacvec),typeof(noise)}(autojacvec,checkpointing,noise,noisemixing)
end

"""
InterpolatingAdjoint{CS,AD,FDT,VJP,NOISE} <: AbstractAdjointSensitivityAlgorithm{CS,AD,FDT}

An implementation of adjoint sensitivity analysis which uses the interpolation of
the forward solution for the reverse solve vector-Jacobian products. By
default it requires a dense solution of the forward pass and will internally
ignore saving arguments during the gradient calculation. When checkpointing is
enabled it will only require the memory to interpolate between checkpoints.

## Constructor

```julia
function InterpolatingAdjoint(;chunk_size=0,autodiff=true,
                               diff_type=Val{:central},
                               autojacvec=autodiff,
                               checkpointing=false, noise=true, noisemixing=false)
```

## Keyword Arguments

* `autodiff`: Use automatic differentiation for constructing the Jacobian
  if the Jacobian needs to be constructed.  Defaults to `true`.
* `chunk_size`: Chunk size for forward-mode differentiation if full Jacobians are
  built (`autojacvec=false` and `autodiff=true`). Default is `0` for automatic
  choice of chunk size.
* `diff_type`: The method used by FiniteDiff.jl for constructing the Jacobian
  if the full Jacobian is required with `autodiff=false`.
* `autojacvec`: Calculate the vector-Jacobian product (`J'*v`) via automatic
  differentiation with special seeding. The default is `true`. The total set
  of choices are:
    - `false`: the Jacobian is constructed via FiniteDiff.jl
    - `true`: the Jacobian is constructed via ForwardDiff.jl
    - `TrackerVJP`: Uses Tracker.jl for the vjp.
    - `ZygoteVJP`: Uses Zygote.jl for the vjp.
    - `EnzymeVJP`: Uses Enzyme.jl for the vjp.
    - `ReverseDiffVJP(compile=false)`: Uses ReverseDiff.jl for the vjp. `compile`
      is a boolean for whether to precompile the tape, which should only be done
      if there are no branches (`if` or `while` statements) in the `f` function.
* `checkpointing`: whether checkpointing is enabled for the reverse pass. Defaults
  to `true`.
* `noise`: Calculate the vector-Jacobian product (`J'*v`) of the diffusion term
  of an SDE via automatic differentiation with special seeding. The default is `true`.
  The total set of choices are:
    - `false`: the Jacobian is constructed via FiniteDiff.jl
    - `true`: the Jacobian is constructed via ForwardDiff.jl
    - `DiffEqSensitivity.ZygoteNoise()`: Uses Zygote.jl for the vjp.
    - `DiffEqSensitivity.ReverseDiffNoise(compile=false)`: Uses ReverseDiff.jl for
      the vjp. `compile` is a boolean for whether to precompile the tape, which
      should only be done if there are no branches (`if` or `while` statements) in
      the `f` function.
* `noisemixing`: Handle noise processes that are not of the form `du[i] = f(u[i])`.
  For example, to compute the sensitivities of an SDE with diagonal diffusion
  ```julia
  function g_mixing!(du,u,p,t)
    du[1] = p[3]*u[1] + p[4]*u[2]
    du[2] = p[3]*u[1] + p[4]*u[2]
    nothing
  end
  ```
  correctly, `noisemixing=true` must be enabled. The default is `false`.

For more details on the vjp choices, please consult the sensitivity algorithms
documentation page or the docstrings of the vjp types.

## Checkpointing

To reduce the memory usage of the reverse pass, `InterpolatingAdjoint` includes a checkpointing
feature. If `sol` is `dense`, checkpointing is ignored and the continuous solution is used for
calculating `u(t)` at arbitrary time points. If `checkpointing=true` and `sol` is not `dense`,
then dense intervals between `sol.t[i]` and `sol.t[i+1]` are reconstructed on-demand for calculating
`u(t)` at arbitrary time points. This reduces the total memory requirement to only the cost of
holding the dense solution over the largest time interval (in terms of number of required steps).
The total compute cost is no more than double the original forward compute cost.

## SciMLProblem Support

This `sensealg` only supports `ODEProblem`s, `SDEProblem`s, and `RODEProblem`s. This `sensealg`
supports callbacks (events).

## References

 Rackauckas, C. and Ma, Y. and Martensen, J. and Warner, C. and Zubov, K. and Supekar,
 R. and Skinner, D. and Ramadhana, A. and Edelman, A., Universal Differential Equations
 for Scientific Machine Learning,	arXiv:2001.04385

 Hindmarsh, A. C. and Brown, P. N. and Grant, K. E. and Lee, S. L. and Serban, R.
 and Shumaker, D. E. and Woodward, C. S., SUNDIALS: Suite of nonlinear and
 differential/algebraic equation solvers, ACM Transactions on Mathematical
 Software (TOMS), 31, pp:363–396 (2005)

 Rackauckas, C. and Ma, Y. and Dixit, V. and Guo, X. and Innes, M. and Revels, J.
 and Nyberg, J. and Ivaturi, V., A comparison of automatic differentiation and
 continuous sensitivity analysis for derivatives of differential equation solutions,
 arXiv:1812.01892
"""
struct InterpolatingAdjoint{CS,AD,FDT,VJP,NOISE} <: AbstractAdjointSensitivityAlgorithm{CS,AD,FDT}
  autojacvec::VJP
  checkpointing::Bool
  noise::NOISE
  noisemixing::Bool
end
Base.@pure function InterpolatingAdjoint(;chunk_size=0,autodiff=true,
                                         diff_type=Val{:central},
                                         autojacvec=autodiff,
                                         checkpointing=false, noise=true,noisemixing=false)
  InterpolatingAdjoint{chunk_size,autodiff,diff_type,typeof(autojacvec),typeof(noise)}(autojacvec,checkpointing,noise,noisemixing)
end

"""
QuadratureAdjoint{CS,AD,FDT,VJP} <: AbstractAdjointSensitivityAlgorithm{CS,AD,FDT}

An implementation of adjoint sensitivity analysis which develops a full
continuous solution of the reverse solve in order to perform a post-ODE
quadrature. This method requires the the dense solution and will ignore
saving arguments during the gradient calculation. The tolerances in the
constructor control the inner quadrature. The inner quadrature uses a
ReverseDiff vjp if autojacvec, and `compile=false` by default but can
compile the tape under the same circumstances as `ReverseDiffVJP`.

This method is O(n^3 + p) for stiff / implicit equations (as opposed to the
O((n+p)^3) scaling of BacksolveAdjoint and InterpolatingAdjoint), and thus
is much more compute efficient. However, it requires holding a dense reverse
pass and is thus memory intensive.

## Constructor

```julia
function QuadratureAdjoint(;chunk_size=0,autodiff=true,
                            diff_type=Val{:central},
                            autojacvec=autodiff,abstol=1e-6,
                            reltol=1e-3,compile=false)
```

## Keyword Arguments

* `autodiff`: Use automatic differentiation for constructing the Jacobian
  if the Jacobian needs to be constructed.  Defaults to `true`.
* `chunk_size`: Chunk size for forward-mode differentiation if full Jacobians are
  built (`autojacvec=false` and `autodiff=true`). Default is `0` for automatic
  choice of chunk size.
* `diff_type`: The method used by FiniteDiff.jl for constructing the Jacobian
  if the full Jacobian is required with `autodiff=false`.
* `autojacvec`: Calculate the vector-Jacobian product (`J'*v`) via automatic
  differentiation with special seeding. The default is `true`. The total set
  of choices are:
    - `false`: the Jacobian is constructed via FiniteDiff.jl
    - `true`: the Jacobian is constructed via ForwardDiff.jl
    - `TrackerVJP`: Uses Tracker.jl for the vjp.
    - `ZygoteVJP`: Uses Zygote.jl for the vjp.
    - `EnzymeVJP`: Uses Enzyme.jl for the vjp.
    - `ReverseDiffVJP(compile=false)`: Uses ReverseDiff.jl for the vjp. `compile`
      is a boolean for whether to precompile the tape, which should only be done
      if there are no branches (`if` or `while` statements) in the `f` function.
* `abstol`: absolute tolerance for the quadrature calculation
* `reltol`: relative tolerance for the quadrature calculation
* `compile`: whether to compile the vjp calculation for the integrand calculation.
  See `ReverseDiffVJP` for more details.

For more details on the vjp choices, please consult the sensitivity algorithms
documentation page or the docstrings of the vjp types.

## SciMLProblem Support

This `sensealg` only supports `ODEProblem`s. This `sensealg` supports events (callbacks).

## References

 Rackauckas, C. and Ma, Y. and Martensen, J. and Warner, C. and Zubov, K. and Supekar,
 R. and Skinner, D. and Ramadhana, A. and Edelman, A., Universal Differential Equations
 for Scientific Machine Learning,	arXiv:2001.04385

 Hindmarsh, A. C. and Brown, P. N. and Grant, K. E. and Lee, S. L. and Serban, R.
 and Shumaker, D. E. and Woodward, C. S., SUNDIALS: Suite of nonlinear and
 differential/algebraic equation solvers, ACM Transactions on Mathematical
 Software (TOMS), 31, pp:363–396 (2005)

 Rackauckas, C. and Ma, Y. and Dixit, V. and Guo, X. and Innes, M. and Revels, J.
 and Nyberg, J. and Ivaturi, V., A comparison of automatic differentiation and
 continuous sensitivity analysis for derivatives of differential equation solutions,
 arXiv:1812.01892

 Kim, S., Ji, W., Deng, S., Ma, Y., & Rackauckas, C. (2021). Stiff neural ordinary
 differential equations. Chaos: An Interdisciplinary Journal of Nonlinear Science, 31(9), 093122.
"""
struct QuadratureAdjoint{CS,AD,FDT,VJP} <: AbstractAdjointSensitivityAlgorithm{CS,AD,FDT}
  autojacvec::VJP
  abstol::Float64
  reltol::Float64
  compile::Bool
end
Base.@pure function QuadratureAdjoint(;chunk_size=0,autodiff=true,
                                         diff_type=Val{:central},
                                         autojacvec=autodiff,abstol=1e-6,
                                         reltol=1e-3,compile=false)
  QuadratureAdjoint{chunk_size,autodiff,diff_type,typeof(autojacvec)}(autojacvec,abstol,reltol,compile)
end

"""
TrackerAdjoint <: AbstractAdjointSensitivityAlgorithm{nothing,true,nothing}

An implementation of discrete adjoint sensitivity analysis
using the Tracker.jl tracing-based AD. Supports in-place functions through
an Array of Structs formulation, and supports out of place through struct of
arrays.

## Constructor

```julia
TrackerAdjoint()
```

## SciMLProblem Support

This `sensealg` supports any `DEProblem` if the algorithm is `SciMLBase.isautodifferentiable`
Compatible with a limited subset of `AbstractArray` types for `u0`, including `CuArrays`.

!!! warn

    TrackerAdjoint is incompatible with Stiff ODE solvers using forward-mode automatic
    differentiation for the Jacobians. Thus for example, `TRBDF2()` will error. Instead,
    use `autodiff=false`, i.e. `TRBDF2(autodiff=false)`. This will only remove the
    forward-mode automatic differentiation of the Jacobian construction, not the reverse-mode
    AD usage, and thus performance will still be nearly the same, though Jacobian accuracy
    may suffer which could cause more steps to be required.
"""
struct TrackerAdjoint <: AbstractAdjointSensitivityAlgorithm{nothing,true,nothing} end

"""
ReverseDiffAdjoint <: AbstractAdjointSensitivityAlgorithm{nothing,true,nothing}

An implementation of discrete adjoint sensitivity analysis using the ReverseDiff.jl
tracing-based AD. Supports in-place functions through an Array of Structs formulation,
and supports out of place through struct of arrays.

## Constructor

```julia
ReverseDiffAdjoint()
```

## SciMLProblem Support

This `sensealg` supports any `DEProblem` if the algorithm is `SciMLBase.isautodifferentiable`.
Requires that the state variables are CPU-based `Array` types.
"""
struct ReverseDiffAdjoint <: AbstractAdjointSensitivityAlgorithm{nothing,true,nothing} end

"""
ZygoteAdjoint <: AbstractAdjointSensitivityAlgorithm{nothing,true,nothing}

An implementation of discrete adjoint sensitivity analysis
using the Zygote.jl source-to-source AD directly on the differential equation
solver.

## Constructor

```julia
ZygoteAdjoint()
```

## SciMLProblem Support

Currently fails on almost every solver.
"""
struct ZygoteAdjoint <: AbstractAdjointSensitivityAlgorithm{nothing,true,nothing} end

"""
ForwardLSS{CS,AD,FDT,RType,gType} <: AbstractShadowingSensitivityAlgorithm{CS,AD,FDT}

An implementation of the discrete, forward-mode
[least squares shadowing](https://arxiv.org/abs/1204.0159) (LSS) method. LSS replaces
the ill-conditioned initial value probem (`ODEProblem`) for chaotic systems by a
well-conditioned least-squares problem. This allows for computing sensitivities of
long-time averaged quantities with respect to the parameters of the `ODEProblem`. The
computational cost of LSS scales as (number of states x number of time steps). Converges
to the correct sensitivity at a rate of `T^(-1/2)`, where `T` is the time of the trajectory.
See `NILSS()` and `NILSAS()` for a more efficient non-intrusive formulation.

## Constructor

```julia
ForwardLSS(;
          chunk_size=0,autodiff=true,
          diff_type=Val{:central},
          LSSregularizer=TimeDilation(10.0,0.0,0.0),
          g=nothing)
```

## Keyword Arguments

* `autodiff`: Use automatic differentiation for constructing the Jacobian
  if the Jacobian needs to be constructed.  Defaults to `true`.
* `chunk_size`: Chunk size for forward-mode differentiation if full Jacobians are
  built (`autojacvec=false` and `autodiff=true`). Default is `0` for automatic
  choice of chunk size.
* `diff_type`: The method used by FiniteDiff.jl for constructing the Jacobian
  if the full Jacobian is required with `autodiff=false`.
* `LSSregularizer`: Using `LSSregularizer`, one can choose between three different
  regularization routines. The default choice is `TimeDilation(10.0,0.0,0.0)`.
    - `CosWindowing()`: cos windowing of the time grid, i.e. the time grid (saved
      time steps) is transformed using a cosine.
    - `Cos2Windowing()`: cos^2 windowing of the time grid.
    - `TimeDilation(alpha::Number,t0skip::Number,t1skip::Number)`: Corresponds to
      a time dilation. `alpha` controls the weight. `t0skip` and `t1skip` indicate
      the times truncated at the beginnning and end of the trajectory, respectively.
* `g`: instantaneous objective function of the long-time averaged objective.

## SciMLProblem Support

This `sensealg` only supports `ODEProblem`s. This `sensealg` does not support
events (callbacks). This `sensealg` assumes that the objective is a long-time averaged
quantity and ergodic, i.e. the time evolution of the system behaves qualitatively the
same over infinite time independent of the specified initial conditions, such that only
the sensitivity with respect to the parameters is of interest.

## References

Wang, Q., Hu, R., and Blonigan, P. Least squares shadowing sensitivity analysis of
chaotic limit cycle oscillations. Journal of Computational Physics, 267, 210-224 (2014).

Wang, Q., Convergence of the Least Squares Shadowing Method for Computing Derivative of Ergodic
Averages, SIAM Journal on Numerical Analysis, 52, 156–170 (2014).

Blonigan, P., Gomez, S., Wang, Q., Least Squares Shadowing for sensitivity analysis of turbulent
fluid flows, in: 52nd Aerospace Sciences Meeting, 1–24 (2014).
"""
struct ForwardLSS{CS,AD,FDT,RType,gType} <: AbstractShadowingSensitivityAlgorithm{CS,AD,FDT}
  LSSregularizer::RType
  g::gType
end
Base.@pure function ForwardLSS(;
  chunk_size=0, autodiff=true,
  diff_type=Val{:central},
  LSSregularizer=TimeDilation(10.0,0.0,0.0),
  g=nothing)

  ForwardLSS{chunk_size,autodiff,diff_type,typeof(LSSregularizer),typeof(g)}(LSSregularizer, g)
end

"""
AdjointLSS{CS,AD,FDT,RType,gType} <: AbstractShadowingSensitivityAlgorithm{CS,AD,FDT}

An implementation of the discrete, adjoint-mode
[least square shadowing](https://arxiv.org/abs/1204.0159) method. LSS replaces
the ill-conditioned initial value probem (`ODEProblem`) for chaotic systems by a
well-conditioned least-squares problem. This allows for computing sensitivities of
long-time averaged quantities with respect to the parameters of the `ODEProblem`. The
computational cost of LSS scales as (number of states x number of time steps). Converges
to the correct sensitivity at a rate of `T^(-1/2)`, where `T` is the time of the trajectory.
See `NILSS()` and `NILSAS()` for a more efficient non-intrusive formulation.

## Constructor

```julia
AdjointLSS(;
          chunk_size=0,autodiff=true,
          diff_type=Val{:central},
          LSSRegularizer=CosWindowing(),
          g=nothing)
```

## Keyword Arguments

* `autodiff`: Use automatic differentiation for constructing the Jacobian
  if the Jacobian needs to be constructed.  Defaults to `true`.
* `chunk_size`: Chunk size for forward-mode differentiation if full Jacobians are
  built (`autojacvec=false` and `autodiff=true`). Default is `0` for automatic
  choice of chunk size.
* `diff_type`: The method used by FiniteDiff.jl for constructing the Jacobian
  if the full Jacobian is required with `autodiff=false`.
* `LSSregularizer`: Using `LSSregularizer`, one can choose between different
  regularization routines. The default choice is `TimeDilation(10.0,0.0,0.0)`.
    - `TimeDilation(alpha::Number,t0skip::Number,t1skip::Number)`: Corresponds to
      a time dilation. `alpha` controls the weight. `t0skip` and `t1skip` indicate
      the times truncated at the beginnning and end of the trajectory, respectively.
      The default value for `t0skip` and `t1skip` is `zero(alpha)`.
* `g`: instantaneous objective function of the long-time averaged objective.

## SciMLProblem Support

This `sensealg` only supports `ODEProblem`s. This `sensealg` does not support
events (callbacks). This `sensealg` assumes that the objective is a long-time averaged
quantity and ergodic, i.e. the time evolution of the system behaves qualitatively the
same over infinite time independent of the specified initial conditions, such that only
the sensitivity with respect to the parameters is of interest.

## References

Wang, Q., Hu, R., and Blonigan, P. Least squares shadowing sensitivity analysis of
chaotic limit cycle oscillations. Journal of Computational Physics, 267, 210-224 (2014).
"""
struct AdjointLSS{CS,AD,FDT,RType,gType} <: AbstractShadowingSensitivityAlgorithm{CS,AD,FDT}
  LSSregularizer::RType
  g::gType
end
Base.@pure function AdjointLSS(;
  chunk_size=0, autodiff=true,
  diff_type=Val{:central},
  LSSregularizer=TimeDilation(10.0, 0.0, 0.0),
  g=nothing)
  AdjointLSS{chunk_size,autodiff,diff_type,typeof(LSSregularizer),typeof(g)}(LSSregularizer, g)
end

abstract type AbstractLSSregularizer end
abstract type AbstractCosWindowing <: AbstractLSSregularizer end
struct CosWindowing <: AbstractCosWindowing end
struct Cos2Windowing <: AbstractCosWindowing end

"""
TimeDilation{T1<:Number} <: AbstractLSSregularizer

A regularization method for `LSS`. See `?LSS` for
additional information and other methods.

## Constructor

```julia
TimeDilation(alpha;
          t0skip=zero(alpha),
          t1skip=zero(alpha))
```
"""
struct TimeDilation{T1<:Number} <: AbstractLSSregularizer
  alpha::T1 # alpha: weight of the time dilation term in LSS.
  t0skip::T1
  t1skip::T1
end
function TimeDilation(alpha,t0skip=zero(alpha),t1skip=zero(alpha))
  TimeDilation{typeof(alpha)}(alpha,t0skip,t1skip)
end
"""
struct NILSS{CS,AD,FDT,RNG,nType,gType} <: AbstractShadowingSensitivityAlgorithm{CS,AD,FDT}

An implementation of the forward-mode, continuous
[non-intrusive least squares shadowing](https://arxiv.org/abs/1611.00880) method. `NILSS`
allows for computing sensitivities of long-time averaged quantities with respect to the
parameters of an `ODEProblem` by constraining the computation to the unstable subspace.
`NILSS` employs the continuous-time `ForwardSensitivity` method as tangent solver. To
avoid an exponential blow-up of the (homogenous and inhomogenous) tangent solutions,
the trajectory should be divided into sufficiently small segments, where the tangent solutions
are rescaled on the interfaces. The computational and memory cost of NILSS scale with
the number of unstable (positive) Lyapunov exponents (instead of the number of states as
in the LSS method). `NILSS` avoids the explicit construction of the Jacobian at each time
step and thus should generally be preferred (for large system sizes) over `ForwardLSS`.

## Constructor

```julia
NILSS(nseg, nstep; nus = nothing,
                   rng = Xorshifts.Xoroshiro128Plus(rand(UInt64)),
                   chunk_size=0,autodiff=true,
                   diff_type=Val{:central},
                   autojacvec=autodiff,
                   g=nothing)
```

## Arguments

* `nseg`: Number of segments on full time interval on the attractor.
* `nstep`: number of steps on each segment.

## Keyword Arguments

* `nus`: Dimension of the unstable subspace. Default is `nothing`. `nus` must be
  smaller or equal to the state dimension (`length(u0)`). With the default choice,
  `nus = length(u0) - 1` will be set at compile time.
* `rng`: (Pseudo) random number generator. Used for initializing the homogenous
  tangent states (`w`). Default is `Xorshifts.Xoroshiro128Plus(rand(UInt64))`.
* `autodiff`: Use automatic differentiation in the internal sensitivity algorithm
  computations. Default is `true`.
* `chunk_size`: Chunk size for forward mode differentiation if full Jacobians are
  built (`autojacvec=false` and `autodiff=true`). Default is `0` for automatic
  choice of chunk size.
* `autojacvec`: Calculate the Jacobian-vector product via automatic
  differentiation with special seeding.
* `diff_type`: The method used by FiniteDiff.jl for constructing the Jacobian
  if the full Jacobian is required with `autodiff=false`.
* `g`: instantaneous objective function of the long-time averaged objective.

## SciMLProblem Support

This `sensealg` only supports `ODEProblem`s. This `sensealg` does not support
events (callbacks). This `sensealg` assumes that the objective is a long-time averaged
quantity and ergodic, i.e. the time evolution of the system behaves qualitatively the
same over infinite time independent of the specified initial conditions, such that only
the sensitivity with respect to the parameters is of interest.

## References
Ni, A., Blonigan, P. J., Chater, M., Wang, Q., Zhang, Z., Sensitivity analy-
sis on chaotic dynamical system by Non-Intrusive Least Square Shadowing
(NI-LSS), in: 46th AIAA Fluid Dynamics Conference, AIAA AVIATION Forum (AIAA 2016-4399),
American Institute of Aeronautics and Astronautics, 1–16 (2016).

Ni, A., and Wang, Q. Sensitivity analysis on chaotic dynamical systems by Non-Intrusive
Least Squares Shadowing (NILSS). Journal of Computational Physics 347, 56-77 (2017).
"""
struct NILSS{CS,AD,FDT,RNG,nType,gType} <: AbstractShadowingSensitivityAlgorithm{CS,AD,FDT}
  rng::RNG
  nseg::Int
  nstep::Int
  nus::nType
  autojacvec::Bool
  g::gType
end
Base.@pure function NILSS(nseg, nstep; nus=nothing, rng=Xorshifts.Xoroshiro128Plus(rand(UInt64)),
  chunk_size=0, autodiff=true,
  diff_type=Val{:central},
  autojacvec=autodiff,
  g=nothing
)
  NILSS{chunk_size,autodiff,diff_type,typeof(rng),typeof(nus),typeof(g)}(rng, nseg, nstep, nus, autojacvec, g)
end

"""
NILSAS{CS,AD,FDT,RNG,SENSE,gType} <: AbstractShadowingSensitivityAlgorithm{CS,AD,FDT}

An implementation of the adjoint-mode, continuous
[non-intrusive adjoint least squares shadowing](https://arxiv.org/abs/1801.08674) method.
`NILSAS` allows for computing sensitivities of long-time averaged quantities with respect
to the parameters of an `ODEProblem` by constraining the computation to the unstable subspace.
`NILSAS` employs SciMLSensitivity.jl's continuous adjoint sensitivity methods on each segment
to compute (homogenous and inhomogenous) adjoint solutions. To avoid an exponential blow-up
of the adjoint solutions, the trajectory should be divided into sufficiently small segments,
where the adjoint solutions are rescaled on the interfaces. The computational and memory cost
of NILSAS scale with the number of unstable, adjoint Lyapunov exponents (instead of the number
of states as in the LSS method). `NILSAS` avoids the explicit construction of the Jacobian at
each time step and thus should generally be preferred (for large system sizes) over `AdjointLSS`.
`NILSAS` is favourable over `NILSS` for many parameters because NILSAS computes the gradient
with respect to multiple parameters with negligible additional cost.

## Constructor

```julia
NILSAS(nseg, nstep, M=nothing; rng = Xorshifts.Xoroshiro128Plus(rand(UInt64)),
                                adjoint_sensealg = BacksolveAdjoint(),
                                chunk_size=0,autodiff=true,
                                diff_type=Val{:central},
                                autojacvec=autodiff,
                                g=nothing
                                )
```

## Arguments

* `nseg`: Number of segments on full time interval on the attractor.
* `nstep`: number of steps on each segment.
* `M`: number of homogenous adjoint solutions. This number must be bigger or equal
  than the number of (positive, adjoint) Lyapunov exponents. Default is `nothing`.

## Keyword Arguments

* `rng`: (Pseudo) random number generator. Used for initializing the terminate
  conditions of the homogenous adjoint states (`w`). Default is `Xorshifts.Xoroshiro128Plus(rand(UInt64))`.
* `adjoint_sensealg`: Continuous adjoint sensitivity method to compute homogenous
  and inhomogenous adjoint solutions on each segment. Default is `BacksolveAdjoint()`.
* `autodiff`: Use automatic differentiation for constructing the Jacobian
  if the Jacobian needs to be constructed.  Defaults to `true`.
* `chunk_size`: Chunk size for forward-mode differentiation if full Jacobians are
  built (`autojacvec=false` and `autodiff=true`). Default is `0` for automatic
  choice of chunk size.
* `diff_type`: The method used by FiniteDiff.jl for constructing the Jacobian
  if the full Jacobian is required with `autodiff=false`.
* `autojacvec`: Calculate the vector-Jacobian product (`J'*v`) via automatic
  differentiation with special seeding. The default is `true`. The total set
  of choices are:
    - `false`: the Jacobian is constructed via FiniteDiff.jl
    - `true`: the Jacobian is constructed via ForwardDiff.jl
    - `TrackerVJP`: Uses Tracker.jl for the vjp.
    - `ZygoteVJP`: Uses Zygote.jl for the vjp.
    - `EnzymeVJP`: Uses Enzyme.jl for the vjp.
    - `ReverseDiffVJP(compile=false)`: Uses ReverseDiff.jl for the vjp. `compile`
      is a boolean for whether to precompile the tape, which should only be done
      if there are no branches (`if` or `while` statements) in the `f` function.
* `g`: instantaneous objective function of the long-time averaged objective.

## SciMLProblem Support

This `sensealg` only supports `ODEProblem`s. This `sensealg` does not support
events (callbacks). This `sensealg` assumes that the objective is a long-time averaged
quantity and ergodic, i.e. the time evolution of the system behaves qualitatively the
same over infinite time independent of the specified initial conditions, such that only
the sensitivity with respect to the parameters is of interest.

## References

Ni, A., and Talnikar, C., Adjoint sensitivity analysis on chaotic dynamical systems
by Non-Intrusive Least Squares Adjoint Shadowing (NILSAS). Journal of Computational
Physics 395, 690-709 (2019).
"""
struct NILSAS{CS,AD,FDT,RNG,SENSE,gType} <: AbstractShadowingSensitivityAlgorithm{CS,AD,FDT}
  rng::RNG
  adjoint_sensealg::SENSE
  M::Int
  nseg::Int
  nstep::Int
  autojacvec::Bool
  g::gType
end
Base.@pure function NILSAS(nseg, nstep, M=nothing; rng=Xorshifts.Xoroshiro128Plus(rand(UInt64)),
  adjoint_sensealg=BacksolveAdjoint(),
  chunk_size=0, autodiff=true,
  diff_type=Val{:central},
  autojacvec=autodiff,
  g=nothing
)
  # integer dimension of the unstable subspace
  M === nothing && error("Please provide an `M` with `M >= nus + 1`, where nus is the number of unstable covariant Lyapunov vectors.")

  NILSAS{chunk_size,autodiff,diff_type,typeof(rng),typeof(adjoint_sensealg),typeof(g)}(rng, adjoint_sensealg, M,
    nseg, nstep, autojacvec, g)
end

"""
SteadyStateAdjoint{CS,AD,FDT,VJP,LS} <: AbstractAdjointSensitivityAlgorithm{CS,AD,FDT}

An implementation of the adjoint differentiation of a nonlinear solve. Uses the
implicit function theorem to directly compute the derivative of the solution to
``f(u,p) = 0`` with respect to `p`.

## Constructor

```julia
SteadyStateAdjoint(;chunk_size = 0, autodiff = true,
                    diff_type = Val{:central},
                    autojacvec = autodiff, linsolve = nothing)
```

## Keyword Arguments

## Keyword Arguments

* `autodiff`: Use automatic differentiation for constructing the Jacobian
  if the Jacobian needs to be constructed.  Defaults to `true`.
* `chunk_size`: Chunk size for forward-mode differentiation if full Jacobians are
  built (`autojacvec=false` and `autodiff=true`). Default is `0` for automatic
  choice of chunk size.
* `diff_type`: The method used by FiniteDiff.jl for constructing the Jacobian
  if the full Jacobian is required with `autodiff=false`.
* `autojacvec`: Calculate the vector-Jacobian product (`J'*v`) via automatic
  differentiation with special seeding. The default is `true`. The total set
  of choices are:
    - `false`: the Jacobian is constructed via FiniteDiff.jl
    - `true`: the Jacobian is constructed via ForwardDiff.jl
    - `TrackerVJP`: Uses Tracker.jl for the vjp.
    - `ZygoteVJP`: Uses Zygote.jl for the vjp.
    - `EnzymeVJP`: Uses Enzyme.jl for the vjp.
    - `ReverseDiffVJP(compile=false)`: Uses ReverseDiff.jl for the vjp. `compile`
      is a boolean for whether to precompile the tape, which should only be done
      if there are no branches (`if` or `while` statements) in the `f` function.
* `linsolve`: the linear solver used in the adjoint solve. Defaults to `nothing`,
  which uses a polyalgorithm to attempt to automatically choose an efficient
  algorithm.

For more details on the vjp choices, please consult the sensitivity algorithms
documentation page or the docstrings of the vjp types.

## References

Johnson, S. G., Notes on Adjoint Methods for 18.336, Online at
http://math.mit.edu/stevenj/18.336/adjoint.pdf (2007)
"""
struct SteadyStateAdjoint{CS,AD,FDT,VJP,LS} <: AbstractAdjointSensitivityAlgorithm{CS,AD,FDT}
  autojacvec::VJP
  linsolve::LS
end

Base.@pure function SteadyStateAdjoint(;chunk_size = 0, autodiff = true, diff_type = Val{:central},
                                        autojacvec = autodiff, linsolve = nothing)
  SteadyStateAdjoint{chunk_size,autodiff,diff_type,typeof(autojacvec),typeof(linsolve)}(autojacvec,linsolve)
end

abstract type VJPChoice end

"""
ZygoteVJP <: VJPChoice

Uses Zygote.jl to compute vector-Jacobian products. Tends to be the fastest VJP method if the
ODE/DAE/SDE/DDE is written with mostly vectorized  functions (like neural networks and other
layers from Flux.jl) and the `f` functions is given out-of-place. If the `f` function is
in-place, then `Zygote.Buffer` arrays are used internally which can greatly reduce the
performance of the VJP method.

## Constructor

```julia
ZygoteVJP(compile=false)
```
"""
struct ZygoteVJP <: VJPChoice end

"""
EnzymeVJP <: VJPChoice

Uses Enzyme.jl to compute vector-Jacobian products. Is the fastest VJP whenever applicable,
though Enzyme.jl currently has low coverage over the Julia programming language, for example
restricting the user's defined `f` function to not do things like require garbage collection
or calls to BLAS/LAPACK. However, mutation is supported, meaning that in-place `f` with
fully mutating non-allocating code will work with Enzyme (provided no high level calls to C
like BLAS/LAPACK are used) and this will be the most efficient adjoint implementation.

## Constructor

```julia
EnzymeVJP(compile=false)
```
"""
struct EnzymeVJP <: VJPChoice end

"""
TrackerVJP <: VJPChoice

Uses Tracker.jl to compute the vector-Jacobian products. If `f` is in-place,
then it uses a array of structs formulation to do scalarized reverse mode,
while if `f` is out-of-place then it uses an array-based reverse mode.

Not as efficient as `ReverseDiffVJP`, but supports GPUs when doing array-based
reverse mode.

## Constructor

```julia
TrackerVJP(compile=false)
```
"""
struct TrackerVJP <: VJPChoice end

"""
ReverseDiffVJP{compile} <: VJPChoice

Uses ReverseDiff.jl to compute the vector-Jacobian products. If `f` is in-place,
then it uses a array of structs formulation to do scalarized reverse mode,
while if `f` is out-of-place then it uses an array-based reverse mode.

Usually the fastest when scalarized operations exist in the f function
(like in scientific machine learning applications like Universal Differential Equations)
and the boolean compilation is enabled (i.e. ReverseDiffVJP(true)), if EnzymeVJP fails on
a given choice of `f`.

Does not support GPUs (CuArrays).

## Constructor

```julia
ReverseDiffVJP(compile=false)
```

## Keyword Arguments

* `compile`: Whether to cache the compilation of the reverse tape. This heavily increases
  the performance of the method but requires that the `f` function of the ODE/DAE/SDE/DDE
  has no branching.
"""
struct ReverseDiffVJP{compile} <: VJPChoice
  ReverseDiffVJP(compile=false) = new{compile}()
end

abstract type NoiseChoice end

"""
ZygoteNoise <: NoiseChoice

Uses Zygote.jl to compute vector-Jacobian products for the noise term (for SDE adjoints only).
Tends to be the fastest VJP method if the ODE/DAE/SDE/DDE is written with mostly vectorized
functions (like neural networks and other layers from Flux.jl) and the `f` functions is given
out-of-place. If the `f` function is in-place, then `Zygote.Buffer` arrays are used
internally which can greatly reduce the performance of the VJP method.

## Constructor

```julia
ZygoteNoise()
```
"""
struct ZygoteNoise <: NoiseChoice end

"""
ReverseDiffNoise{compile} <: NoiseChoice

Uses ReverseDiff.jl to compute the vector-Jacobian products for the noise
term differentiation (for SDE adjoints only). If `f` is in-place,
then it uses a array of structs formulation to do scalarized reverse mode,
while if `f` is out-of-place then it uses an array-based reverse mode.

Usually the fastest when scalarized operations exist in the f function
(like in scientific machine learning applications like Universal Differential Equations)
and the boolean compilation is enabled (i.e. ReverseDiffVJP(true)), if EnzymeVJP fails on
a given choice of `f`.

Does not support GPUs (CuArrays).

## Constructor

```julia
ReverseDiffNoise(compile=false)
```

## Keyword Arguments

* `compile`: Whether to cache the compilation of the reverse tape. This heavily increases
  the performance of the method but requires that the `f` function of the ODE/DAE/SDE/DDE
  has no branching.
"""
struct ReverseDiffNoise{compile} <: NoiseChoice
  ReverseDiffNoise(compile=false) = new{compile}()
end

@inline convert_tspan(::ForwardDiffSensitivity{CS,CTS}) where {CS,CTS} = CTS
@inline convert_tspan(::Any) = nothing
@inline alg_autodiff(alg::DiffEqBase.AbstractSensitivityAlgorithm{CS,AD,FDT}) where {CS,AD,FDT} = AD
@inline get_chunksize(alg::DiffEqBase.AbstractSensitivityAlgorithm{CS,AD,FDT}) where {CS,AD,FDT} = CS
@inline diff_type(alg::DiffEqBase.AbstractSensitivityAlgorithm{CS,AD,FDT}) where {CS,AD,FDT} = FDT
@inline function get_jacvec(alg::DiffEqBase.AbstractSensitivityAlgorithm)
  alg.autojacvec isa Bool ? alg.autojacvec : true
end
@inline function get_jacmat(alg::DiffEqBase.AbstractSensitivityAlgorithm)
  alg.autojacmat isa Bool ? alg.autojacmat : true
end
@inline ischeckpointing(alg::DiffEqBase.AbstractSensitivityAlgorithm, sol=nothing) = false
@inline ischeckpointing(alg::InterpolatingAdjoint) = alg.checkpointing
@inline ischeckpointing(alg::InterpolatingAdjoint, sol) = alg.checkpointing || !sol.dense
@inline ischeckpointing(alg::BacksolveAdjoint, sol=nothing) = alg.checkpointing

@inline isnoise(alg::DiffEqBase.AbstractSensitivityAlgorithm) = false
@inline isnoise(alg::InterpolatingAdjoint) = alg.noise
@inline isnoise(alg::BacksolveAdjoint) = alg.noise

@inline isnoisemixing(alg::DiffEqBase.AbstractSensitivityAlgorithm) = false
@inline isnoisemixing(alg::InterpolatingAdjoint) = alg.noisemixing
@inline isnoisemixing(alg::BacksolveAdjoint) = alg.noisemixing

@inline compile_tape(vjp::ReverseDiffVJP{compile}) where compile = compile
@inline compile_tape(noise::ReverseDiffNoise{compile}) where compile = compile
@inline compile_tape(autojacvec::Bool) = false
@inline compile_tape(sensealg::QuadratureAdjoint) = sensealg.compile

"""
ForwardDiffOverAdjoint{A} <: AbstractSecondOrderSensitivityAlgorithm{nothing,true,nothing}

ForwardDiff.jl over a choice of `sensealg` method for the adjoint.

## Constructor

```julia
ForwardDiffOverAdjoint(sensealg)
```

## SciMLProblem Support

This supports any SciMLProblem that the `sensealg` choice supports, provided the solver algorithm
is `SciMLBase.isautodifferentiable`.

## References

Hindmarsh, A. C. and Brown, P. N. and Grant, K. E. and Lee, S. L. and Serban, R.
and Shumaker, D. E. and Woodward, C. S., SUNDIALS: Suite of nonlinear and
differential/algebraic equation solvers, ACM Transactions on Mathematical
Software (TOMS), 31, pp:363–396 (2005)
"""
struct ForwardDiffOverAdjoint{A} <: AbstractSecondOrderSensitivityAlgorithm{nothing,true,nothing}
  adjalg::A
end