function JetProp2DAcoVTIDenQ_DEO2_FDTD(;
        v = Float32[],
        b = Float32[],
        ϵ = Float32[],
        η = Float32[],
        f = 0.85,
        srcfieldfile = joinpath(tempdir(), "field-$(uuid4()).bin"),
        comptype = nothing,
        compscale = 1e-2,
        isinterior = false,
        nz_subcube = 32,
        nx_subcube = 32,
        sz = 0.0,
        sx = 0.0,
        st = 0.0,
        interpmethod = :hicks,
        rz = [0.0],
        rx = [0.0],
        z0 = 0.0,
        x0 = 0.0,
        dz = 10.0,
        dx = 10.0,
        freqQ = 5.0,
        qMin = 0.1,
        qInterior = 100.0,
        padz = 0.0,
        padx = 0.0,
        dtmod,
        dtrec,
        ntrec,
        nbz_cache = 512,
        nbx_cache = 8,
        nbz_inject = 16,
        nbx_inject = 16,
        nsponge = 50,
        wavelet = WaveletCausalRicker(f=5.0),
        freesurface = false,
        imgcondition = "standard",
        nthreads = Sys.CPU_THREADS,
        reportinterval = 500)
    # active and passive earth model properties.  The active set is in the model-space
    active_modelset = Dict{String,Int}()
    passive_modelset = Dict{String,Array{Float32,2}}()
    i = 1
    for (n,x) in (("v",v), ("b",b), ("ϵ",ϵ), ("η",η))
        if length(x) > 0
            passive_modelset[n] = x
        else
            active_modelset[n] = i # so that m[:,:,i] corresponds to property n∈("v","b","ϵ","η")
            i += 1
        end
    end
    @assert length(active_modelset) > 0

    # active and passive wavefields (an active wavefield is serialized to disk)
    active_modelset_keys = keys(active_modelset)
    local active_wavefields, modeltype
    if "v" ∈ active_modelset_keys && "b" ∉ active_modelset_keys && "ϵ" ∈ active_modelset_keys && "η" ∈ active_modelset_keys
        active_wavefields = ["pold","mold","pspace","mspace"]
        modeltype = WaveFD.Prop2DAcoVTIDenQ_DEO2_FDTD_Model_VEA()
    elseif "v" ∈ active_modelset_keys && "b" ∉ active_modelset_keys && "ϵ" ∉ active_modelset_keys && "η" ∉ active_modelset_keys
        active_wavefields = ["pspace","mspace"]
        modeltype = WaveFD.Prop2DAcoVTIDenQ_DEO2_FDTD_Model_V()
    else
        error("unsupported model-space")
    end

    # dtrec must be integer multiple of dtmod
    @assert abs(dtrec - dtmod*round(dtrec/dtmod,RoundNearest)) < eps(Float32)

    # source location (sz,sx,st)
    sz,sx,st = map(val->Float64[val...], (sz,sx,st))

    # domain and range
    dom = JetSpace(Float32, size(first(values(passive_modelset)))..., length(active_modelset))
    rng = JetSpace(Float32, ntrec, length(rz))

    # type conversions
    z0, x0, padz, padx = map(val->Float64(val), (z0, x0, padz, padx)) # used for computing integer grid locations for source injection
    rz, rx = map(val->convert(Array{Float64}, val), (rz, rx))
    dz, dx, dtmod, dtrec, f, freqQ, qMin, qInterior = map(val->Float32(val), (dz, dx, dtmod, dtrec, f, freqQ, qMin, qInterior)) # used in F.D. calculations
    @assert length(rz) == length(rx)
    if isa(wavelet, Array) == true
        wavelet = convert(Array{Float32}, wavelet)
    end

    # ginsu view of earth model... ginsu is aware of the sponge
    nsponge_top = freesurface ? 0 : nsponge
    padz_top = freesurface ? 0.0 : padz

    ginsu = Ginsu((z0,x0), (dz,dx), size(b), (sz,sx), (rz,rx), ((padz_top,padz),(padx,padx)), ((nsponge_top,nsponge),(nsponge,nsponge)), T=Float32)

    # if srcfieldfile is specified make sure its containing folder exists
    if srcfieldfile != ""
        srcfieldpath = split(srcfieldfile, "/")[1:end-1]
        if length(srcfieldpath) > 0
            srcfieldpath = join(srcfieldpath, "/")
            mkpath(srcfieldpath)
        end
    end

    # compression for nonlinear source wavefields
    C = WaveFD.comptype(comptype, Float32)[1]
    compressor = Dict{String,WaveFD.Compressor{Float32,Float32,C,2}}()

    # we need to serialize for the data, but not for the linearization
    _active_wavefields = "pold" ∈ active_wavefields ? active_wavefields : [active_wavefields;"pold"]

    for active_wavefield in _active_wavefields
        compressor[active_wavefield] = WaveFD.Compressor(Float32, Float32, C, size(ginsu,interior=isinterior),
            (nz_subcube,nx_subcube), compscale, ntrec, isinterior)
    end

    # imaging condition
    icdict = Dict(
        lowercase("standard") => WaveFD.ImagingConditionStandard(),
        lowercase("FWI") => WaveFD.ImagingConditionWaveFieldSeparationFWI(),
        lowercase("RTM") => WaveFD.ImagingConditionWaveFieldSeparationRTM())

    if lowercase(imgcondition) ∉ keys(icdict)
        error("Supplied imaging condition 'imgcondition' is not in [standard, FWI, RTM]")
    end

    Jet(
        dom = dom,
        rng = rng,
        f! = JopProp2DAcoVTIDenQ_DEO2_FDTD_f!,
        df! = JopProp2DAcoVTIDenQ_DEO2_FDTD_df!,
        df′! = JopProp2DAcoVTIDenQ_DEO2_FDTD_df′!,
        s = (
            modeltype = modeltype,
            active_modelset = active_modelset,
            passive_modelset = passive_modelset,
            active_wavefields = active_wavefields,
            f = Float32(f),
            srcfieldfile = srcfieldfile,
            srcfieldhost = Ref(""),
            chksum = Ref(zero(UInt32)),
            compressor = compressor,
            isinterior = isinterior,
            sz = sz,
            sx = sx,
            st = st,
            interpmethod = interpmethod,
            rz = rz,
            rx = rx,
            z0 = z0,
            x0 = x0,
            dz = dz,
            dx = dx,
            freqQ = freqQ,
            qMin = qMin,
            qInterior = qInterior,
            ginsu = ginsu,
            nsponge = nsponge,
            dtmod = dtmod,
            dtrec = dtrec,
            ntrec = ntrec,
            nbz_cache = nbz_cache,
            nbx_cache = nbx_cache,
            nbz_inject = nbz_inject,
            nbx_inject = nbx_inject,
            wavelet = wavelet,
            freesurface = freesurface,
            imgcondition = get(icdict, lowercase(imgcondition), WaveFD.ImagingConditionStandard()),
            nthreads = nthreads,
            reportinterval = reportinterval,
            stats = Dict{String,Float64}("MCells/s"=>0.0, "%io"=>0.0, "%inject/extract"=>0.0, "%imaging"=>0.0)))
end

@doc """
    JopNlProp2DAcoVTIDenQ_DEO2_FDTD(; kwargs...)
    JopLnProp2DAcoVTIDenQ_DEO2_FDTD(; m₀, kwargs...)

Create a `Jets` nonlinear or linearized operator for 2D pseudo- visco-acoustic, vertical transverse isotropy,
variable density modeling.

# Model Parameters
This propagator operates with four model parameters, as shown in the table below.

| Parameter | Description                                       |
|:---------:|:--------------------------------------------------|
| `v`       | P wave velocity                                   |
| `ϵ`       | Modified Thomsen's weak anisotropy parameter      |
| `η`       | Modified Alkhalifah's weak anisotropy parameter η |
| `b`       | buoyancy (reciprocal density)                     |

## Pseudo-acoustic approximation
With the pseudo-acoustic approximation we employ, the transformation to self-adjoint form requires a
parameter `f` representing the average ratio of shear-wave to p-wave velocity, and a modfication
to Alkhalifah's weak anisotropy parameter `η`. We show formulas for these two parameters below.
The parameter `f` is specified as a scalar for the entire model, with default value 0.85, implying
a shear velocity around 38% of the p wave velocity.
* `η = sqrt[2(ϵ-δ)/(f+2ϵ)]`
* `f = 1 - Vₛ² / Vₚ²`

For more information about the pseudo-acoustic approximation we employ please see
https://library.seg.org/doi/10.1190/segam2016-13878451.1.

## Active and passive model parameters
There are two modes of operation that define different sets of **active** and **passive** parameters,
*velocity only* and *velocity and anisotropy*.

An **active** parameter can be inverted for using the Jacobian linearized operator machinery,
and a **passive** parameter is constant. The two modes are:

| Mode                    | Active Parameters | Passive Parameters |
|:------------------------|:------------------|:-------------------|
| velocity only           | [`v`]             | [`ϵ`, `η`, `b`]    |
| velocity and anisotropy | [`v`, `ϵ`, `η`]   | [`b`]              |

To make a parameter **passive**, you pass the array for that parameter in the constructor for the
operator, implying that it is in state and does not change with the action of the operator.
Parameters that are *not* passed to the constructor are assumed to be **active**, and will be part
of the model that the operator acts on, stored as a 3D array with the following indices:
`[Z coord][X coord][Active Parameter]`

# Examples

## Model and acquisition geometry setup
1. load modules Jets, WaveFD, and JetPackWaveFD
1. set up the model discretization, coordinate size and spacing
1. set up the acquisition geometry, including the time discretization, locations for source and receivers, and the source wavelet
1. create constant models for `v`, `ϵ`, `η`, `b`
```
using Jets, WaveFD, JetPackWaveFD
nz,nx = 200, 100                      # spatial discretization size
dz,dx = 20.0, 20.0                    # spatial discretization sampling
ntrec = 1101                          # number of temporal samples in recorded data
dtrec = 0.0100                        # sample rate for recorded data
dtmod = 0.0025                        # sample rate for modeled data
wavelet = WaveletCausalRicker(f=5.0)  # type of wavelet to use (source signature)
sz = dz                               # source Z location
sx = dx*(nx/2)                        # source X location
rx = dx*[0:0.5:nx-1;];                # array of receiver X locations
rz = 2*dz*ones(length(0:0.5:nx-1));   # array of receiver Z locations
b = ones(Float32, nz, nx);            # constant buoyancy
ϵ = 0.1 * ones(Float32, nz, nx);      # constant epsilon
η = 0.2 * ones(Float32, nz, nx);      # constant eta
```

## Construct and apply the nonlinear operator (`v` is active; `ϵ, η, b` are passive)
1. create the nonlinear operator `F`
1. create the model vector and set parameter `v`
1. perform nonlinear forward modeling with model `m₀` and return the resulting modeled data in `d`
```
F = JopNlProp2DAcoVTIDenQ_DEO2_FDTD(; b = b, ϵ = ϵ, η = η, ntrec = ntrec, dtrec = dtrec,
    dtmod = dtmod, dz = dz, dx = dx, wavelet = wavelet, sz = sz, sx = sx, rz = rz, rx = rx)
m₀ = zeros(domain(F));
m₀[:,:,1] .= 1500;
d = F*m₀;              # forward nonlinear op
```

## Construct and apply the nonlinear operator (`v, ϵ, η` are active; `b` is passive)
1. create the nonlinear operator `F`
1. create the model vector and set parameters `v, ϵ, η`
1. perform nonlinear forward modeling with model `m₀` and return the resulting modeled data in `d`
```
F = JopNlProp2DAcoVTIDenQ_DEO2_FDTD(; b = b, ntrec = ntrec, dtrec = dtrec, dtmod = dtmod,
    dz = dz, dx = dx, wavelet = wavelet, sz = sz, sx = sx, rz = rz, rx = rx)
m₀ = zeros(domain(F));
m₀[:,:,1] .= 1500;
m₀[:,:,2] .= 0.1;
m₀[:,:,3] .= 0.2;
d = F*m₀;              # forward nonlinear op
```

## Construct and apply the linearized Jacobian operator (method 1)
For this example we assume in the model that `v, ϵ, η` are active parameters, and `b` is passive.
1. create the nonlinear operator `F` directly by construction of a jet.
1. create the model vector and set parameters `v, ϵ, η`
1. create the Jacobian operator `J` by linearization of `F` at point `m₀`
1. create a random model perturbation vector `δm`.
1. perform linearized forward (Born) modeling on the model perturbation vector `δm` and
    return the resulting data perturbation in `δd`.
1. perform linearized adjoint (Born) migration on the data perturbation vector `δd` and
    return the resulting model perturbation in `δm`.

Note that the Jacobian operators `J` and `J'` require the serialized nonlinear forward wavefield,
and this is generated automatically whenever required. If you watch the logging to standard out
from this example, you will first see the finite difference evolution for the nonlinear forward,
followed by the linearized forward, and finally the linearized adjoint.
```
F = JopNlProp2DAcoVTIDenQ_DEO2_FDTD(; b = b, ntrec = ntrec, dz = dz, dx = dx,
    dtrec = dtrec, dtmod = dtmod, wavelet = wavelet, sz = sz, sx = sx, rz = rz, rx = rx)
m₀ = zeros(domain(F));
m₀[:,:,1] .= 1500;
m₀[:,:,2] .= 0.1;
m₀[:,:,3] .= 0.2;
J = jacobian(F, m₀)
δm = rand(domain(J));
δd = J*δm;             # forward linearized op
δm = J'*δd;            # adjoint linearized op
```

## Construct and apply the linearized Jacobian operator (method 2)
For this example we assume in the model that `v, ϵ, η` are active parameters, and `b` is passive.
1. create the model vector and set parameters `v, ϵ, η`
1. create the Jacobian operator `J` at point `m₀` directly by construction of a jet.
1. create a random model perturbation vector `δm`.
1. perform linearized forward (Born) modeling on the model perturbation vector `δm` and
    return the resulting data perturbation in `δd`.
1. perform linearized adjoint (Born) migration on the data perturbation vector `δd` and
    return the resulting model perturbation in `δm`.
```
m₀ = zeros(Float32, nz, nx, 3);
m₀[:,:,1] .= 1500;
m₀[:,:,2] .= 0.1;
m₀[:,:,3] .= 0.2;
J = JopLnProp2DAcoVTIDenQ_DEO2_FDTD(; m₀ = m₀, b = b, ntrec = ntrec, dz = dz, dx = dx,
    dtrec = dtrec, dtmod = dtmod, wavelet = wavelet, sz = sz, sx = sx, rz = rz, rx = rx)
δm = rand(domain(J));
δd = J*δm;             # forward linearized op
δm = J'*δd;            # adjoint linearized op
```

# Required Parameters
* `m₀` the point at which the jet is linearized. Note this argument is required in the constuctor for
    `JopLnProp2DAcoVTIDenQ_DEO2_FDTD` but not `JopNlProp2DAcoVTIDenQ_DEO2_FDTD`. This constuctor is shown
    in the last example above. Please note that you must consider which parameters are active and passive,
    per the discussion on model parameters above and examples.
* `dtmod` The sample rate for the modeled data. You can establish a lower bound for the modeling sample rate
    with the following expression: `dt = 0.75 * 0.38 * max(dx,dz) / maximum(v)`. Note that the usual
    Courant–Friedrichs–Lewy condition (CFL condition) for stability in finite difference modeling is
    modified *heuristically* to include the impact of the visco-acoustic implementation of this operator,
    requiring a 25% smaller smaple rate.
* `dtrec` The number of time samples in the recorded data. Note requirement that `dtrec`
    be an even multiple of `dtmod`.
* `ntrec` The number of time samples in the recorded data. Note that the number of samples in the modeled
    data is determined from `ntrec`, `dtrec`, and `dtmod`.

# Optional Parameters
Defaults for arguments are shown inside square brackets.

* `b [ones(Float32,0,0)]` the buoyancy (reciprocal density) array.
* `f [0.85]` See the discussion above regarding the **Pseudo-acoustic approximation** used here
* `srcfieldfile [joinpath(tempdir(), "field-$(uuid4()).bin")]` the full path to a scratch file used for
    the serializationof the compressed nonlinear source wavefield.
* `comptype [nothing]` the type of compression to use for the serialization of
    the nonlinear source wavefield. The type of compression must be one of:
    * `nothing` - no compression.
    * `Float32` - if wavefield is `Float64`, then do a simple conversion to Float32.
    * `UInt32` - compression using CvxCompress (windowing + 2D wavelet transform +
        thresholding + quantization + run-length-encoding).
* `compscale [1e-2]` determines the thresholding for the compression of the nonlinear source
    wavefield prior to serialization. Larger values mean more aggressive compression. You can
    likely increase compscale to 1.0 before you start to notice differences in output from
    Jacobian operations.
* `nz_subcube, nx_subcube [32]` The Z and X sizes of windows used for compression
    of the nonlinear source wavefield with `CvxCompress`. Note the requirement
    `[8 <= n*_subcube <=256]`.
* `isinterior [false]` boolean flag that indicates how the nonlinear source wavefield is
    saved. For large models, operation will be faster with `isinterior = true`, but
    the linearization correctness test may fail.
    * `true` the entire model including absorbing boundaries is serialized and deserialized
    * `false` the interior part of the model excluding absorbing boundaries is serialized
        and deserialized
* `sz [0.0]` Array of source Z coordinate. Note that if multiple sources are provided,
    they will will be injected simultaneously during finite difference evolution.
* `sx [0.0]` Array of source X coordinate.
* `st [0.0]` Array of source delay times.
* `interpmethod [:hicks]` Type of physical interpolation for sources and receivers. For locations
    that are not on the physical grid coordinates, interpolation is used either to inject or extract
    information. `interpmethod` must be one of:
    * `:hicks` Hicks 2D sinc interpolation (up to 8x8 nonzero points per location)
    * `:linear` bilinear interpolation (up to 2x2 nonzero points per location)
* `rz [[0.0]]` 2D array of receiver Z coordinates
* `rx [[0.0]]` 2D array of receiver Z coordinates
* `z0 [0.0]` Origin of physical coordinates for the Z dimension.
* `x0 [0.0]` Origin of physical coordinates for the X dimension.
* `dz [10.0]` Spacing of physical coordinates in the Z dimension.
* `dx [10.0]` Spacing of physical coordinates in the X dimension.
* `freqQ [5.0]` The center frequency for the Maxwell body approximation to dissipation only attenuation.
    Please see `JetPackWaveFD` package documentation for more information concerning the attenuation model.
* `qMin [0.1]` The minimum value for Qp at the boundary of the model used in our Maxwell body approximation
    to dissipation only attenuation. This is not a physically meaningful value for Qp, as we use the
    attenuation to implement absorbing boundary conditions and eliminate outgoing waves on the
    boundaries of the computational domain.
    Please see `JetPackWaveFD` package documentation for more information concerning the attenuation model.
* `qInterior [100.0]` the value for Qp in the interior of the model used in our Maxwell body approximation
    to dissipation only attenuation. This is the value for Qp away from the absorbing boundaries and is
    a physically meaningful value.
    Please see `JetPackWaveFD` package documentation for more information concerning the attenuation model.
* `padz, padx [0.0], [0.0]` - apply extra padding to the survey determined aperture in `Ginsu`.
    Please see `Ginsu` for more information.
* `nbz_cache, nbx_cache [512], [8]` The size of cache blocks in the Z and X dimensions.
    In general the cache block in the Z (fast) dimension should be ≥ the entire size of that dimension,
    and the cache block size in the slower dimension is generally small in order to allow the entire
    block to fit in cache.
* `nbz_inject, nbx_inject = [16], [16]` The number of blocks in the Z and X dimensions for threading
    the wavefield injection.
* `nsponge [50]` The number of grid cells to use for the absorbing boundary. For high fidelity modeling
    this should be > 60 grid points, but can be significantly smaller for some use cases like low frequency
    full waveform inversion.
* `wavelet [WaveletCausalRicker(f=5.0)]` The source wavelet, can be specified as either a Wavelet type
    or an array.
* `freesurface [false]` Determines if a free surface (`true`) or absorbing (`false`) top boundary condition
    is applied.
* `imgcondition` ["standard"] Selects the type of imaging condition used. Choose from "standard", "FWI",
    and "RTM". "FWI" and "RTM" will perform Kz wavenumber filtering prior to the imaging condition
    in order to promote long wavelengths (for FWI), or remove long wavelength backscattered energy (for
    RTM). Note the true adjoint only exists for "standard" imaging condition currently.
* `nthreads [Sys.CPU_THREADS]` The number of threads to use for OpenMP parallelization of the modeling.
* `reportinterval [500]` The interval at which information about the propagtion is logged.

See also: `Ginsu`, `WaveletSine`, `WaveletRicker`, `WaveletMinPhaseRicker`, `WaveletDerivRicker`,
    `WaveletCausalRicker`, `WaveletOrmsby`, `WaveletMinPhaseOrmsby`
"""
JopNlProp2DAcoVTIDenQ_DEO2_FDTD(;kwargs...) = JopNl(JetProp2DAcoVTIDenQ_DEO2_FDTD(;kwargs...))

@doc (@doc JopNlProp2DAcoVTIDenQ_DEO2_FDTD)
JopLnProp2DAcoVTIDenQ_DEO2_FDTD(; m₀, kwargs...) = JopLn(JetProp2DAcoVTIDenQ_DEO2_FDTD(;kwargs...), m₀)

export JopNlProp2DAcoVTIDenQ_DEO2_FDTD
export JopLnProp2DAcoVTIDenQ_DEO2_FDTD

function JopProp2DAcoVTIDenQ_DEO2_FDTD_nonlinearforward!(d::AbstractArray, m::AbstractArray; kwargs...)
    # make propagator
    nz_ginsu,nx_ginsu = size(kwargs[:ginsu])
    z0_ginsu,x0_ginsu = origin(kwargs[:ginsu])
    p = Prop2DAcoVTIDenQ_DEO2_FDTD(
        nz = nz_ginsu,
        nx = nx_ginsu,
        nbz = kwargs[:nbz_cache],
        nbx = kwargs[:nbx_cache],
        dz = kwargs[:dz],
        dx = kwargs[:dx],
        dt = kwargs[:dtmod],
        nsponge = kwargs[:nsponge],
        nthreads = kwargs[:nthreads],
        freesurface = kwargs[:freesurface],
        freqQ = kwargs[:freqQ],
        qMin = kwargs[:qMin],
        qInterior = kwargs[:qInterior])

    # wave-fields
    wavefields = Dict("pcur"=>WaveFD.PCur(p), "pold"=>WaveFD.POld(p), "pspace"=>WaveFD.PSpace(p), "mcur"=>WaveFD.MCur(p), "mold"=>WaveFD.MOld(p), "mspace"=>WaveFD.MSpace(p))

    # earth model
    model_ginsu = Dict("v"=>WaveFD.V(p), "b"=>WaveFD.B(p), "ϵ"=>WaveFD.Eps(p), "η"=>WaveFD.Eta(p), "f"=>WaveFD.F(p))

    # ginsu'd earth model (active-set)
    for prop in keys(kwargs[:active_modelset])
        sub!(model_ginsu[prop], kwargs[:ginsu], @view(m[:,:,kwargs[:active_modelset][prop]]), extend=true)
    end
    for prop in keys(kwargs[:passive_modelset])
        sub!(model_ginsu[prop], kwargs[:ginsu], kwargs[:passive_modelset][prop], extend=true)
    end
    model_ginsu["f"] .= kwargs[:f]

    ginsu_interior_range = interior(kwargs[:ginsu])

    # we need to serialize "pold" for the data but, not (necessarily) for the linearization
    active_wavefields = "pold" ∈ kwargs[:active_wavefields] ? kwargs[:active_wavefields] : [kwargs[:active_wavefields]; "pold"]

    it0, ntmod_wav = WaveFD.default_ntmod(kwargs[:dtrec], kwargs[:dtmod], kwargs[:st], kwargs[:ntrec])

    # source wavelet for injection, one for each source location
    wavelet_realization = realizewavelet(kwargs[:wavelet], kwargs[:sz], kwargs[:sx], kwargs[:st], kwargs[:dtmod], ntmod_wav)

    # Get source and receiver interpolation coefficients
    local iz_sou, ix_sou, c_sou
    if kwargs[:interpmethod] == :hicks
        iz_sou, ix_sou, c_sou = WaveFD.hickscoeffs(kwargs[:dz], kwargs[:dx], z0_ginsu, x0_ginsu, nz_ginsu, nx_ginsu, kwargs[:sz], kwargs[:sx])
    else
        iz_sou, ix_sou, c_sou = WaveFD.linearcoeffs(kwargs[:dz], kwargs[:dx], z0_ginsu, x0_ginsu, nz_ginsu, nx_ginsu, kwargs[:sz], kwargs[:sx])
    end
    blks_sou = WaveFD.source_blocking(nz_ginsu, nx_ginsu, kwargs[:nbz_inject], kwargs[:nbx_inject], iz_sou, ix_sou, c_sou)

    c_sou_scaled = Array{Array{Float32,2},1}(undef, length(c_sou))
    for i = 1:length(c_sou_scaled)
        c_sou_scaled[i] = similar(c_sou[i])
        for ix = 1:size(c_sou_scaled[i], 2), iz = 1:size(c_sou_scaled[i], 1)
            jz = iz_sou[i][iz]
            jx = ix_sou[i][ix]
            c_sou_scaled[i][iz,ix] = c_sou[i][iz,ix] * kwargs[:dtmod]^2 * model_ginsu["v"][jz,jx]^2 / model_ginsu["b"][jz,jx]
        end
    end
    blks_sou_scaled = WaveFD.source_blocking(nz_ginsu, nx_ginsu, kwargs[:nbz_inject], kwargs[:nbx_inject], iz_sou, ix_sou, c_sou_scaled)

    local iz_rec, ix_rec, c_rec
    if length(d) > 0
        if kwargs[:interpmethod] == :hicks
            iz_rec, ix_rec, c_rec = WaveFD.hickscoeffs(kwargs[:dz], kwargs[:dx], z0_ginsu, x0_ginsu, nz_ginsu, nx_ginsu, kwargs[:rz], kwargs[:rx])
        else
            iz_rec, ix_rec, c_rec = WaveFD.linearcoeffs(kwargs[:dz], kwargs[:dx], z0_ginsu, x0_ginsu, nz_ginsu, nx_ginsu, kwargs[:rz], kwargs[:rx])
        end
    end

    # disk-file for source-field serialization
    iofield = Dict{String,IOStream}()
    if kwargs[:srcfieldfile] != ""
        for active_wavefield in active_wavefields
            filename = "$(kwargs[:srcfieldfile])-$(active_wavefield)"
            try
                if isfile(filename) == true
                    rm(filename)
                end
                iofield[active_wavefield] = open(filename, "w")
            catch
                @info "Unable to open $(filename) on proc $(myid()) - $(gethostname())"
                rethrow()
            end
            open(kwargs[:compressor][active_wavefield])
        end
    end

    itskip = round(Int, kwargs[:dtrec]/kwargs[:dtmod])
    time1 = time()
    cumtime_io, cumtime_ex, cumtime_pr = 0.0, 0.0, 0.0
    kwargs[:reportinterval] == 0 || @info "nonlinear forward on $(gethostname()), srcfieldfile=$(kwargs[:srcfieldfile])"

    set_zero_subnormals(true)
    for it = 1:ntmod_wav
        if kwargs[:reportinterval] != 0 && (it % kwargs[:reportinterval] == 0 || it == ntmod_wav)
            JopProp2DAcoVTIDenQ_DEO2_FDTD_write_history_nl(kwargs[:ginsu], it, ntmod_wav, time()-time1, cumtime_io, cumtime_ex, cumtime_pr, wavefields["pcur"], d)
        end

        # propagate and wavefield swap
        cumtime_pr += @elapsed WaveFD.propagateforward!(p)
        wavefields["pcur"],wavefields["pold"] = wavefields["pold"],wavefields["pcur"]
        wavefields["mcur"],wavefields["mold"] = wavefields["mold"],wavefields["mcur"]

        # inject source wavelet
        cumtime_ex += @elapsed begin
            WaveFD.injectdata!(wavefields["pcur"], blks_sou_scaled, wavelet_realization, it)
            WaveFD.injectdata!(wavefields["pspace"], blks_sou, wavelet_realization, it)
        end

        if it >= it0 && rem(it-it0,itskip) == 0
            if length(d) > 0
                # extract receiver data
                cumtime_ex += @elapsed WaveFD.extractdata!(d, wavefields["pold"], div(it-it0,itskip)+1, iz_rec, ix_rec, c_rec)
            end

            # scale spatial derivatives by v^2/b to make them temporal derivatives
            cumtime_io += @elapsed WaveFD.scale_spatial_derivatives!(p)

            if kwargs[:srcfieldfile] != ""
                cumtime_io += @elapsed for active_wavefield in active_wavefields
                    if kwargs[:isinterior]
                        WaveFD.compressedwrite(iofield[active_wavefield], kwargs[:compressor][active_wavefield], div(it-it0,itskip)+1,
                            wavefields[active_wavefield], ginsu_interior_range)
                    else
                        WaveFD.compressedwrite(iofield[active_wavefield], kwargs[:compressor][active_wavefield], div(it-it0,itskip)+1,
                            wavefields[active_wavefield])
                    end
                end
            end
        end
    end
    set_zero_subnormals(false)
    JopProp2DAcoVTIDenQ_DEO2_FDTD_stats(kwargs[:stats], kwargs[:ginsu], ntmod_wav, time()-time1, cumtime_io, cumtime_ex)

    if kwargs[:srcfieldfile] != ""
        for active_wavefield in active_wavefields
            close(iofield[active_wavefield])
            close(kwargs[:compressor][active_wavefield])
        end
    end

    free(p)

    nothing
end

function JopProp2DAcoVTIDenQ_DEO2_FDTD_f!(d::AbstractArray, m::AbstractArray{Float32}; kwargs...)
    d .= 0
    isvalid, _chksum = isvalid_srcfieldfile(m, kwargs[:srcfieldhost][], kwargs[:srcfieldfile]*"-pold", kwargs[:chksum][])
    if !isvalid
        JopProp2DAcoVTIDenQ_DEO2_FDTD_nonlinearforward!(d, m; kwargs...)
        kwargs[:chksum][] = _chksum
        kwargs[:srcfieldhost][] = gethostname()
    else
        field = Array{Float32}(undef,size(kwargs[:ginsu], interior=kwargs[:isinterior]))
        local iz, ix, c
        if kwargs[:interpmethod] == :hicks
            iz, ix, c = WaveFD.hickscoeffs(kwargs[:dz], kwargs[:dx],
                origin(kwargs[:ginsu], interior=kwargs[:isinterior])...,
                size(kwargs[:ginsu], interior=kwargs[:isinterior])..., kwargs[:rz], kwargs[:rx])
        else
            iz, ix, c = WaveFD.linearcoeffs(kwargs[:dz], kwargs[:dx],
                origin(kwargs[:ginsu], interior=kwargs[:isinterior])...,
                size(kwargs[:ginsu], interior=kwargs[:isinterior])..., kwargs[:rz], kwargs[:rx])
        end

        iofield = open("$(kwargs[:srcfieldfile])-pold")
        open(kwargs[:compressor]["pold"])
        for it = 1:kwargs[:ntrec]
            WaveFD.compressedread!(iofield, kwargs[:compressor]["pold"], it, field)
            WaveFD.extractdata!(d, field, it, iz, ix, c)
        end
        close(iofield)
        close(kwargs[:compressor]["pold"])
    end
    d
end

function JopProp2DAcoVTIDenQ_DEO2_FDTD_df!(δd::AbstractArray, δm::AbstractArray; kwargs...)
    nz_ginsu,nx_ginsu = size(kwargs[:ginsu])
    z0_ginsu,x0_ginsu = origin(kwargs[:ginsu])
    p = Prop2DAcoVTIDenQ_DEO2_FDTD(
        nz = nz_ginsu,
        nx = nx_ginsu,
        nbz = kwargs[:nbz_cache],
        nbx = kwargs[:nbx_cache],
        dz = kwargs[:dz],
        dx = kwargs[:dx],
        dt = kwargs[:dtmod],
        nsponge = kwargs[:nsponge],
        nthreads = kwargs[:nthreads],
        freesurface = kwargs[:freesurface],
        freqQ = kwargs[:freqQ],
        qMin = kwargs[:qMin],
        qInterior = kwargs[:qInterior])

    # wave-fields
    pcur,pold = WaveFD.PCur(p),WaveFD.POld(p)

    # earth model
    earth_ginsu = Dict("v"=>WaveFD.V(p), "b"=>WaveFD.B(p), "ϵ"=>WaveFD.Eps(p), "η"=>WaveFD.Eta(p), "f"=>WaveFD.F(p))

    # ginsu'd earth model (active-set)
    for prop in keys(kwargs[:active_modelset])
        sub!(earth_ginsu[prop], kwargs[:ginsu], @view(kwargs[:mₒ][:,:,kwargs[:active_modelset][prop]]), extend=true)
    end
    for prop in keys(kwargs[:passive_modelset])
        sub!(earth_ginsu[prop], kwargs[:ginsu], kwargs[:passive_modelset][prop], extend=true)
    end
    earth_ginsu["f"] .= kwargs[:f]

    δm_ginsu = Dict{String,Array{Float32,2}}()
    for prop in keys(kwargs[:active_modelset])
        δm_ginsu[prop] = sub(kwargs[:ginsu], @view(δm[:,:,kwargs[:active_modelset][prop]]), extend=false)
    end

    ginsu_interior_range = interior(kwargs[:ginsu])

    # pre-compute receiver interpolation coefficients
    local iz,ix,c
    if kwargs[:interpmethod] == :hicks
        iz, ix, c = WaveFD.hickscoeffs(kwargs[:dz], kwargs[:dx], z0_ginsu, x0_ginsu, nz_ginsu, nx_ginsu, kwargs[:rz], kwargs[:rx])
    else
        iz, ix, c = WaveFD.linearcoeffs(kwargs[:dz], kwargs[:dx], z0_ginsu, x0_ginsu, nz_ginsu, nx_ginsu, kwargs[:rz], kwargs[:rx])
    end

    # if necessary, re-run the nonlinear forward
    isvalid, _chksum = isvalid_srcfieldfile(kwargs[:mₒ], kwargs[:srcfieldhost][], kwargs[:srcfieldfile]*"-pold", kwargs[:chksum][])
    if !isvalid
        JopProp2DAcoVTIDenQ_DEO2_FDTD_nonlinearforward!(Array{Float32}(undef,0,0), kwargs[:mₒ]; kwargs...)
        kwargs[:srcfieldhost][] = gethostname()
        kwargs[:chksum][] = _chksum
    end

    # local disk file for source wavefield deserialization
    wavefields = Dict{String,Array{Float32,2}}()
    iofields = Dict{String,IOStream}()
    for active_wavefield in kwargs[:active_wavefields]
        iofields[active_wavefield] = open(kwargs[:srcfieldfile]*"-"*active_wavefield)
        open(kwargs[:compressor][active_wavefield])
        wavefields[active_wavefield] = zeros(Float32, nz_ginsu, nx_ginsu)
    end

    ntmod = WaveFD.default_ntmod(kwargs[:dtrec], kwargs[:dtmod], kwargs[:ntrec])

    δdinterp = zeros(Float32, ntmod, size(δd, 2))

    itskip = round(Int, kwargs[:dtrec]/kwargs[:dtmod])
    kwargs[:reportinterval] == 0 || @info "linear forward on $(gethostname()), srcfieldfile=$(kwargs[:srcfieldfile])"
    time1 = time()
    cumtime_io, cumtime_ex, cumtime_im, cumtime_pr = 0.0, 0.0, 0.0, 0.0

    set_zero_subnormals(true)
    for it = 1:ntmod
        if kwargs[:reportinterval] != 0 && (it % kwargs[:reportinterval] == 0 || it == ntmod)
            JopProp2DAcoVTIDenQ_DEO2_FDTD_write_history_ln(kwargs[:ginsu], it, ntmod, time()-time1,
                cumtime_io, cumtime_ex, cumtime_im, cumtime_pr, pcur, δdinterp, "forward")
        end

        # propagate and swap wavefields
        cumtime_pr += @elapsed WaveFD.propagateforward!(p)
        pcur,pold = pold,pcur

        if rem(it-1,itskip) == 0
            # read source field from disk
            cumtime_io += @elapsed for active_wavefield in kwargs[:active_wavefields]
                if kwargs[:isinterior]
                    WaveFD.compressedread!(iofields[active_wavefield], kwargs[:compressor][active_wavefield], div(it-1,itskip)+1,
                        wavefields[active_wavefield], ginsu_interior_range)
                else
                    WaveFD.compressedread!(iofields[active_wavefield], kwargs[:compressor][active_wavefield], div(it-1,itskip)+1,
                        wavefields[active_wavefield])
                end
            end

            # born injection
            cumtime_im += @elapsed WaveFD.forwardBornInjection!(p, kwargs[:modeltype], δm_ginsu, wavefields)
        end

        # extract data at receivers
        cumtime_ex += @elapsed WaveFD.extractdata!(δdinterp, pold, it, iz, ix, c)
    end
    set_zero_subnormals(false)
    JopProp2DAcoVTIDenQ_DEO2_FDTD_stats(kwargs[:stats], kwargs[:ginsu], ntmod, time()-time1,
        cumtime_io, cumtime_ex, cumtime_im)

    δd .= 0
    WaveFD.interpforward!(WaveFD.interpfilters(kwargs[:dtmod], kwargs[:dtrec], 0, WaveFD.LangC(), kwargs[:nthreads]), δd, δdinterp)

    for active_wavefield in kwargs[:active_wavefields]
        close(iofields[active_wavefield])
        close(kwargs[:compressor][active_wavefield])
    end

    free(p)

    δd
end

function JopProp2DAcoVTIDenQ_DEO2_FDTD_df′!(δm::AbstractArray, δd::AbstractArray; kwargs...)
    nz_ginsu,nx_ginsu = size(kwargs[:ginsu])
    z0_ginsu,x0_ginsu = origin(kwargs[:ginsu])
    p = Prop2DAcoVTIDenQ_DEO2_FDTD(
        nz = nz_ginsu,
        nx = nx_ginsu,
        nbz = kwargs[:nbz_cache],
        nbx = kwargs[:nbx_cache],
        dz = kwargs[:dz],
        dx = kwargs[:dx],
        dt = kwargs[:dtmod],
        nsponge = kwargs[:nsponge],
        nthreads = kwargs[:nthreads],
        freesurface = kwargs[:freesurface],
        freqQ = kwargs[:freqQ],
        qMin = kwargs[:qMin],
        qInterior = kwargs[:qInterior])

    # wave-fields
    pcur,pold = WaveFD.PCur(p),WaveFD.POld(p)

    # ginsu'd earth model
    earth_ginsu = Dict("v"=>WaveFD.V(p), "b"=>WaveFD.B(p), "ϵ"=>WaveFD.Eps(p), "η"=>WaveFD.Eta(p), "f"=>WaveFD.F(p))

    # active model-set
    for prop in keys(kwargs[:active_modelset])
        sub!(earth_ginsu[prop], kwargs[:ginsu], @view(kwargs[:mₒ][:,:,kwargs[:active_modelset][prop]]), extend=true)
    end
    # passive model-set
    for prop in keys(kwargs[:passive_modelset])
        sub!(earth_ginsu[prop], kwargs[:ginsu], kwargs[:passive_modelset][prop], extend=true)
    end
    earth_ginsu["f"] .= kwargs[:f]

    δm_ginsu = Dict{String,Array{Float32,2}}()
    for prop in keys(kwargs[:active_modelset])
        δm_ginsu[prop] = zeros(Float32, nz_ginsu, nx_ginsu)
    end

    ginsu_interior_range = interior(kwargs[:ginsu])

    # Get receiver interpolation coefficients
    local iz, ix, c
    if kwargs[:interpmethod] == :hicks
        iz, ix, c = WaveFD.hickscoeffs(kwargs[:dz], kwargs[:dx], z0_ginsu, x0_ginsu, nz_ginsu, nx_ginsu, kwargs[:rz], kwargs[:rx])
    else
        iz, ix, c = WaveFD.linearcoeffs(kwargs[:dz], kwargs[:dx], z0_ginsu, x0_ginsu, nz_ginsu, nx_ginsu, kwargs[:rz], kwargs[:rx])
    end
    for i = 1:length(c)
        for jx = 1:size(c[i], 2), jz = 1:size(c[i], 1)
            kz = iz[i][jz]
            kx = ix[i][jx]
            c[i][jz,jx] *= kwargs[:dtmod]^2 * earth_ginsu["v"][kz,kx]^2 / earth_ginsu["b"][kz,kx]
        end
    end
    blks = WaveFD.source_blocking(nz_ginsu, nx_ginsu, kwargs[:nbz_inject], kwargs[:nbx_inject], iz, ix, c)

    ntmod = WaveFD.default_ntmod(kwargs[:dtrec], kwargs[:dtmod], kwargs[:ntrec])

    # if necessary, re-run the nonlinear forward
    isvalid, _chksum = isvalid_srcfieldfile(kwargs[:mₒ], kwargs[:srcfieldhost][], kwargs[:srcfieldfile]*"-pold", kwargs[:chksum][])
    if !isvalid
        JopProp2DAcoVTIDenQ_DEO2_FDTD_nonlinearforward!(Array{Float32}(undef,0,0), kwargs[:mₒ]; kwargs...)
        kwargs[:srcfieldhost][] = gethostname()
        kwargs[:chksum][] = _chksum
    end

    # local disk file for source wavefield deserialization
    wavefields = Dict{String,Array{Float32,2}}()
    iofields = Dict{String,IOStream}()
    for active_wavefield in kwargs[:active_wavefields]
        iofields[active_wavefield] = open(kwargs[:srcfieldfile]*"-"*active_wavefield)
        open(kwargs[:compressor][active_wavefield])
        wavefields[active_wavefield] = zeros(Float32, nz_ginsu, nx_ginsu)
    end

    # adjoint interpolation of receiver wavefield
    δdinterp = zeros(Float32, ntmod, size(δd, 2))
    WaveFD.interpadjoint!(WaveFD.interpfilters(kwargs[:dtmod], kwargs[:dtrec], 0, WaveFD.LangC(), kwargs[:nthreads]), δdinterp, δd)

    itskip = round(Int, kwargs[:dtrec]/kwargs[:dtmod])
    kwargs[:reportinterval] == 0 || @info "linear adjoint on $(gethostname()), srcfieldfile=$(kwargs[:srcfieldfile])"
    time1 = time()
    cumtime_io, cumtime_ex, cumtime_im, cumtime_pr = 0.0, 0.0, 0.0, 0.0

    set_zero_subnormals(true)
    for it = ntmod:-1:1
        if kwargs[:reportinterval] != 0 && (it % kwargs[:reportinterval] == 0 || it == ntmod)
            JopProp2DAcoVTIDenQ_DEO2_FDTD_write_history_ln(kwargs[:ginsu], it, ntmod, time()-time1, cumtime_io, cumtime_ex, cumtime_im, cumtime_pr, pcur, δdinterp, "adjoint")
        end

        # propagate and wavefield swap
        cumtime_pr += @elapsed WaveFD.propagateadjoint!(p)
        pcur,pold = pold,pcur

        # inject receiver data
        cumtime_ex += @elapsed WaveFD.injectdata!(pcur, blks, δdinterp, it)

        if rem(it-1,itskip) == 0
            # read source field from disk
            cumtime_io += @elapsed for active_wavefield in kwargs[:active_wavefields]
                if kwargs[:isinterior]
                    WaveFD.compressedread!(iofields[active_wavefield], kwargs[:compressor][active_wavefield], div(it-1,itskip)+1,
                        wavefields[active_wavefield], ginsu_interior_range)
                else
                    WaveFD.compressedread!(iofields[active_wavefield], kwargs[:compressor][active_wavefield], div(it-1,itskip)+1,
                        wavefields[active_wavefield])
                end
            end

            # born accumulation
            cumtime_im += @elapsed WaveFD.adjointBornAccumulation!(p, kwargs[:modeltype], kwargs[:imgcondition], δm_ginsu, wavefields)
        end
    end
    set_zero_subnormals(false)
    JopProp2DAcoVTIDenQ_DEO2_FDTD_stats(kwargs[:stats], kwargs[:ginsu], ntmod, time()-time1, cumtime_io, cumtime_ex, cumtime_im)

    # undo ginsu
    for prop in keys(kwargs[:active_modelset])
        super!(@view(δm[:,:,kwargs[:active_modelset][prop]]), kwargs[:ginsu], δm_ginsu[prop], accumulate=false)
    end

    for active_wavefield in kwargs[:active_wavefields]
        close(iofields[active_wavefield])
        close(kwargs[:compressor][active_wavefield])
    end

    free(p)

    δm
end

modelindex(F::Jop{T}, key) where {D,R,T<:Jet{D,R,typeof(JopProp2DAcoVTIDenQ_DEO2_FDTD_f!)}} = state(F).active_modelset[key]

function srcillum!(γ, A::Jop{T}, m::AbstractArray{Float32}) where {D,R,T<:Jet{D,R,typeof(JopProp2DAcoVTIDenQ_DEO2_FDTD_f!)}}
    s = state(A)
    isvalid, _chksum = isvalid_srcfieldfile(jet(A).mₒ, s.srcfieldhost[], s.srcfieldfile*"-pold", s.chksum[])
    if !isvalid
        JopProp2DAcoVTIDenQ_DEO2_FDTD_nonlinearforward!(Array{Float32}(undef,0,0), m; s...)
        s.chksum[] = _chksum
        s.srcfieldhost[] = gethostname()
    end
    open(s.compressor["pold"])
    I = srcillum!(γ, s.srcfieldfile*"-pold", s.compressor["pold"], s.isinterior, s.ginsu, s.ntrec, s.nthreads)
    close(s.compressor["pold"])
    I
end

function JopProp2DAcoVTIDenQ_DEO2_FDTD_stats(stats, ginsu, it, cumtime_total, cumtime_io, cumtime_ex, cumtime_im=0.0)
    stats["MCells/s"] = megacells_per_second(size(ginsu)..., it-1, cumtime_total)
    stats["%io"] = cumtime_total > 0 ? cumtime_io/cumtime_total*100.0 : 0.0
    stats["%inject/extract"] = cumtime_total > 0 ? cumtime_ex/cumtime_total*100.0 : 0.0
    stats["%imaging"] = cumtime_total > 0 ? cumtime_im/cumtime_total*100.0 : 0.0
end

Jets.perfstat(J::T) where {D,R,T<:Jet{D,R,typeof(JopProp2DAcoVTIDenQ_DEO2_FDTD_f!)}} = state(J).stats

@inline function JopProp2DAcoVTIDenQ_DEO2_FDTD_write_history_ln(ginsu, it, ntmod, cumtime_total, cumtime_io, cumtime_ex, cumtime_im, cumtime_pr, pcur, d::AbstractArray{T}, mode) where {T}
    jt = occursin("adjoint", mode) ? ntmod-it+1 : it
    kt = fmt("5d", jt)
    nt = fmt("5d", ntmod)
    mcells = fmt("7.2f", megacells_per_second(size(ginsu)..., jt-1, cumtime_total))
    IO = fmt("5.2f", cumtime_total > 0 ? cumtime_io/cumtime_total*100.0 : 0.0)
    EX = fmt("5.2f", cumtime_total > 0 ? cumtime_ex/cumtime_total*100.0 : 0.0)
    IM = fmt("5.2f", cumtime_total > 0 ? cumtime_im/cumtime_total*100.0 : 0.0)
    PR = fmt("5.2f", cumtime_total > 0 ? cumtime_pr/cumtime_total*100.0 : 0.0)
    rmsd = fmt("10.4e", length(d) > 0 ? sqrt(norm(d)^2 / length(d)) : zero(T))
    rmsp = fmt("10.4e", sqrt(norm(pcur)^2 / length(pcur)))

    @info "Prop2DAcoVTIDenQ_DEO2_FDTD, $mode, time step $kt of $nt $mcells MCells/s (IO=$IO, EX=$EX, IM=$IM, PR=$PR) -- rms d,p; $rmsd $rmsp"
end

@inline function JopProp2DAcoVTIDenQ_DEO2_FDTD_write_history_nl(ginsu, it, ntmod, cumtime_total, cumtime_io, cumtime_ex, cumtime_pr, pcur, d::AbstractArray{T}) where {T}
    kt = fmt("5d", it)
    nt = fmt("5d", ntmod)
    mcells = fmt("7.2f", megacells_per_second(size(ginsu)..., it-1, cumtime_total))
    IO = fmt("5.2f", cumtime_total > 0 ? cumtime_io/cumtime_total*100.0 : 0.0)
    EX = fmt("5.2f", cumtime_total > 0 ? cumtime_ex/cumtime_total*100.0 : 0.0)
    PR = fmt("5.2f", cumtime_total > 0 ? cumtime_pr/cumtime_total*100.0 : 0.0)
    rmsd = fmt("10.4e", length(d) > 0 ? sqrt(norm(d)^2 / length(d)) : zero(T))
    rmsp = fmt("10.4e", sqrt(norm(pcur)^2 / length(pcur)))

    @info "Prop2DAcoVTIDenQ_DEO2_FDTD, nonlinear forward, time step $kt of $nt $mcells MCells/s (IO=$IO, EX=$EX, PR=$PR) -- rms d,p; $rmsd $rmsp"
end

function Base.close(jet::Jet{D,R,typeof(JopProp2DAcoVTIDenQ_DEO2_FDTD_f!)}) where {D,R}
    rm("$(state(jet).srcfieldfile)-pold", force=true)
    rm("$(state(jet).srcfieldfile)-mold", force=true)
    rm("$(state(jet).srcfieldfile)-pspace", force=true)
    rm("$(state(jet).srcfieldfile)-mspace", force=true)
end


using Compat
using KUnet
using HDF5,JLD
typealias LUP Union(Op,UpdateParam)
import Base.isequal

function isequal(l1::LUP, l2::LUP)
    for n in fieldnames(l1)
        isdefined(l1,n) || continue
        isdefined(l2,n) || return false
    end
    for n in fieldnames(l2)
        isdefined(l2,n) || continue
        isdefined(l1,n) || return false
        isequal(l1.(n),l2.(n)) || return false
    end
    return true
end

net=newnet(relu, 1326, 20000,10)
setparam!(net, learningRate=0.02, dropout=0.5)
save("foo.jld", net)
foo=newnet("foo.jld")
isequal(copy(net,:cpu),copy(foo,:cpu))


function FDSFactory(arg0::Field, arg1::jint, arg2::jint)
    return FDSFactory((Field, jint, jint), arg0, arg1, arg2)
end

function build(obj::FDSFactory, arg0::Vector{RealFieldElement})
    return jcall(obj, "build", FieldDerivativeStructure, (Vector{RealFieldElement},), arg0)
end

function build(obj::FDSFactory, arg0::Vector{jdouble})
    return jcall(obj, "build", FieldDerivativeStructure, (Vector{jdouble},), arg0)
end

function constant(obj::FDSFactory, arg0::RealFieldElement)
    return jcall(obj, "constant", FieldDerivativeStructure, (RealFieldElement,), arg0)
end

function constant(obj::FDSFactory, arg0::jdouble)
    return jcall(obj, "constant", FieldDerivativeStructure, (jdouble,), arg0)
end

function get_compiler(obj::FDSFactory)
    return jcall(obj, "getCompiler", DSCompiler, ())
end

function get_derivative_field(obj::FDSFactory)
    return jcall(obj, "getDerivativeField", Field, ())
end

function get_value_field(obj::FDSFactory)
    return jcall(obj, "getValueField", Field, ())
end

function variable(obj::FDSFactory, arg0::jint, arg1::RealFieldElement)
    return jcall(obj, "variable", FieldDerivativeStructure, (jint, RealFieldElement), arg0, arg1)
end

function variable(obj::FDSFactory, arg0::jint, arg1::jdouble)
    return jcall(obj, "variable", FieldDerivativeStructure, (jint, jdouble), arg0, arg1)
end


#=
startup.jl is a startup script which does several things:

1) Parses command line arguments
2) Loads parameters from given parameter file and constructs an array of parameters
3) Displays some messages

After running the script, the following variables are available:

const N_th::Int64 - the number of threads
const N_S::Int64 - number of aggregate states (parametrisations)
parfile::String - name of the parameter file
par::Array{pars{Float64,Int64},1} - parameters for each steady state
=#

#Get command line arguments, save them in parsed_args named tuple (global variable). Also save parfile - the name of the parameter file
using ArgParse #for parsing command line arguments.
#Load command line arguments. Move this to an included file later:
#Set up table of arguments passed by command line - see doc of ArgParse for details
s = ArgParseSettings()
@add_arg_table! s begin
    "--parfile"
        help = "parameter file name"
        arg_type = String
        default = ""
    "--numworkers"
        help = "number of workers"
        arg_type = Int
        default = 1
    "--slurm"
        help = "Use ClusterManagers for SLURM. 1 yes, 0 no (default)."
        arg_type = Int
        default = 0
end
#now get the parsed arguments
parsed_args = parse_args(ARGS, s)

#Copy the often used variables from the dictionary
parfile = parsed_args["parfile"]

#Intro message
const N_th = Threads.nthreads()
println("**************************************")
println("****** Brexit investment model *******")
println("**************************************\n")

println("Number of available threads: $N_th\n")
if N_th == 1
    println("Consider increasing the number of threads by 'export JULIA_NUM_THREADS=#'")
end

#Load parameters from file
if length(parfile) == 0
    println("No parameter file given. Using default parameter file baseline.jl.")
    parfile="baseline.jl"
end
include("../parameters/$parfile") #directory above
println("Parameters loaded from file parameters/$parfile\n")

#Number of aggregate states of the economy/parametrisations
const N_S = length(par_diff)

#Generate array par of parameters for different long-run equilibria
#using values of parameters from the parameter file.
expr = "par=["
for i=1:N_S
    global expr
   expr*="pars("
   if length(par_comm)>0 expr *= par_comm*"," end
   if length(par_diff)>0 expr *= par_diff[i] end
    expr*="),"
end
expr*="]"

#parse the string as a command and evaluate
expr2 = Meta.parse(expr)
eval(expr2)
expr = nothing;expr2 = nothing

#Check parameters (element-wise)
check_par.(par,N_S)

#Initialise results folder:
foldername = "results/"*genFolderName(parfile)
#Create results folder if it does not already exist
if !isdir("results")
    mkdir("results")
end
#Create the results/foldername folder. If it already exists, an error will occur
#(This could happen if the program is executed multiple times within a single second so the folder names are the same).
mkdir(foldername)
#Copy patemeter file in the results folder (for reproducibility)
cp("parameters/$parfile","$foldername/$parfile")

#Print a message about parameters:
println("*********Summary of parameters:*************")
println("All baseline parameters (pre-Brexit):")
@show par[1] #par[1] is the baseline (pre-Brexit)
println("_______________________")
println("Parameters that differ between stationary equilibria:")
@show par_diff
println(" ")
println("********************************************")


export RawAcquisitionData, EncodingCounters, AcquisitionHeader

"""
Encoding counters used in each Profile of a RawAcquisitionData object.
"""
Base.@kwdef struct EncodingCounters
  kspace_encode_step_1::Int16 = 0
  kspace_encode_step_2::Int16 = 0
  average::Int16 = 0
  slice::Int16 = 0
  contrast::Int16 = 0
  phase::Int16 = 0
  repetition::Int16 = 0
  set::Int16 = 0
  segment::Int16 = 0
  user::NTuple{8,Int16} = ntuple(i->Int16(0),8)
end

"""
Encoding counters used in each Profile of a RawAcquisitionData object.
"""
Base.@kwdef struct AcquisitionHeader
  version::Int16 = 0
  flags::Int64 = 0
  measurement_uid::Int32 = 0
  scan_counter::Int32 = 0
  acquisition_time_stamp::Int32 = 0
  physiology_time_stamp::NTuple{3,Int32} = ntuple(i->Int32(0),3)
  number_of_samples::Int16 = 0
  available_channels::Int16 = 0
  active_channels::Int16 = 0
  channel_mask::NTuple{16,Int64} = ntuple(i->Int64(0),16)
  discard_pre::Int16 = 0
  discard_post::Int16 = 0
  center_sample::Int16 = 0
  encoding_space_ref::Int16 = 0
  trajectory_dimensions::Int16 = 0
  sample_time_us::Float32 = 0.0
  position::NTuple{3,Float32} = ntuple(i->Float32(0),3)
  read_dir::NTuple{3,Float32} = ntuple(i->Float32(0),3)
  phase_dir::NTuple{3,Float32} = ntuple(i->Float32(0),3)
  slice_dir::NTuple{3,Float32} = ntuple(i->Float32(0),3)
  patient_table_position::NTuple{3,Float32} = ntuple(i->Float32(0),3)
  idx::EncodingCounters = EncodingCounters()
  user_int::NTuple{8,Int32} = ntuple(i->Int32(0),8)
  user_float::NTuple{8,Float32} = ntuple(i->Float32(0),8)
end

"""
Struct to describe data from one profile of a trajectory (from all coils).
"""
mutable struct Profile
  head::AcquisitionHeader
  traj::Array{Float32,2}
  data::Array{Complex{Float32},2}
end

"""
RawAcquisitionData object.

# Fields
* `params::Dict{String, Any}` - Dict containing the information of the XML header in ISMRMRD
* `profiles::Vector{Profile}` - Vector containing all the profiles of the acquisition

"""
mutable struct RawAcquisitionData
  params::Dict{String, Any}
  profiles::Vector{Profile}
end

"""
  trajectory(f::RawAcquisitionData; slice::Int=1, contrast::Int=1)

returns the `Trajectory` for given `slice` and `contrast` of a `RawAcquisitionData` object.
"""
function trajectory(f::RawAcquisitionData; slice::Int=1, contrast::Int=1)
  name = get(f.params, "trajectory","cartesian")
  if lowercase(name) == "cartesian"
    dim = ( maximum(encSteps2(f))==1 ? 2 : 3)
    if dim==3
      tr = trajectory("Cartesian3D", f.params["encodedSize"][2],
                                     f.params["encodedSize"][1],
                                     numSlices=f.params["encodedSize"][3])
    else
      tr = trajectory("Cartesian", f.params["encodedSize"][2],
                                   f.params["encodedSize"][1])
    end
  else
    name  = f.params["trajectory"]
    encSt1 = encSteps1(f)
    encSt2 = encSteps2(f)
    sl = slices(f)
    rep = repetitions(f)

    numSl = length(unique(sl))
    numRep = length(unique(rep))
    numEncSt1 = length(unique(encSt1))
    numEncSt2 = length(unique(encSt2))

    # assume constant number of samplings per profile
    numSampPerProfile = size(f.profiles[1].data,1)
    numChan = size(f.profiles[1].data,2)
    D = Int(f.profiles[1].head.trajectory_dimensions)

    # remove data that should be discarded
    i1 = f.profiles[1].head.discard_pre + 1
    i2 = numSampPerProfile - f.profiles[1].head.discard_post

    traj = zeros(Float32, D, length(i1:i2), numEncSt1, numEncSt2, numSl, numRep)
    times = zeros(Float32, length(i1:i2), numEncSt1, numEncSt2, numSl, numRep)

    for l=1:length(f.profiles)
      if f.profiles[l].head.idx.slice+1 != slice || f.profiles[l].head.idx.contrast+1 != contrast
        continue
      end
      traj[:, :, encSt1[l], encSt2[l], sl[l]-minimum(sl)+1, rep[l]-minimum(rep)+1] .= f.profiles[l].traj[:,i1:i2]
      dt = f.profiles[l].head.sample_time_us*1e-6
      times[:, encSt1[l], encSt2[l], sl[l]-minimum(sl)+1, rep[l]-minimum(rep)+1] .= 0:dt:(length(i1:i2)-1)*dt
    end

    traj_ = reshape(traj[:,:,:,:,1,1], D, :)
    # tr = Trajectory(traj_, size(traj_,3), size(traj_,2), circular=true)
    tr = Trajectory(traj_, size(traj_,3), size(traj_,2), circular=true, times=vec(times[:,:,:,1,1]))
  end
  return tr
end

function sequence(f::RawAcquisitionData)
  # TODO
end

"""
    numChannels(f::RawAcquisitionData)

returns the number of channels in a `RawAcquisitionData` object.
"""
function numChannels(f::RawAcquisitionData)
  return f.profiles[1].head.active_channels
end

encSteps1(f::RawAcquisitionData) =
   [f.profiles[l].head.idx.kspace_encode_step_1+1 for l=1:length(f.profiles)]
encSteps2(f::RawAcquisitionData) =
   [f.profiles[l].head.idx.kspace_encode_step_2+1 for l=1:length(f.profiles)]
slices(f::RawAcquisitionData) =
   [f.profiles[l].head.idx.slice+1 for l=1:length(f.profiles)]
repetitions(f::RawAcquisitionData) =
   [f.profiles[l].head.idx.repetition+1 for l=1:length(f.profiles)]
contrasts(f::RawAcquisitionData) =
  [f.profiles[l].head.idx.contrast+1 for l=1:length(f.profiles)]

  """
    subsampleIndices(f::RawAcquisitionData; slice::Int=1, contrast::Int=1)

  returns the sampled indices for a given `slice` and `contrast` in a `RawAcquisitionData` object.
  """
function subsampleIndices(f::RawAcquisitionData; slice::Int=1, contrast::Int=1, estimateProfileCenter::Bool=false)
  idx = Int64[]
  encSt1 = encSteps1(f)
  encSt2 = encSteps2(f)
  numEncSamp, numProf, numSl = f.params["encodedSize"]
  for i=1:length(f.profiles)
    # only consider data for the specified slice, contrast and repetition
    if f.profiles[i].head.idx.slice+1 != slice || f.profiles[i].head.idx.contrast+1 != contrast || f.profiles[i].head.idx.repetition != 0
      continue
    end
    numSamp = size(f.profiles[i].data,1)  # number of measured points
    if estimateProfileCenter
      center_sample = div(numSamp,2)+1
    else
      center_sample = f.profiles[i].head.center_sample+1 # idx of the center trajectory in the measurement
    end
    tr_center_idx = div(numEncSamp,2)+1 # center of the encoded trajectory

    # start and end indices of sampled profile
    i1 = tr_center_idx - center_sample + 1 + f.profiles[i].head.discard_pre
    i2 = tr_center_idx - center_sample + numSamp - f.profiles[i].head.discard_post
    # convert to linear index
    lineIdx = collect(i1:i2) .+ numSamp*((encSt2[i]-1)*numProf + (encSt1[i]-1))
    append!(idx, lineIdx)
  end

  return unique(idx)
end

"""
    rawdata(f::RawAcquisitionData)

returns the rawdata contained `RawAcquisitionData` object.
The output is an `Array{Matrix{ComplexF64},3}`, which can be stored in a `AcquisitionData` object.
"""
function rawdata(f::RawAcquisitionData; slice::Int=1, contrast::Int=1, repetition::Int=1)

  # find profiles corresponding to the given slice contrast and repitiion
  sl = slices(f)
  rep = repetitions(f)
  contr = contrasts(f)
  idx_sl = findall(x->x==slice,sl)
  idx_contr = findall(x->x==contrast,contr)
  idx_rep = findall(x->x==repetition,rep)
  idx = intersect(idx_sl,idx_contr,idx_rep)

  # number of unique combination of encoding statuses
  encSt1 = encSteps1(f)[idx]
  encSt2 = encSteps2(f)[idx]
  # find first occurence of each combination of encoding statuses (as `unique` returns)
  idx_unique = uniqueidx(hcat(encSt1,encSt2))
  numEncSt = length(idx_unique)

  # allocate space for k-space data
  # assume the same number of samples for all profiles
  numSampPerProfile, numChan = size(f.profiles[idx[1]].data)
  numSampPerProfile -= (f.profiles[idx[1]].head.discard_pre+f.profiles[idx[1]].head.discard_post)
  kdata = zeros(ComplexF64, numSampPerProfile, numEncSt, numChan)

  # store one profile (kspace data) for each unique encoding status
  cnt = 1
  for l in idx[idx_unique] # TODO: set of profiles (with unique encoding status)
    i1 = f.profiles[l].head.discard_pre + 1
    i2 = i1+numSampPerProfile-1
    kdata[:,cnt,:] .= ComplexF64.(f.profiles[l].data[i1:i2, :])
    cnt += 1
  end

  return reshape(kdata, :, numChan)
end

"""
    AcquisitionData(f::RawAcquisitionData; estimateProfileCenter::Bool=false)

converts `RawAcquisitionData` into the equivalent `AcquisitionData` object.
"""
function AcquisitionData(f::RawAcquisitionData; estimateProfileCenter::Bool=false)
  contrs = sort(unique(contrasts(f)))
  sls = sort(unique(slices(f)))
  reps = sort(unique(repetitions(f)))
  numContr = length(unique(contrasts(f)))
  numSl = length(unique(slices(f)))
  numRep = length(unique(repetitions(f)))
  # tr = [trajectory(f,contrast=contr) for contr=1:numContr]
  # subsampleIdx = [subsampleIndices(f,contrast=contr,estimateProfileCenter=estimateProfileCenter) for contr=1:numContr]
  # kdata = [rawdata(f; contrast=i, slice=j, repetition=k) for i=1:numContr, j=1:numSl, k=1:numRep]
  tr = [trajectory(f,contrast=contr) for contr=contrs]
  subsampleIdx = [subsampleIndices(f,contrast=contr,estimateProfileCenter=estimateProfileCenter) for contr=contrs]
  kdata = [rawdata(f; contrast=i, slice=j, repetition=k) for i=contrs, j=sls, k=reps]

  return AcquisitionData(tr, kdata,
                          idx=subsampleIdx,
                          encodingSize=collect(f.params["encodedSize"]),
                          fov = collect(f.params["encodedFOV"]) )
end

"""
    RawAcquisitionData(acqData::AcquisitionData)

converts `acqData` into the equivalent `RawAcquisitionData` object.
"""
function RawAcquisitionData(acqData::AcquisitionData)
  # XML header
  params = minimalHeader(Tuple(acqData.encodingSize),Tuple(acqData.fov),tr_name=string(trajectory(acqData,1)))
  # acquisition counter
  counter = 1
  # profiles
  profiles = Vector{Profile}()
  for rep = 1:numRepititions(acqData)
    for slice=1:numSlices(acqData)
      for contr = 1:numContrasts(acqData)
        tr = trajectory(acqData,contr)
        profIdx = profileIdx(acqData,contr)
        numSamp = numSamplingPerProfile(tr)
        numProf = div(length(acqData.subsampleIndices[contr]),numSamp)
          for prof_tr = 1:length(profIdx)
            for slice_tr = 1:numSlices(tr)
              head = AcquisitionHeader(acqData,rep,slice,slice_tr,contr,profIdx[prof_tr],counter)
              nodes = profileNodes(tr,prof_tr,slice_tr)
              if dims(tr)==2
                data = profileData(acqData,contr,slice,rep,prof_tr)
              else
                data = profileData(acqData,contr,slice_tr,rep,prof_tr)
              end
              profile = Profile( head, Float32.(nodes), Complex{Float32}.(data) )
              push!( profiles, profile )
              counter += 1
            end
          end
      end
    end
  end

  return RawAcquisitionData(params,profiles)
end

function AcquisitionHeader(acqData::AcquisitionData, rep::Int, slice::Int, slice_tr::Int
                            , contr::Int, prof_tr::Int, counter::Int
                            ; phase::Int=1, set::Int=1, segment::Int=1, repetition::Int=1
                            , user::NTuple{8,Int16} = ntuple(i->Int16(0),8), kargs...)
  numChan = numChannels(acqData)
  tr = trajectory(acqData,contr)
  numSamples = numSamplingPerProfile(tr)
  isCartesian(tr) ? center_sample=div(numSamples,2) : center_sample=0 # needs to be fixed for partial Fourier,etc
  tr_dims = dims(tr)
  sample_time_us = acqTimePerProfile(tr)/numSamples*1.e6

  idx = EncodingCounters( Int16(prof_tr-1)
                        , Int16(slice_tr-1)
                        , Int16(rep-1)
                        , Int16(slice-1)
                        , Int16(contr-1)
                        , Int16(phase-1)
                        , Int16(repetition-1)
                        , Int16(set-1)
                        , Int16(segment-1)
                        , user )

  return AcquisitionHeader(; scan_counter=Int32(counter)
                          , number_of_samples=Int16(numSamples)
                          , available_channels=Int16(numChan)
                          , active_channels=Int16(numChan)
                          , center_sample=Int16(center_sample)
                          , trajectory_dimensions=Int16(tr_dims)
                          , sample_time_us=Float32(sample_time_us)
                          , read_dir=Float32.((1,0,0))
                          , phase_dir=Float32.((0,1,0))
                          , slice_dir=Float32.((0,0,1))
                          , idx=idx )
end

# get indices of the first occurence of unique elements in the matrix x
function uniqueidx(x::Matrix{T}) where T
  uniqueset = Set{Vector{T}}()
  ex = eachindex(x[:,1])
  idxs = Vector{eltype(ex)}()
  for i in ex
      xi = x[i,:]
      if !(xi in uniqueset)
          push!(idxs, i)
          push!(uniqueset, xi)
      end
  end
  idxs
end


__precompile__()
"""
Module for defining and checking properties.
"""
module Properties

using LazySets

import LazySets.dim

# ==============================
# Property struct and evaluation
# ==============================
include("Property.jl")
export Property,
       dim,
       check

include("Conjunction.jl")
include("Disjunction.jl")
export Conjunction,
       Disjunction

include("BadStatesProperty.jl")
export BadStatesProperty

include("SafeStatesProperty.jl")
export SafeStatesProperty

end # module


@testset "543.diameter-of-binary-tree.jl" begin
    t = convert(TreeNode{Int}, [1, 2, 3, 4, 5])
    @test diameter_of_binary_tree(t) == 3
end


#
# Parallel thread spliiter
#
splitter(first,nbatches,n) = first:nbatches:n

"""

```
reduce(output, output_threaded)
```

Functions to reduce the output of common options (vectors of numbers
and vectors of vectors). This function can be replacted by custom
reduction methods. It always must both receive the `output` variable
as a parameter, and return it at the end.

"""
reduce(output::Number, output_threaded::Vector{<:Number}) = sum(output_threaded)
function reduce(output::AbstractVector, output_threaded::AbstractVector{<:AbstractVector})
    @. output = output_threaded[1]
    for ibatch in 2:length(output_threaded)
        @. output += output_threaded[ibatch]
    end
    return output
end

#
# Serial version for self-pairwise computations
#
function map_pairwise_serial!(
    f::F, output, box::Box, cl::CellList{N,T};
    show_progress::Bool=false
) where {F,N,T}
    @unpack n_cells_with_real_particles = cl
    show_progress && (p = Progress(n_cells_with_real_particles, dt=1))
    ibatch = 1
    for i in 1:n_cells_with_real_particles
        cellᵢ = cl.cells[cl.cell_indices_real[i]]
        output = inner_loop!(f, box, cellᵢ, cl, output, ibatch)
        show_progress && next!(p)
    end
    return output
end

#
# Parallel version for self-pairwise computations
#
function map_pairwise_parallel!(
    f::F1, output, box::Box, cl::CellList{N,T};
    output_threaded=output_threaded,
    reduce::F2=reduce,
    show_progress::Bool=false
) where {F1,F2,N,T}
    @unpack n_cells_with_real_particles = cl
    show_progress && (p = Progress(n_cells_with_real_particles, dt=1))
    nbatches = cl.nbatches.map_computation
    @sync for ibatch in 1:nbatches
        Threads.@spawn begin
            for i in splitter(ibatch, nbatches, n_cells_with_real_particles)
                cellᵢ = cl.cells[cl.cell_indices_real[i]]
                output_threaded[ibatch] =
                    inner_loop!(f, box, cellᵢ, cl, output_threaded[ibatch], ibatch)
                show_progress && next!(p)
            end
        end
    end
    output = reduce(output, output_threaded)
    return output
end

"""

```
partition!(x::AbstractVector,by)
```

Internal function or structure - interface may change.

# Extended help

Function that reorders `x` vector by putting in the first positions the
elements with values satisfying `by(el)`. Returns the number of elements
that satisfy the condition.

"""
function partition!(by, x::AbstractVector)
    iswap = 1
    @inbounds for i in eachindex(x)
        if by(x[i])
            if iswap != i
                x[iswap], x[i] = x[i], x[iswap]
            end
            iswap += 1
        end
    end
    return iswap - 1
end

#
# Serial version for cross-interaction computations
#
function map_pairwise_serial!(
    f::F, output, box::Box,
    cl::CellListPair{N,T};
    show_progress=show_progress
) where {F,N,T}
    show_progress && (p = Progress(length(cl.ref), dt=0))
    for i in eachindex(cl.ref)
        output = inner_loop!(f, output, i, box, cl)
        show_progress && next!(p)
    end
    return output
end

#
# Parallel version for cross-interaction computations
#
function map_pairwise_parallel!(
    f::F1, output, box::Box,
    cl::CellListPair{N,T};
    output_threaded=output_threaded,
    reduce::F2=reduce,
    show_progress=show_progress
) where {F1,F2,N,T}
    show_progress && (p = Progress(length(cl.ref), dt=1))
    nbatches = cl.target.nbatches.map_computation
    @sync for ibatch in 1:nbatches
        Threads.@spawn begin
            for i in splitter(ibatch, nbatches, length(cl.ref))
                output_threaded[ibatch] = inner_loop!(f, output_threaded[ibatch], i, box, cl)
                show_progress && next!(p)
            end
        end
    end
    output = reduce(output, output_threaded)
    return output
end

#
# Inner loop for Orthorhombic cells is faster because we can guarantee that
# there are not repeated computations even if running over half of the cells.
#
function inner_loop!(
    f,box::Box{TriclinicCell},cellᵢ,
    cl::CellList{N,T},
    output,
    ibatch
) where {N,T}
    @unpack cutoff, cutoff_sq, nc = box

    for neighbor_cell in neighbor_cells(box)
        jc_cartesian = cellᵢ.cartesian_index + neighbor_cell
        jc_linear = cell_linear_index(nc,jc_cartesian)
        # if cellⱼ is empty, cycle
        if cl.cell_indices[jc_linear] == 0
            continue
        end
        cellⱼ = cl.cells[cl.cell_indices[jc_linear]]

        # same cell
        if cellⱼ.linear_index == cellᵢ.linear_index
            for i in 1:cellᵢ.n_particles
                @inbounds pᵢ = cellᵢ.particles[i]
                (!pᵢ.real) && continue
                xpᵢ = pᵢ.coordinates
                for j in 1:cellᵢ.n_particles
                    @inbounds pⱼ = cellᵢ.particles[j]
                    if pᵢ.index < pⱼ.index
                        xpⱼ = pⱼ.coordinates
                        d2 = norm_sqr(xpᵢ - xpⱼ)
                        if d2 <= cutoff_sq
                            output = f(xpᵢ, xpⱼ, pᵢ.index, pⱼ.index, d2, output)
                        end
                    end
                end
            end
        # neighbor cells
        else
            # Vector connecting cell centers
            Δc = cellⱼ.center - cellᵢ.center
            Δc_norm = norm(Δc)
            Δc = Δc / Δc_norm
            pp = project_particles!(cl.projected_particles[ibatch],cellⱼ,cellᵢ,Δc,Δc_norm,box)
            for i in 1:cellᵢ.n_particles
                @inbounds pᵢ = cellᵢ.particles[i]
                (!pᵢ.real) && continue
                xpᵢ = pᵢ.coordinates

                # Partition pp array according to the current projections. This
                # is faster than it looks, because after the first partitioning, the
                # array will be almost correct, and essentially the algorithm
                # will only run over most elements. Avoiding this partitioning or
                # trying to sort the array before always resulted to be much more
                # expensive.
                xproj = dot(xpᵢ - cellᵢ.center, Δc)
                n = partition!(el -> abs(el.xproj - xproj) <= cutoff, pp)

                for j in 1:n
                    @inbounds pⱼ = pp[j]
                    if pᵢ.index < pⱼ.index
                        xpⱼ = pⱼ.coordinates
                        d2 = norm_sqr(xpᵢ - xpⱼ)
                        if d2 <= cutoff_sq
                            output = f(xpᵢ, xpⱼ, pᵢ.index, pⱼ.index, d2, output)
                        end
                    end
                end
            end
        end
    end

    return output
end

#
# Inner loop for Orthorhombic cells is faster because we can guarantee that
# there are not repeated computations even if running over half of the cells.
#
function inner_loop!(
    f,box::Box{OrthorhombicCell},cellᵢ,
    cl::CellList{N,T},
    output,
    ibatch
) where {N,T}
    @unpack cutoff_sq = box

    # loop over list of non-repeated particles of cell ic
    for i in 1:cellᵢ.n_particles - 1
        @inbounds pᵢ = cellᵢ.particles[i]
        xpᵢ = pᵢ.coordinates
        for j in i + 1:cellᵢ.n_particles
            @inbounds pⱼ = cellᵢ.particles[j]
            xpⱼ = pⱼ.coordinates
            d2 = norm_sqr(xpᵢ - xpⱼ)
            if d2 <= cutoff_sq
                output = f(xpᵢ, xpⱼ, pᵢ.index, pⱼ.index, d2, output)
            end
        end
    end

    for jcell in neighbor_cells_forward(box)
        jc_linear = cell_linear_index(box.nc,cellᵢ.cartesian_index + jcell)
        if cl.cell_indices[jc_linear] != 0
            cellⱼ = cl.cells[cl.cell_indices[jc_linear]]
            output = cell_output!(f, box, cellᵢ, cellⱼ, cl, output, ibatch)
        end
    end

    return output
end

function cell_output!(
    f,
    box::Box{OrthorhombicCell},
    cellᵢ,
    cellⱼ,
    cl::CellList{N,T},
    output,
    ibatch
) where {N,T}
    @unpack cutoff, cutoff_sq, nc = box

    # Vector connecting cell centers
    Δc = cellⱼ.center - cellᵢ.center
    Δc_norm = norm(Δc)
    Δc = Δc / Δc_norm
    pp = project_particles!(cl.projected_particles[ibatch],cellⱼ,cellᵢ,Δc,Δc_norm,box)
    if length(pp) == 0
        return output
    end

    # Loop over particles of cell icell
    for i in 1:cellᵢ.n_particles
        @inbounds pᵢ = cellᵢ.particles[i]
        xpᵢ = pᵢ.coordinates
        xproj = dot(xpᵢ - cellᵢ.center, Δc)

        # Partition pp array according to the current projections
        n = partition!(el -> abs(el.xproj - xproj) <= cutoff, pp)

        # Compute the interactions
        for j in 1:n
            @inbounds pⱼ = pp[j]
            xpⱼ = pⱼ.coordinates
            d2 = norm_sqr(xpᵢ - xpⱼ)
            if d2 <= cutoff_sq
                output = f(xpᵢ, xpⱼ, pᵢ.index, pⱼ.index, d2, output)
            end
        end
    end

    return output
end

"""

```
project_particles!(projected_particles,cellⱼ,cellᵢ,Δc,Δc_norm,box)
```

Internal function or structure - interface may change.

# Extended help

Projects all particles of the cell `cellⱼ` into unnitary vector `Δc` with direction
connecting the centers of `cellⱼ` and `cellᵢ`. Modifies `projected_particles`, and
returns a view of `projected particles, where only the particles for which
the projection on the direction of the cell centers still allows the particle
to be within the cutoff distance of any point of the other cell.

"""
function project_particles!(
    projected_particles,cellⱼ,cellᵢ,
    Δc,Δc_norm,box::Box{UnitCellType,N}
) where {UnitCellType,N}
    if box.lcell == 1
        margin = box.cutoff + Δc_norm/2 # half of the distance between centers
    else
        margin = box.cutoff*(1 + sqrt(N)/2) # half of the diagonal of the cutoff-box
    end
    iproj = 0
    @inbounds for j in 1:cellⱼ.n_particles
        pⱼ = cellⱼ.particles[j]
        xproj = dot(pⱼ.coordinates - cellᵢ.center, Δc)
        if abs(xproj) <= margin
            iproj += 1
            projected_particles[iproj] = ProjectedParticle(pⱼ.index, xproj, pⱼ.coordinates)
        end
    end
    pp = @view(projected_particles[1:iproj])
    return pp
end

#
# Inner loop of cross-interaction computations. If the two sets
# are large, this might(?) be improved by computing two separate
# cell lists and using projection and partitioning.
#
function inner_loop!(
    f,output,i,box,
    cl::CellListPair{N,T}
) where {N,T}
    @unpack nc, cutoff_sq = box
    xpᵢ = wrap_to_first(cl.ref[i], box)
    ic = particle_cell(xpᵢ, box)
    for neighbor_cell in neighbor_cells(box)
        jc_cartesian = neighbor_cell + ic
        jc_linear = cell_linear_index(nc,jc_cartesian)
        # If cellⱼ is empty, cycle
        if cl.target.cell_indices[jc_linear] == 0
            continue
        end
        cellⱼ = cl.target.cells[cl.target.cell_indices[jc_linear]]
        # loop over particles of cellⱼ
        for j in 1:cellⱼ.n_particles
            @inbounds pⱼ = cellⱼ.particles[j]
            xpⱼ = pⱼ.coordinates
            d2 = norm_sqr(xpᵢ - xpⱼ)
            if d2 <= cutoff_sq
                output = f(xpᵢ, xpⱼ, i, pⱼ.index, d2, output)
            end
        end
    end
    return output
end


module SignalView

using GeometryTypes, Reactive, GLVisualize, GLAbstraction, Colors, GLWindow
import GraphLayout
import GraphLayout: AdjList

include("graphlayout.jl")
include("view.jl")

export view_signal

end


using PkgSRA, Test

@testset "sindy_turing_test.jl" begin
    include("sindy_turing_test.jl")
end
@testset "sindy_test.jl" begin
    include("sindy_test.jl")
end
@testset "regression_utils_test.jl" begin
    include("regression_utils_test.jl")
end


const PrecisionPiece{T} = Tuple{Vector{Int64}, Vector{Int64}, Vector{T}}

abstract type AbstractVecchiaConfig{H,D,F} end

# TODO (cg 2021/04/25 13:06): should these fields chunksize and blockrank be in
# here? Arguably the are redundant and encoded in the data/pts/condix values.
# And having them sort of provides a dangerously easy option to not check and
# make sure what those sizes really need to be.
struct VecchiaConfig{H,D,F} <: AbstractVecchiaConfig{H,D,F}
  chunksize::Int64
  blockrank::Int64
  kernel::F
  data::Vector{Matrix{H}}
  pts::Vector{Vector{SVector{D, Float64}}}
  condix::Vector{Vector{Int64}}
end

function Base.display(V::VecchiaConfig)
  println("Vecchia configuration with:")
  println("chunksize:  $(V.chunksize)")
  println("block rank: $(V.blockrank)")
  println("data size:  $(sum(x->size(x,1), V.data))")
  println("nsamples:   $(size(V.data[1], 2))")
end

# TODO (cg 2021/04/25 13:06): should these fields chunksize and blockrank be in
# here? Arguably the are redundant and encoded in the data/pts/condix values.
# And having them sort of provides a dangerously easy option to not check and
# make sure what those sizes really need to be.
struct ScalarVecchiaConfig{H,D,F} <: AbstractVecchiaConfig{H,D,F}
  chunksize::Int64
  blockrank::Int64
  kernel::F
  data::Vector{Matrix{H}}
  pts::Vector{Vector{Float64}}
  condix::Vector{Vector{Int64}}
end

function Base.display(V::ScalarVecchiaConfig)
  println("Scalarized Vecchia configuration with:")
  println("chunksize:  $(V.chunksize)")
  println("block rank: $(V.blockrank)")
  println("data size:  $(sum(x->size(x,1), V.data))")
  println("nsamples:   $(size(V.data[1], 2))")
end

struct RCholesky{T}
  diagonals::Vector{UpperTriangular{T,Matrix{T}}}
  odiagonals::Vector{Matrix{T}}
  condix::Vector{Vector{Int64}}
  idxs::Vector{UnitRange{Int64}}
  is_instantiated::Bool
end

# TODO (cg 2022/05/30 12:10): make this more information.
Base.display(U::RCholesky{T}) where{T} = println("RCholesky{$T}")
Base.display(Ut::Adjoint{T,RCholesky{T}}) where{T} = println("Adjoint{$T, RCholesky{$T}}")

struct MemoizedKernel{F,T}
  cache::Dict{Tuple{UInt64, UInt64},T}
  phash::UInt64
  kernel::F
end

# Not good code or anything. Just a quick and dirty way to make a Vecchia object
# with a KD-tree to choose the conditioning points.
function kdtreeconfig(data, pts, chunksize, blockrank, kfun)
  (data isa Vector) && return kdtreeconfig(hcat(data), pts, chunksize, blockrank, kfun)
  # Make a KDTree of the points with a certain leaf size
  tree    = KDTree(pts, leafsize=chunksize)
  # re-order the data accordingly:
  _d      = Dict(zip(pts, eachrow(data)))
  data_p  = [_d[xj] for xj in tree.data]
  # Get the data in chunks of size $chunksize.
  pts_out = map(copy, Iterators.partition(tree.data, chunksize))
  # TODO (cg 2022/04/21 16:17): The inner function here (vec_of_...), really
  # could use some optimization and is kind of just a stand-in at the moment. So
  # long as this constructor isn't in the hot loop of an optimizer it's probably
  # fine as-is, but just a note that there is some perf on the table here.
  dat_out = map(vec_of_vecs_to_matrows, Iterators.partition(data_p, chunksize))
  # re-order the centroids of THOSE leaves.
  centroids = map(x->sum(x)/length(x), pts_out)
  c_ix_dict = Dict(zip(centroids, eachindex(centroids)))
  c_tree    = KDTree(centroids)
  perm      = [c_ix_dict[x] for x in c_tree.data]
  @assert sort(perm) == collect(eachindex(centroids)) "debug test."
  pts_out   = pts_out[perm]
  dat_out   = dat_out[perm]
  # Create the conditioning meta-indices for the chunks.
  condix  = map(j->cond_ixs(j,blockrank), eachindex(pts_out))
  (H,D,F) = (eltype(data), length(first(pts)), typeof(kfun))
  VecchiaConfig{H,D,F}(min(chunksize, length(first(pts_out))),
                       min(blockrank, length(pts_out)),
                       kfun, dat_out, pts_out, condix)
end

# Even less good code, but here's a simple connection between Vecchia and the
# H-matrix with global Nystrom that I've worked on earlier: add a single
# collection of "global" points to the conditioning set of all subsequent
# points. Pretty easy to do with a couple extra tree constructions.
function nystrom_kdtreeconfig(data, pts, chunksize, blockrank, kfun, nys_size)
  (data isa Vector) && return nystrom_kdtreeconfig(hcat(data), pts, chunksize,
                                                   blockrank, kfun, nys_size)
  # Make a KDTree of the points with a certain leaf size
  tree   = KDTree(pts, leafsize=chunksize)
  _d     = Dict(zip(pts, eachrow(data)))
  data_p = reshape(reduce(vcat, [_d[xj] for xj in tree.data]), :, size(data,2))
  # Pick the "nystrom points" in the simplest way:
  nys_inds = 1:div(length(pts), nys_size):length(pts)
  nys_pts  = tree.data[nys_inds]
  # now for the rest of the points, do the ordering above:
  _cfg     = kdtreeconfig(data_p, tree.data[setdiff(1:length(pts), nys_inds)],
            chunksize, blockrank, kfun)
  # extract and modify the fields we actually want:
  pts_out  = vcat([nys_pts], _cfg.pts)
  nys_dat  = reshape(reduce(vcat, data_p[nys_inds]), :, size(data, 2))
  dat_out  = pushfirst!(_cfg.data, nys_dat)
  condix   = map(j->cond_ixs(j,blockrank), eachindex(pts_out))
  for j in 2:length(condix)
    cixj = condix[j]
    if !in(1, cixj)
      pushfirst!(cixj, 1)
    end
  end
  (H,D,F) = (eltype(data), length(first(pts)), typeof(kfun))
  VecchiaConfig{H, D,F}(min(chunksize+nys_size, length(first(pts_out))),
                        min(blockrank+1, length(pts_out)),
                        kfun, dat_out, pts_out, condix)
end

function scalarize(v::VecchiaConfig{H,D,F}, scalarized_kernel::G) where{H,D,F,G}
  scalarized_pts = map(x->reduce(vcat, x), v.pts)
  ScalarVecchiaConfig{H,D,G}(v.chunksize,
                             v.blockrank,
                             scalarized_kernel,
                             v.data,
                             scalarized_pts,
                             v.condix)
end

function MemoizedKernel(f, sample_x, p)
  f11 = f(sample_x, sample_x, p)
  ph  = hash(p)
  cache = Dict{Tuple{UInt64, UInt64},typeof(f11)}()
  MemoizedKernel(cache, ph, f)
end

function (k::MemoizedKernel{F,T})(x, y, p) where{F,T}
  _key = (hash(x), hash(y))
  if haskey(k.cache, _key)
    return k.cache[_key]
  else
    _val = k.kernel(x,y,p)
    k.cache[_key] = _val
    return _val
  end
end


using ParametricOptInterface
using BenchmarkTools
using JuMP
using LinearAlgebra
const POI = ParametricOptInterface
import Pkg

function moi_add_variables(N::Int)
    model = direct_model(
        MOI.Utilities.CachingOptimizer(
            MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
            MOI.Utilities.AUTOMATIC,
        ),
    )
    @variable(model, x[i = 1:N])
    return nothing
end

function poi_add_variables(N::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:N])
    return nothing
end

function poi_add_parameters(N::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:N] in POI.Parameter(1))
    return nothing
end

function poi_add_parameters_and_variables(N::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:N] in POI.Parameter(1))
    return nothing
end

function poi_add_parameters_and_variables_alternating(N::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    for i in 1:Int(N / 2)
        @variable(model)
        @variable(model, set = POI.Parameter(1))
    end
    return nothing
end

function moi_add_saf_ctr(N::Int, M::Int)
    model = direct_model(
        MOI.Utilities.CachingOptimizer(
            MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
            MOI.Utilities.AUTOMATIC,
        ),
    )
    @variable(model, x[i = 1:N])
    @constraint(model, cons[i = 1:M], sum(x) >= 1)
    return nothing
end

function poi_add_saf_ctr(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:N])
    @constraint(model, cons[i = 1:M], sum(x) >= 1)
    return nothing
end

function poi_add_saf_variables_and_parameters_ctr(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:Int(N / 2)])
    @variable(model, p[i = 1:Int(N / 2)] in POI.Parameter.(0))
    @constraint(model, con[i = 1:M], sum(x) + sum(p) >= 1)
    return nothing
end

function poi_add_saf_variables_and_parameters_ctr_parameter_update(
    N::Int,
    M::Int,
)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:Int(N / 2)])
    @variable(model, p[i = 1:Int(N / 2)] in POI.Parameter.(0))
    @constraint(model, con[i = 1:M], sum(x) + sum(p) >= 1)
    MOI.set.(model, POI.ParameterValue(), p, 0.5)
    POI.update_parameters!(backend(model))
    return nothing
end

function moi_add_sqf_variables_ctr(N::Int, M::Int)
    model = direct_model(
        MOI.Utilities.CachingOptimizer(
            MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
            MOI.Utilities.AUTOMATIC,
        ),
    )
    @variable(model, x[i = 1:N])
    @constraint(model, con[i = 1:M], dot(x , x) >= 1)
    return nothing
end

function poi_add_sqf_variables_ctr(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:N])
    @constraint(model, con[i = 1:M], dot(x, x) >= 1)
    return nothing
end

function poi_add_sqf_variables_parameters_ctr(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:Int(N / 2)])
    @variable(model, p[i = 1:Int(N / 2)] in POI.Parameter.(1))
    @constraint(model, con[i = 1:M], dot(x , p) >= 1)
    return nothing
end

function poi_add_sqf_variables_parameters_ctr_parameter_update(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:Int(N / 2)])
    @variable(model, p[i = 1:Int(N / 2)] in POI.Parameter.(1))
    @constraint(model, con[i = 1:M], dot(x , p) >= 1)
    MOI.set.(model, POI.ParameterValue(), p, 0.5)
    POI.update_parameters!(backend(model))
    return nothing
end

function poi_add_sqf_parameters_parameters_ctr(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:Int(N / 2)])
    @variable(model, p[i = 1:Int(N / 2)] in POI.Parameter.(1))
    @constraint(model, con[i = 1:M], dot(p , p) >= 1)
    return nothing
end

function poi_add_sqf_parameters_parameters_ctr_parameter_update(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:Int(N / 2)])
    @variable(model, p[i = 1:Int(N / 2)] in POI.Parameter.(1))
    @constraint(model, con[i = 1:M], dot(p , p) >= 1)
    MOI.set.(model, POI.ParameterValue(), p, 0.5)
    POI.update_parameters!(backend(model))
    return nothing
end

function moi_add_saf_obj(N::Int, M::Int)
    model = direct_model(
        MOI.Utilities.CachingOptimizer(
            MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
            MOI.Utilities.AUTOMATIC,
        ),
    )
    @variable(model, x[i = 1:N])
    @objective(model, Min, sum(x))
    return nothing
end

function poi_add_saf_obj(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:N])
    for _ in 1:M
        @objective(model, Min, sum(x))
    end
    return nothing
end

function poi_add_saf_variables_and_parameters_obj(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:Int(N / 2)])
    @variable(model, p[i = 1:Int(N / 2)] in POI.Parameter.(1))
    for _ in 1:M
        @objective(model, Min, sum(x) + sum(p))
    end
    return nothing
end

function poi_add_saf_variables_and_parameters_obj_parameter_update(
    N::Int,
    M::Int,
)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:Int(N / 2)])
    @variable(model, p[i = 1:Int(N / 2)] in POI.Parameter.(1))
    for _ in 1:M
        @objective(model, Min, sum(x) + sum(p))
    end
    for _ in 1:M
        MOI.set.(model, POI.ParameterValue(), p, 0.5)
        POI.update_parameters!(backend(model))
    end
    return nothing
end

function moi_add_sqf_variables_obj(N::Int, M::Int)
    model = direct_model(
        MOI.Utilities.CachingOptimizer(
            MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
            MOI.Utilities.AUTOMATIC,
        ),
    )
    @variable(model, x[i = 1:N])
    for _ in 1:M
        @objective(model, Min, dot(x , x))
    end
    return nothing
end

function poi_add_sqf_variables_obj(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:N])
    for _ in 1:M
        @objective(model, Min, dot(x , x))
    end
    return nothing
end

function poi_add_sqf_variables_parameters_obj(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:Int(N / 2)])
    @variable(model, p[i = 1:Int(N / 2)] in POI.Parameter.(1))
    for _ in 1:M
        @objective(model, Min, dot(x , p))
    end
    return nothing
end

function poi_add_sqf_variables_parameters_obj_parameter_update(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:Int(N / 2)])
    @variable(model, p[i = 1:Int(N / 2)] in POI.Parameter.(1))
    for _ in 1:M
        @objective(model, Min, dot(x , p))
    end
    for _ in 1:M
        MOI.set.(model, POI.ParameterValue(), p, 0.5)
        POI.update_parameters!(backend(model))
    end
    return nothing
end

function poi_add_sqf_parameters_parameters_obj(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:Int(N / 2)])
    @variable(model, p[i = 1:Int(N / 2)] in POI.Parameter.(1))
    for _ in 1:M
        @objective(model, Min, dot(p , p))
    end
    return nothing
end

function poi_add_sqf_parameters_parameters_obj_parameter_update(N::Int, M::Int)
    model = direct_model(
        POI.Optimizer(
            MOI.Utilities.CachingOptimizer(
                MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
                MOI.Utilities.AUTOMATIC,
            ),
        ),
    )
    @variable(model, x[i = 1:Int(N / 2)])
    @variable(model, p[i = 1:Int(N / 2)] in POI.Parameter.(1))
    for _ in 1:M
        @objective(model, Min, dot(p , p))
    end
    for _ in 1:M
        MOI.set.(model, POI.ParameterValue(), p, 0.5)
        POI.update_parameters!(backend(model))
    end
    return nothing
end

function run_benchmarks(N::Int, M::Int)
    println("Pkg status:")
    Pkg.status()
    println("")
    GC.gc()
    println("variables on a MOIU.Model.")
    @btime moi_add_variables($N)
    GC.gc()
    println("variables on a POI.Optimizer.")
    @btime poi_add_variables($N)
    GC.gc()
    println("parameters on a POI.Optimizer.")
    @btime poi_add_parameters($N)
    GC.gc()
    println("parameters and variables on a POI.Optimizer.")
    @btime poi_add_parameters_and_variables($N)
    GC.gc()
    println("alternating parameters and variables on a POI.Optimizer.")
    @btime poi_add_parameters_and_variables_alternating($N)
    GC.gc()
    println("SAF constraint with variables on a MOIU.Model.")
    @btime moi_add_saf_ctr($N, $M)
    GC.gc()
    println("SAF constraint with variables on a POI.Optimizer.")
    @btime poi_add_saf_ctr($N, $M)
    GC.gc()
    println("SAF constraint with variables and parameters on a POI.Optimizer.")
    @btime poi_add_saf_variables_and_parameters_ctr($N, $M)
    GC.gc()
    println("SQF constraint with variables on a MOIU.Model{Float64}.")
    @btime moi_add_sqf_variables_ctr($N, $M)
    GC.gc()
    println("SQF constraint with variables on a POI.Optimizer.")
    @btime poi_add_sqf_variables_ctr($N, $M)
    GC.gc()
    println("SQF constraint with product of variables and parameters on a POI.Optimizer.")
    @btime poi_add_sqf_variables_parameters_ctr($N, $M)
    GC.gc()
    println("SQF constraint with product of parameters on a POI.Optimizer.")
    @btime poi_add_sqf_parameters_parameters_ctr($N, $M)
    GC.gc()
    println("SAF objective with variables on a MOIU.Model.")
    @btime moi_add_saf_obj($N, $M)
    GC.gc()
    println("SAF objective with variables on a POI.Optimizer.")
    @btime poi_add_saf_obj($N, $M)
    GC.gc()
    println("SAF objective with variables and parameters on a POI.Optimizer.")
    @btime poi_add_saf_variables_and_parameters_obj($N, $M)
    GC.gc()
    println("SQF objective with variables on a MOIU.Model.")
    @btime moi_add_sqf_variables_obj($N, $M)
    GC.gc()
    println("SQF objective with variables on a POI.Optimizer.")
    @btime poi_add_sqf_variables_obj($N, $M)
    GC.gc()
    println("SQF objective with product of variables and parameters on a POI.Optimizer.")
    @btime poi_add_sqf_variables_parameters_obj($N, $M)
    GC.gc()
    println("SQF objective with product of parameters on a POI.Optimizer.")
    @btime poi_add_sqf_parameters_parameters_obj($N, $M)
    GC.gc()
    println("Update parameters in SAF constraint with variables and parameters on a POI.Optimizer.")
    @btime poi_add_saf_variables_and_parameters_ctr_parameter_update($N, $M)
    GC.gc()
    println("Update parameters in SAF objective with variables and parameters on a POI.Optimizer.")
    @btime poi_add_saf_variables_and_parameters_obj_parameter_update($N, $M)
    GC.gc()
    println("Update parameters in SQF constraint with product of variables and parameters on a POI.Optimizer.")
    @btime poi_add_sqf_variables_parameters_ctr_parameter_update($N, $M)
    GC.gc()
    println("Update parameters in SQF constraint with product of parameters on a POI.Optimizer.")
    @btime poi_add_sqf_parameters_parameters_ctr_parameter_update($N, $M)
    GC.gc()
    println("Update parameters in SQF objective with product of variables and parameters on a POI.Optimizer.")
    @btime poi_add_sqf_variables_parameters_obj_parameter_update($N, $M)
    GC.gc()
    println("Update parameters in SQF objective with product of parameters on a POI.Optimizer.")
    @btime poi_add_sqf_parameters_parameters_obj_parameter_update($N, $M)
    return nothing
end

N = 10_000
M = 100
run_benchmarks(N, M)


using Base.Test
using Reactive

facts("Flatten") do

    a = Signal(0)
    b = Signal(1)

    c = Signal(a)

    d = flatten(c)
    cnt = foldp((x, y) -> x+1, 0, d)

    context("Signal{Signal} -> flat Signal") do
        # Flatten implies:
        @fact value(c) --> a
        @fact value(d) --> value(a)
    end

    context("Initial update count") do

        @fact value(cnt) --> 0
    end

    context("Current signal updates") do
        push!(a, 2)
        step()

        @fact value(cnt) --> 1
        @fact value(d) --> value(a)
    end

    context("Signal swap") do
        push!(c, b)
        step()
        @fact value(cnt) --> 2
        @fact value(d) --> value(b)

        push!(a, 3)
        step()
        @fact value(cnt) --> 2
        @fact value(d) --> value(b)

        push!(b, 3)
        step()

        @fact value(cnt) --> 3
        @fact value(d) --> value(b)
    end
end


using Pkg: TOML.parsefile, Types.VersionRange
using DataFrames, CSV

function parse_package(name)
    dir = "$(homedir())/.julia/registries/General/$(name[1:1])/$name"
    version = parsefile("$dir/Versions.toml") |>
        keys |>
        (k -> maximum(VersionNumber, k))
    compat = parsefile("$dir/Compat.toml") |>
        (x -> [ ((name = n,
                  version = VersionRange(v))
                  for (n, v) in v) for (k, v) ∈ x if version ∈ VersionRange(k) ] ) |>
        Iterators.flatten |>
        collect |>
        sort!
    if v"1" ∈ compat[findfirst(x-> isequal("julia", x.name), compat)].version
        package = parsefile("$dir/Package.toml") |>
            (x -> (name = x["name"], uuid = x["uuid"], repo = x["repo"][1:end - 4]))
        output = DataFrame(name = package.name,
                           repository = package.repo,
                           version = version,
                           dependency = getproperty.(compat, :name))
    else
        output = DataFrame([String, String, VersionNumber, String],
                           [:name, :repository, :version, :dependency],
                           0)
    end
    output
end

data = readdir.(string.("$(homedir())/.julia/registries/General/", 'A':'Z')) |>
    Iterators.flatten |>
    collect |>
    (x -> filter!(x -> !any(x ∈ ["julia", ".DS_Store"]) , x)) |>
    (x -> vcat(parse_package.(x)...))
CSV.write("data/data.tsv", data, delim = '\t')