text:
import CausalityToolsBase:
    TriangulationBinningScheme,
    TriangulationBinning,
    SimplexIntersectionType,
    ExactIntersection

import StateSpaceReconstruction:
    invariantize,
    triangulate

import ..TransferOperators:
    transferoperator_triangulation_exact

"""
    TriangulationExactInvariantMeasure

An invariant measure estimated from a triangulation using the exact simplex
intersection estimator `transferoperator_triangulation_exact` for the
approximation of the transfer matrix.

## Fields
- **`points`**: The points from which the measure is estimated.
- **`invariantized_points`**: The points from which the measure is estimated,
possibly with the last point moved toward the center of the convex hull of
the points to ensure no information is lost when computing the transfer matrix.
- **`triangulation`**. A triangulation of `invariantized_points[1:(end - 1)]` which is used to estimate the transfer matrix.

"""
struct TriangulationExactInvariantMeasure{PT, TT, TOT} <: AbstractTriangulationInvariantMeasure
    points::PT
    invariantized_points::PT
    triangulation::TT
    transferoperator::TOT
    measure::InvariantDistribution
end

function triangulationexactinvariantmeasure(points::PT; kwargs...) where {PT}
    inv_points = invariantize(points)
    triang = triangulate(inv_points[1:(end - 1)])
    TO = transferoperator_triangulation_exact(points, triang.simplexindices)
    measure = invariantmeasure(TO, kwargs...)

    TriangulationExactInvariantMeasure(
        points,
        inv_points,
        triang,
        TO,
        measure)
end

function description(μ::TriangulationExactInvariantMeasure)
    T_pts = typeof(μ.points)
    T_invpts = typeof(μ.invariantized_points)
    triang = μ.triangulation
    to = μ.transferoperator
    measure = μ.measure

    npts =

    invmeasure_type = typeof(μ)
    return join(["TriangulationExactInvariantMeasure", "\n",
                "  μ.points:\t\t", T_pts,   "\n",
                 "  μ.invariantized_points:\t", T_invpts, "\n",
                 "  μ.triangulation:\t", triang,
                "  μ.transferoperator:\t", to,
                "  μ.measure:\t\t", measure])
end

Base.show(io::IO, μ::TriangulationExactInvariantMeasure) = println(io, description(μ))

export TriangulationExactInvariantMeasure, triangulationexactinvariantmeasure


text:
if VERSION >= v"1.3" && !haskey(ENV, "JULIA_CBC_LIBRARY_PATH")
    exit()  # Use Cbc_jll instead.
end

using BinaryProvider # requires BinaryProvider 0.3.0 or later

# Parse some basic command-line arguments
const verbose = "--verbose" in ARGS
const prefix = Prefix(get([a for a in ARGS if a != "--verbose"], 1, joinpath(@__DIR__, "usr")))
products = [
    LibraryProduct(prefix, ["libCbcSolver"], :libcbcsolver),
    LibraryProduct(prefix, ["libCbc"], :libCbc),
]

## START-VERSION-UPDATE-BLOCK
# Download binaries from hosted location
bin_prefix = "https://github.com/JuliaBinaryWrappers/Cbc_jll.jl/releases/download/Cbc-v2.10.3+3"
# Listing of files generated by BinaryBuilder:
download_info = Dict(
    Linux(:aarch64, libc=:glibc, compiler_abi=CompilerABI(:gcc4, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.aarch64-linux-gnu-libgfortran3-cxx03.tar.gz", "9993d0480527baa39c83bb0cc81e891f5ef999217c01a0a7618253eb0c5f4430"),
    Linux(:aarch64, libc=:glibc, compiler_abi=CompilerABI(:gcc4, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.aarch64-linux-gnu-libgfortran3-cxx11.tar.gz", "519fec1e69b74f1f700b6ca0cf1784fd92890e39b5f62bb42517a388046e8598"),
    Linux(:aarch64, libc=:glibc, compiler_abi=CompilerABI(:gcc7, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.aarch64-linux-gnu-libgfortran4-cxx03.tar.gz", "afd4911e2b2b7271c05ac21405e0b422a00edcab625b058e85b218bbcdd48c35"),
    Linux(:aarch64, libc=:glibc, compiler_abi=CompilerABI(:gcc7, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.aarch64-linux-gnu-libgfortran4-cxx11.tar.gz", "4e6010e93ebcc4bdc08ed65c94f75ed7d7990d46feb6aaa1a1d58f3b036a116d"),
    Linux(:aarch64, libc=:glibc, compiler_abi=CompilerABI(:gcc8, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.aarch64-linux-gnu-libgfortran5-cxx03.tar.gz", "f3e8d51def4545bafeb4b15b9fae8bb5267026c7fdedd561b776cc8c98bb6ad6"),
    Linux(:aarch64, libc=:glibc, compiler_abi=CompilerABI(:gcc8, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.aarch64-linux-gnu-libgfortran5-cxx11.tar.gz", "f451bf8ccc77b34a8f494da64811af0f4ae00fd10e79d215e17c0037b14c5c49"),
    Linux(:aarch64, libc=:musl, compiler_abi=CompilerABI(:gcc4, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.aarch64-linux-musl-libgfortran3-cxx03.tar.gz", "a41c7961aa99a233d6779ad4ae5e9955f9d407f0b5c109c86a03d7e9d4ab724b"),
    Linux(:aarch64, libc=:musl, compiler_abi=CompilerABI(:gcc4, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.aarch64-linux-musl-libgfortran3-cxx11.tar.gz", "a54ed0ef44b2758dc087ef6444db37826be39a1bf608c7545599f85f8d2dab77"),
    Linux(:aarch64, libc=:musl, compiler_abi=CompilerABI(:gcc7, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.aarch64-linux-musl-libgfortran4-cxx03.tar.gz", "fa343ee27ac7a583aed4c9b63ea7bfaa564d4150ec5ded153d2d191338af7512"),
    Linux(:aarch64, libc=:musl, compiler_abi=CompilerABI(:gcc7, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.aarch64-linux-musl-libgfortran4-cxx11.tar.gz", "4d79c86e4870c36886a61ed48da7bd38d100473800d358f6db4d9551cfdb3fb0"),
    Linux(:aarch64, libc=:musl, compiler_abi=CompilerABI(:gcc8, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.aarch64-linux-musl-libgfortran5-cxx03.tar.gz", "16d4cc7c2e6d2dd54ed85b54416f7a50d331cc3bbc2cd72c116c871db6ba82a4"),
    Linux(:aarch64, libc=:musl, compiler_abi=CompilerABI(:gcc8, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.aarch64-linux-musl-libgfortran5-cxx11.tar.gz", "b441a8a6597304604715891088d615b9c8b44cbd454a7657a55a2ef68ee8e6f3"),
    Linux(:armv7l, libc=:glibc, call_abi=:eabihf, compiler_abi=CompilerABI(:gcc4, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.armv7l-linux-gnueabihf-libgfortran3-cxx03.tar.gz", "fe220a8ed47c26f6da3e765d252e07f96fff131b2b44d0d54f61b20b2d0984e0"),
    Linux(:armv7l, libc=:glibc, call_abi=:eabihf, compiler_abi=CompilerABI(:gcc4, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.armv7l-linux-gnueabihf-libgfortran3-cxx11.tar.gz", "3b7715877e07b9ed3d481989d37851f35fdea28372600f7c38a9b29b1f59c6a5"),
    Linux(:armv7l, libc=:glibc, call_abi=:eabihf, compiler_abi=CompilerABI(:gcc7, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.armv7l-linux-gnueabihf-libgfortran4-cxx03.tar.gz", "2bdfd75e483df6fcfa1431402d54e8a6927a94b3ed34c5145ba32702fed234ad"),
    Linux(:armv7l, libc=:glibc, call_abi=:eabihf, compiler_abi=CompilerABI(:gcc7, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.armv7l-linux-gnueabihf-libgfortran4-cxx11.tar.gz", "15ed9a74917673e3613c0230759227ba00555c083d13c9475b932b8ff0d41bda"),
    Linux(:armv7l, libc=:glibc, call_abi=:eabihf, compiler_abi=CompilerABI(:gcc8, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.armv7l-linux-gnueabihf-libgfortran5-cxx03.tar.gz", "3007bca5c492aa78a228f762e5ae2b0b01b85cecfeb79cb8301f961bf0142528"),
    Linux(:armv7l, libc=:glibc, call_abi=:eabihf, compiler_abi=CompilerABI(:gcc8, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.armv7l-linux-gnueabihf-libgfortran5-cxx11.tar.gz", "d6673ccc34c017d1854b248a3508ef6bc01c096c1c5fcb9f0923e814ef66841f"),
    Linux(:armv7l, libc=:musl, call_abi=:eabihf, compiler_abi=CompilerABI(:gcc4, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.armv7l-linux-musleabihf-libgfortran3-cxx03.tar.gz", "f6b7a800f226e90b16f9b60750ea5ab9591707e5022f64241a3f10916da28a40"),
    Linux(:armv7l, libc=:musl, call_abi=:eabihf, compiler_abi=CompilerABI(:gcc4, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.armv7l-linux-musleabihf-libgfortran3-cxx11.tar.gz", "dba027aa3795bc05086c18d9cca61f25a72d4714cc080da3174ca6ff57e2fdfc"),
    Linux(:armv7l, libc=:musl, call_abi=:eabihf, compiler_abi=CompilerABI(:gcc7, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.armv7l-linux-musleabihf-libgfortran4-cxx03.tar.gz", "c956b6311fe88cb210ee6accb017765cae2bdbadd541f153e0d18c045a4391bb"),
    Linux(:armv7l, libc=:musl, call_abi=:eabihf, compiler_abi=CompilerABI(:gcc7, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.armv7l-linux-musleabihf-libgfortran4-cxx11.tar.gz", "d436e027db81669bfb30c919c88877c893190f62b3f80986a2c5d827d97aa759"),
    Linux(:armv7l, libc=:musl, call_abi=:eabihf, compiler_abi=CompilerABI(:gcc8, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.armv7l-linux-musleabihf-libgfortran5-cxx03.tar.gz", "850070d2c0b1ff5a00d327c98e8391c56b7135b8f9e3a9ea93cc325e0ad31bc1"),
    Linux(:armv7l, libc=:musl, call_abi=:eabihf, compiler_abi=CompilerABI(:gcc8, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.armv7l-linux-musleabihf-libgfortran5-cxx11.tar.gz", "8818fbcb71d29826c2567414292792073c43bf61d942c5474271015fb7529ce9"),
    Linux(:i686, libc=:glibc, compiler_abi=CompilerABI(:gcc4, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.i686-linux-gnu-libgfortran3-cxx03.tar.gz", "7c490f60c12e5ecb63fdc0239375b27fa0f67af4350ccbfa37aa2b7456f14b65"),
    Linux(:i686, libc=:glibc, compiler_abi=CompilerABI(:gcc4, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.i686-linux-gnu-libgfortran3-cxx11.tar.gz", "25ebbf011b74f8212408abc1ef81364741fb8b06ad5265d83c956b5f5d951f6b"),
    Linux(:i686, libc=:glibc, compiler_abi=CompilerABI(:gcc7, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.i686-linux-gnu-libgfortran4-cxx03.tar.gz", "b59f3855562278db9fec46c664a13926048ae4c8b2c96a1cc0b2d13f402aaca5"),
    Linux(:i686, libc=:glibc, compiler_abi=CompilerABI(:gcc7, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.i686-linux-gnu-libgfortran4-cxx11.tar.gz", "19f8bc8bbf18855e031812393f1754ecde2709c867a60d39431962bc701f172a"),
    Linux(:i686, libc=:glibc, compiler_abi=CompilerABI(:gcc8, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.i686-linux-gnu-libgfortran5-cxx03.tar.gz", "eae4234c625cdea9965ccefda39be6c404c9ce08205715a58dd6a9215bd19e9c"),
    Linux(:i686, libc=:glibc, compiler_abi=CompilerABI(:gcc8, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.i686-linux-gnu-libgfortran5-cxx11.tar.gz", "b0344c003b4c125c17adcba9d6422ba98435cc59f8899c73229aca4828451510"),
    Linux(:i686, libc=:musl, compiler_abi=CompilerABI(:gcc4, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.i686-linux-musl-libgfortran3-cxx03.tar.gz", "32a553bddc233640738060db86eb8f70f2f5fb5ab9528443dd192a6f46245cbe"),
    Linux(:i686, libc=:musl, compiler_abi=CompilerABI(:gcc4, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.i686-linux-musl-libgfortran3-cxx11.tar.gz", "4a9d91184bcbdd8866b977e343e667c4c97d1917219b5a856aa812416d4d31e4"),
    Linux(:i686, libc=:musl, compiler_abi=CompilerABI(:gcc7, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.i686-linux-musl-libgfortran4-cxx03.tar.gz", "7a1197aed7983fd35728019a0848cc88ac2853a55ee66d63662fd114a6f92707"),
    Linux(:i686, libc=:musl, compiler_abi=CompilerABI(:gcc7, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.i686-linux-musl-libgfortran4-cxx11.tar.gz", "cf7c121b3587c15c341018435bcf9837ad064999c94aa363feb4ee705805db0f"),
    Linux(:i686, libc=:musl, compiler_abi=CompilerABI(:gcc8, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.i686-linux-musl-libgfortran5-cxx03.tar.gz", "3136effb3a6d7c5540dd6e8c08dcfd3107876152287ae28495266f921fd52fc0"),
    Linux(:i686, libc=:musl, compiler_abi=CompilerABI(:gcc8, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.i686-linux-musl-libgfortran5-cxx11.tar.gz", "7a5d86231e0632db5bb9a2b5e640b481a8724993e6d1188bee6c17f1aa945251"),
    Windows(:i686, compiler_abi=CompilerABI(:gcc4, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.i686-w64-mingw32-libgfortran3-cxx03.tar.gz", "7b9edb5ae152ec2d13c1cd134240f57ff22037e8d97660c694a7563503a28a9e"),
    Windows(:i686, compiler_abi=CompilerABI(:gcc4, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.i686-w64-mingw32-libgfortran3-cxx11.tar.gz", "c1df3b74ec7581099c91c72650b64710a0eb1b80c08bbce399a2422a9a437663"),
    Windows(:i686, compiler_abi=CompilerABI(:gcc7, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.i686-w64-mingw32-libgfortran4-cxx03.tar.gz", "dfd74135a506c935d15e3fa3d385cb67909116cb2bae7d0c3c8759cd82d7e253"),
    Windows(:i686, compiler_abi=CompilerABI(:gcc7, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.i686-w64-mingw32-libgfortran4-cxx11.tar.gz", "af9e1fbe8551c3ce64bab87b4a7eb24fd34734b16e0162526210217f6ed6ca0d"),
    Windows(:i686, compiler_abi=CompilerABI(:gcc8, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.i686-w64-mingw32-libgfortran5-cxx03.tar.gz", "ae3acb2281aadfab1a98e576431be461c2bfc9f7f6f30429602b124d3236c566"),
    Windows(:i686, compiler_abi=CompilerABI(:gcc8, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.i686-w64-mingw32-libgfortran5-cxx11.tar.gz", "37575ad61b0f6011ba9f278a0873b7114c72bf04ef6a25b45e93c8698af10eac"),
    MacOS(:x86_64, compiler_abi=CompilerABI(:gcc4, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-apple-darwin14-libgfortran3-cxx03.tar.gz", "8caf7f626903b215cd5017576b91794518b78f23707d71b18c8a3e09b5955bfa"),
    MacOS(:x86_64, compiler_abi=CompilerABI(:gcc4, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-apple-darwin14-libgfortran3-cxx11.tar.gz", "130640305462f015f53fbd19df145036978325e51afe33fc4aacb24018b3e67b"),
    MacOS(:x86_64, compiler_abi=CompilerABI(:gcc7, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-apple-darwin14-libgfortran4-cxx03.tar.gz", "8b8b963ffb6f807e971d13f9642217cf8acea66351c91fc201761da99c5937de"),
    MacOS(:x86_64, compiler_abi=CompilerABI(:gcc7, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-apple-darwin14-libgfortran4-cxx11.tar.gz", "541a82ec36fb37960cb69e96f39b3990b891d69d82c0bfec73ad11813f42e513"),
    MacOS(:x86_64, compiler_abi=CompilerABI(:gcc8, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-apple-darwin14-libgfortran5-cxx03.tar.gz", "4077078028e54d5676166f3df0f6a8b84932a485616ac70471c0ed8c1add0fe9"),
    MacOS(:x86_64, compiler_abi=CompilerABI(:gcc8, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-apple-darwin14-libgfortran5-cxx11.tar.gz", "0609b9f97b2d18201fa8b5a16fdb68bcb205f3d7ca7fabd80ca1713ff33f42e5"),
    Linux(:x86_64, libc=:glibc, compiler_abi=CompilerABI(:gcc4, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-linux-gnu-libgfortran3-cxx03.tar.gz", "ec3e297558d34ca965f890f87149781654a03d3661a6b706f1797f63dd0a8972"),
    Linux(:x86_64, libc=:glibc, compiler_abi=CompilerABI(:gcc4, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-linux-gnu-libgfortran3-cxx11.tar.gz", "98a9993f322960636d49acc8e2efe725ca3d2bd9c859a63ef8ee1fe41bdfd95f"),
    Linux(:x86_64, libc=:glibc, compiler_abi=CompilerABI(:gcc7, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-linux-gnu-libgfortran4-cxx03.tar.gz", "5941f82ddfff5434b4a8073a5aa22f516fa7ee7bf6ba12086dd04ef7c0002b78"),
    Linux(:x86_64, libc=:glibc, compiler_abi=CompilerABI(:gcc7, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-linux-gnu-libgfortran4-cxx11.tar.gz", "e93a7b1cfe402d203ecfe778197e0eca5c7b65c4e9cfda79fd0f7b9e809bcdc3"),
    Linux(:x86_64, libc=:glibc, compiler_abi=CompilerABI(:gcc8, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-linux-gnu-libgfortran5-cxx03.tar.gz", "0d195469189549016e4327e2bfda45f94475dfd7556c53f993840afe1972530b"),
    Linux(:x86_64, libc=:glibc, compiler_abi=CompilerABI(:gcc8, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-linux-gnu-libgfortran5-cxx11.tar.gz", "d107978527ac8bc2b8f805d24fc0d057200f9de4fe4ffc5a2fd6a39af556c6f6"),
    Linux(:x86_64, libc=:musl, compiler_abi=CompilerABI(:gcc4, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-linux-musl-libgfortran3-cxx03.tar.gz", "f82ce68ee9c90d56c9d86f75e95a45507cb0d94dea8c104260c7c841d252dd9d"),
    Linux(:x86_64, libc=:musl, compiler_abi=CompilerABI(:gcc4, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-linux-musl-libgfortran3-cxx11.tar.gz", "377360fdf5f9db1b1b87aad3e2016532e4d04148e441d7106b95c8469b81f902"),
    Linux(:x86_64, libc=:musl, compiler_abi=CompilerABI(:gcc7, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-linux-musl-libgfortran4-cxx03.tar.gz", "210fd8b6e217cb40c245b059a2034817407441c7cdf6d799bec57510f74ade5e"),
    Linux(:x86_64, libc=:musl, compiler_abi=CompilerABI(:gcc7, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-linux-musl-libgfortran4-cxx11.tar.gz", "fcf9dd22539205e5f7836b4dd5ff7ca6280d1461fb42343a36beef58adb8ea59"),
    Linux(:x86_64, libc=:musl, compiler_abi=CompilerABI(:gcc8, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-linux-musl-libgfortran5-cxx03.tar.gz", "929c0f39c1949a9cc1b9414036c97458820b63d304e4dd6dd9791fdef86b428b"),
    Linux(:x86_64, libc=:musl, compiler_abi=CompilerABI(:gcc8, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-linux-musl-libgfortran5-cxx11.tar.gz", "8f10d8fbc6dcbcd415420813942f782288523f10f9346d87d69cdbf87aeefc43"),
    Windows(:x86_64, compiler_abi=CompilerABI(:gcc4, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-w64-mingw32-libgfortran3-cxx03.tar.gz", "1331d7685eaa775e6b1006eb7130104c34c5179111c3a739c3720a282c90c762"),
    Windows(:x86_64, compiler_abi=CompilerABI(:gcc4, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-w64-mingw32-libgfortran3-cxx11.tar.gz", "5fd902eb049490fe6ec260761aa4599d372b94629a20e88ce3e4db8f81032ad8"),
    Windows(:x86_64, compiler_abi=CompilerABI(:gcc7, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-w64-mingw32-libgfortran4-cxx03.tar.gz", "a7948eb3b1b0ac8565d80eeec9f1be4b22d5efdf04f6a9b4568a098969db3dce"),
    Windows(:x86_64, compiler_abi=CompilerABI(:gcc7, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-w64-mingw32-libgfortran4-cxx11.tar.gz", "aafc41743c6d631b5f1863dd9f2f2d1f21b13464fc02736a33040a171c94d44a"),
    Windows(:x86_64, compiler_abi=CompilerABI(:gcc8, :cxx03)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-w64-mingw32-libgfortran5-cxx03.tar.gz", "a77665065620b11c36f621e4f5a2aa6211843ccceb010ae4fdb33b7a084604c8"),
    Windows(:x86_64, compiler_abi=CompilerABI(:gcc8, :cxx11)) => ("$bin_prefix/Cbc.v2.10.3.x86_64-w64-mingw32-libgfortran5-cxx11.tar.gz", "7dc140e2c4e2c93b486ed53e819f967f10a345d91483b44df05d1fd4e817ab58"),
)
## END-VERSION-UPDATE-BLOCK

# To fix gcc4 bug in Windows
# https://sourceforge.net/p/mingw-w64/bugs/727/
this_platform = platform_key_abi()
if typeof(this_platform)==Windows && this_platform.compiler_abi.gcc_version == :gcc4
   this_platform = Windows(arch(this_platform), libc=libc(this_platform), compiler_abi=CompilerABI(:gcc6))
end

# no dynamic dependencies until Pkg3 support for binaries
dependencies = [
#     "https://github.com/juan-pablo-vielma/CglBuilder/releases/download/v0.59.10-1/build_CglBuilder.v0.59.10.jl",
#     "https://github.com/JuliaOpt/ClpBuilder/releases/download/v1.16.11-1/build_ClpBuilder.v1.16.11.jl",
#     "https://github.com/juan-pablo-vielma/OsiBuilder/releases/download/v0.107.9-1/build_OsiBuilder.v0.107.9.jl",
#     "https://github.com/juan-pablo-vielma/CoinUtilsBuilder/releases/download/v2.10.14-1/build_CoinUtilsBuilder.v2.10.14.jl",
#     "https://github.com/juan-pablo-vielma/COINMumpsBuilder/releases/download/v1.6.0-1/build_COINMumpsBuilder.v1.6.0.jl",
#     "https://github.com/juan-pablo-vielma/COINMetisBuilder/releases/download/v1.3.5-1/build_COINMetisBuilder.v1.3.5.jl",
#     "https://github.com/juan-pablo-vielma/COINLapackBuilder/releases/download/v1.5.6-1/build_COINLapackBuilder.v1.5.6.jl",
#     "https://github.com/juan-pablo-vielma/COINBLASBuilder/releases/download/v1.4.6-1/build_COINBLASBuilder.v1.4.6.jl",
#     "https://github.com/juan-pablo-vielma/ASLBuilder/releases/download/v3.1.0-1/build_ASLBuilder.v3.1.0.jl"
]

custom_library = false
if haskey(ENV,"JULIA_CBC_LIBRARY_PATH")
    custom_products = [LibraryProduct(ENV["JULIA_CBC_LIBRARY_PATH"],product.libnames,product.variable_name) for product in products]
    if all(satisfied(p; verbose=verbose) for p in custom_products)
        products = custom_products
        custom_library = true
    else
        error("Could not install custom libraries from $(ENV["JULIA_CBC_LIBRARY_PATH"]).\nTo fall back to BinaryProvider call delete!(ENV,\"JULIA_CBC_LIBRARY_PATH\") and run build again.")
    end
end

if !custom_library
    # Install unsatisfied or updated dependencies:
    # We added `, isolate=true` as otherwise, it would segfault when closing `OpenBLAS32`,
    # probably because it is conflicting with Julia openblas.
    unsatisfied = any(!satisfied(p; verbose=verbose, isolate=true) for p in products)

    dl_info = choose_download(download_info, this_platform)
    if dl_info === nothing && unsatisfied
        # If we don't have a compatible .tar.gz to download, complain.
        # Alternatively, you could attempt to install from a separate provider,
        # build from source or something even more ambitious here.
        error("Your platform (\"$(Sys.MACHINE)\", parsed as \"$(triplet(platform_key_abi()))\") is not supported by this package!")
    end

    # If we have a download, and we are unsatisfied (or the version we're
    # trying to install is not itself installed) then load it up!
    if unsatisfied || !isinstalled(dl_info...; prefix=prefix)
        # Download and install binaries
        # no dynamic dependencies until Pkg3 support for binaries
        # for dependency in reverse(dependencies)          # We do not check for already installed dependencies
        #    download(dependency,basename(dependency))
        #    evalfile(basename(dependency))
        # end
        evalfile("build_CompilerSupportLibraries.v0.3.3.jl")
        evalfile("build_OpenBLAS32.v0.3.9.jl")
        evalfile("build_CoinUtils.v2.11.3.jl")
        evalfile("build_Osi.v0.108.5.jl")
        evalfile("build_Clp.v1.17.6.jl")
        evalfile("build_Cgl.v0.60.2.jl")
        install(dl_info...; prefix=prefix, force=true, verbose=verbose)
    end
 end

# Write out a deps.jl file that will contain mappings for our products
write_deps_file(joinpath(@__DIR__, "deps.jl"), products, verbose=verbose)

ext: jl


text:
"""
    Valuation

A data structure for approximating the valuation of a solution path ``x(t)``.
"""
mutable struct Valuation
    val_x::Vector{Float64}
    val_tẋ::Vector{Float64}
    Δval_x::Vector{Float64}
    Δval_tẋ::Vector{Float64}

    # Data for computing the val_x and val_tẋ and ν̈ by finite differences
    val_x_data::NTuple{2,Vector{Float64}}
    val_ẋ_data::NTuple{2,Vector{Float64}}
    logx_data::NTuple{2,Vector{Float64}}
    logẋ_data::NTuple{2,Vector{Float64}}
    logt_data::NTuple{2,Float64}
end

Valuation(x::AbstractVector) = Valuation(length(x))
Valuation(n::Integer) =
    Valuation((zeros(n) for i = 1:4)..., ((zeros(n), zeros(n)) for i = 1:4)..., (NaN, NaN))

function Base.show(io::IO, val::Valuation)
    print(io, "Valuation :")
    for field in [:val_x, :val_tẋ, :Δval_x, :Δval_tẋ]
        vs = [Printf.@sprintf("%#.4g", v) for v in getfield(val, field)]
        print(io, "\n • ", field, " → ", "[", join(vs, ", "), "]")
    end
end
Base.show(io::IO, ::MIME"application/prs.juno.inline", v::Valuation) = v

"""
    init!(val::Valuation)

Initialize the valuation estimator to start from scratch.
"""
function init!(val::Valuation)
    val.val_x .= 0.0
    val.val_tẋ .= 0.0
    val.Δval_x .= 0.0
    val.Δval_tẋ .= 0.0
    val.val_x_data[1] .= val.val_x_data[2] .= 0.0
    val.val_ẋ_data[1] .= val.val_ẋ_data[2] .= 0.0
    val.logx_data[1] .= val.logx_data[2] .= 0.0
    val.logẋ_data[1] .= val.logẋ_data[2] .= 0.0
    val.logt_data = (NaN, NaN)
    val
end

function ν(x, ẋ, t)
    u, v = reim(x)
    u¹, v¹ = reim(ẋ)
    xx = abs2(x)

    μ = u * u¹ + v * v¹
    l = μ / xx

    t * l
end

function ν_ν¹(x, ẋ, x², t)
    u, v = reim(x)
    u¹, v¹ = reim(ẋ)
    u², v² = reim(x²)

    xx = abs2(x)

    μ = u * u¹ + v * v¹
    l = μ / xx

    μ¹ = u * u² + u¹^2 + v * v² + v¹^2
    l¹ = μ¹ / xx - 2 * l^2

    t * l, t * l¹ + l
end

logabs(x) = log(fast_abs(x))

function update!(val::Valuation, pred::Predictor{AD{N}}, t::Real) where {N}
    # use analytic expressions if derivatives are computed with automatic differentiation
    if N ≥ 3
        update_analytic!(val, pred.tx³, t)
        return val
    end

    # otherwise use finite difference scheme
    logt = log(t)
    ν₂, ν₁ = val.val_x_data
    logx₂, logx₁, = val.logx_data
    logẋ₂, logẋ₁, = val.logẋ_data
    logt₂, logt₁ = val.logt_data
    val_ẋ₂, val_ẋ₁ = val.val_ẋ_data
    for (i, (xᵢ, ẋᵢ)) in enumerate(pred.tx¹)
        logxᵢ = logabs(xᵢ)
        νᵢ = ν(xᵢ, ẋᵢ, t)
        Δνᵢ = finite_diff(νᵢ, logt, ν₂[i], logt₂, ν₁[i], logt₁)

        logẋᵢ = logabs(ẋᵢ)
        val_ẋᵢ = finite_diff(logẋᵢ, logt, logẋ₂[i], logt₂, logẋ₁[i], logt₁)
        Δval_ẋᵢ = finite_diff(val_ẋᵢ, logt, val_ẋ₂[i], logt₂, val_ẋ₁[i], logt₁)

        val.val_x[i] = νᵢ
        val.Δval_x[i] = Δνᵢ
        val.val_tẋ[i] = val_ẋᵢ + 1
        val.Δval_tẋ[i] = Δval_ẋᵢ

        ν₂[i], ν₁[i] = νᵢ, ν₂[i]
        logx₂[i], logx₁[i] = logxᵢ, logx₂[i]
        logẋ₂[i], logẋ₁[i] = logẋᵢ, logẋ₂[i]
        val_ẋ₂[i], val_ẋ₁[i] = val_ẋᵢ, val_ẋ₂[i]
    end
    val.logt_data = (logt, logt₂)

    val
end

function update_analytic!(val::Valuation, tx::TaylorVector, t::Real)
    for i in eachindex(tx)
        xᵢ, ẋᵢ, x²ᵢ, x³ᵢ = tx[i]

        ν, ν¹ = ν_ν¹(xᵢ, ẋᵢ, 2x²ᵢ, t)
        val.val_x[i] = ν
        val.Δval_x[i] = t * ν¹

        ν, ν¹ = ν_ν¹(ẋᵢ, 2x²ᵢ, 6x³ᵢ, t)
        val.val_tẋ[i] = ν + 1
        val.Δval_tẋ[i] = t * ν¹
    end

    val
end

"""
    finite_diff(ν₃, s₃, ν₂, s₂, ν₁, s₁)

Compute the derivative `ν̇` of a function ``ν(s)`` at `s₃` by using a finite
difference scheme. This uses the values `νᵢ = ν(sᵢ)` for ``i=1,…,3``.
Since we have a non-uniform grid, we need a more elaborate difference scheme.
The implementation follows the formulas derived in [^BS05].

[^BS05]: Bowen, M. K., and Ronald Smith. "Derivative formulae and errors for non-uniformly
  spaced points." Proceedings of the Royal Society A: Mathematical, Physical and Engineering
  Sciences 461.2059 (2005): 1975-1997.
"""
function finite_diff(ν, s, ν₂, s₂, ν₁, s₁)
    Δ₁, Δ₂, Δ₁₂ = s - s₁, s - s₂, s₁ - s₂
    ν̇ = (Δ₂ * ν₁) / (Δ₁₂ * Δ₁) - ((Δ₁₂ + Δ₂) * ν₂) / (Δ₁₂ * Δ₂) - (Δ₁₂ * ν) / (Δ₁ * Δ₂)
    # ν̈ = -2ν₁ / (Δ₁₂ * Δ₁) + 2ν₂ / (Δ₁₂ * Δ₂) + 2ν / (Δ₁ * Δ₂)
    ν̇
end

function analyze(
    val::Valuation;
    finite_tol::Float64,
    zero_is_finite::Bool,
    max_winding_number::Int,
)
    @unpack val_x, val_tẋ, Δval_x, Δval_tẋ = val

    at_infinity_tol = at_zero_tol = Inf
    finite = true
    δ = inv(max_winding_number)
    for (i, val_xᵢ) in enumerate(val_x)
        abs_Δ = abs(Δval_x[i])
        ε∞ = max(
            abs(1.0 - val_tẋ[i] / val_xᵢ),
            abs(Δval_x[i] / val_xᵢ),
            abs(Δval_tẋ[i] / val_tẋ[i]),
        )
        if isnan(abs_Δ)
            finite = false
            continue
        end
        # ∞ check
        if val_xᵢ + ε∞ < -(δ + finite_tol)
            at_infinity_tol = min(ε∞, at_infinity_tol)
        end

        # 0 check
        if !zero_is_finite && val_xᵢ - ε∞ > (δ + finite_tol)
            at_zero_tol = min(ε∞, at_zero_tol)
        end

        # finite
        # Case a: val(x) = 0
        if abs(val_xᵢ) < finite_tol
            if abs_Δ > finite_tol || val_tẋ[i] ≤ 0
                finite = false
            end
            # Case b: val(x) = val(tẋ) > 0
        elseif zero_is_finite && val_xᵢ > finite_tol
            if ε∞ > finite_tol
                finite = false
            end
        else
            finite = false
        end
    end

    return (
        val_finite = finite,
        at_infinity_tol = at_infinity_tol,
        at_zero_tol = at_zero_tol,
    )
end

function validate_coord_growth(
    val::Valuation,
    scale_old,
    scale_new;
    finite_tol::Float64,
    at_infinity_tol::Float64,
    tol::Float64,
)
    @unpack val_x, val_tẋ, Δval_x, Δval_tẋ = val

    for (i, val_xᵢ) in enumerate(val_x)
        ε∞ = max(
            abs(1.0 - val_tẋ[i] / val_xᵢ),
            abs(Δval_x[i] / val_xᵢ),
            abs(Δval_tẋ[i] / val_tẋ[i]),
        )
        # ∞ check
        if val_xᵢ + ε∞ < -finite_tol && ε∞ < at_infinity_tol
            if scale_new[i] / scale_old[i] > tol
                return true
            end
        end
    end

    false
end


text:
@model lbaModel(data, N, Nc, ::Type{T}=Float64) where {T} = begin
     mn = minimum(x -> x[2], data)
     tau ~ TruncatedNormal(0.4, 0.1, 0, mn)
     A ~ TruncatedNormal(0.8, 0.4, 0, Inf)
     k ~ TruncatedNormal(0.2, 0.3, 0, Inf)
     v = Vector{T}(undef, Nc)
     for i in 1:Nc
          v[i] ~ TruncatedNormal(0, 3, 0, Inf)
     end
     data ~ [LBA(; ν=v, τ=tau, A=A, k=k)]
end

function AHMClba(choice,rt,N,Nc)
    data=[(c,r) for (c,r) in zip(choice,rt)]
    return lbaModel(data,N,Nc)
end

AHMCconfig = Turing.NUTS(1000,.85)

CmdStanLBA = "
functions{

     real lba_pdf(real t, real b, real A, real v, real s){
          //PDF of the LBA model

          real b_A_tv_ts;
          real b_tv_ts;
          real term_1;
          real term_2;
          real term_3;
          real term_4;
          real pdf;

          b_A_tv_ts = (b - A - t*v)/(t*s);
          b_tv_ts = (b - t*v)/(t*s);
          term_1 = v*Phi(b_A_tv_ts);
          term_2 = s*exp(normal_lpdf(b_A_tv_ts|0,1));
          term_3 = v*Phi(b_tv_ts);
          term_4 = s*exp(normal_lpdf(b_tv_ts|0,1));
          pdf = (1/A)*(-term_1 + term_2 + term_3 - term_4);

          return pdf;
     }

     real lba_cdf(real t, real b, real A, real v, real s){
          //CDF of the LBA model

          real b_A_tv;
          real b_tv;
          real ts;
          real term_1;
          real term_2;
          real term_3;
          real term_4;
          real cdf;

          b_A_tv = b - A - t*v;
          b_tv = b - t*v;
          ts = t*s;
          term_1 = b_A_tv/A * Phi(b_A_tv/ts);
          term_2 = b_tv/A   * Phi(b_tv/ts);
          term_3 = ts/A     * exp(normal_lpdf(b_A_tv/ts|0,1));
          term_4 = ts/A     * exp(normal_lpdf(b_tv/ts|0,1));
          cdf = 1 + term_1 - term_2 + term_3 - term_4;

          return cdf;

     }

     real lba_lpdf(matrix RT, real k, real A, vector v, real s, real tau){

          real t;
          real b;
          real cdf;
          real pdf;
          vector[rows(RT)] prob;
          real out;
          real prob_neg;

          b = A + k;
          for (i in 1:rows(RT)){
               t = RT[i,1] - tau;
               if(t > 0){
                    cdf = 1;

                    for(j in 1:num_elements(v)){
                         if(RT[i,2] == j){
                              pdf = lba_pdf(t, b, A, v[j], s);
                         }else{
                              cdf = (1-lba_cdf(t, b, A, v[j], s)) * cdf;
                         }
                    }
                    prob_neg = 1;
                    for(j in 1:num_elements(v)){
                         prob_neg = Phi(-v[j]/s) * prob_neg;
                    }
                    prob[i] = pdf*cdf;
                    prob[i] = prob[i]/(1-prob_neg);
                    if(prob[i] < 1e-10){
                         prob[i] = 1e-10;
                    }

               }else{
                    prob[i] = 1e-10;
               }
          }
          out = sum(log(prob));
          return out;
     }
}

data{
     int N;
     int Nc;
     vector[N] rt;
     vector[N] choice;
}

parameters {
     real<lower=0> k;
     real<lower=0> A;
     real<lower=0> tau;
     vector<lower=0>[Nc] v;
}

model {
     real s;
     real minRT;
     matrix[N,2] RT;
     s=1;
     minRT = min(rt);
     RT[:,1] = rt;
     RT[:,2] = choice;
     k ~ normal(.2,.3)T[0,];
     A ~ normal(.8,.4)T[0,];
     tau ~ normal(.4,.1)T[0,minRT];
     for(n in 1:Nc){
          v[n] ~ normal(0,3)T[0,];
     }
     RT ~ lba(k,A,v,s,tau);
}
"

CmdStanConfig = Stanmodel(name="CmdStanLBA", model=CmdStanLBA, nchains=1,
   Sample(num_samples=1000, num_warmup=1000, adapt=CmdStan.Adapt(delta=0.8),
   save_warmup=true))

   struct LBAProb{T}
      data::T
      N::Int
      Nc::Int
  end

  function (problem::LBAProb)(θ)
      @unpack data=problem
      @unpack v,A,k,tau=θ
      d=LBA(ν=v, A=A, k=k, τ=tau)
      minRT = minimum(x->x[2], data)
      logpdf(d,data)+sum(logpdf.(TruncatedNormal(0, 3, 0, Inf), v)) +
      logpdf(TruncatedNormal(.8, .4, 0, Inf),A)+logpdf(TruncatedNormal(.2, .3 ,0 ,Inf), k)+
      logpdf(TruncatedNormal(.4, .1, 0, minRT), tau)
  end

function sampleDHMC(choice, rt, N, Nc, nsamples)
    data = [(c, r) for (c, r) in zip(choice, rt)]
    return sampleDHMC(data, N, Nc, nsamples)
end

# Define problem with data and inits.
function sampleDHMC(data, N, Nc, nsamples, autodiff)
    p = LBAProb(data, N, Nc)
    p((v=fill(.5, Nc), A=.8, k=.2, tau=.4))
    # Write a function to return properly dimensioned transformation.
    trans = as((v=as(Array, asℝ₊, Nc),A=asℝ₊, k=asℝ₊, tau=asℝ₊))
    # Use Flux for the gradient.
    P = TransformedLogDensity(trans, p)
    ∇P = ADgradient(autodiff, P)
    # FSample from the posterior.
    n = dimension(trans)
    results = mcmc_with_warmup(Random.GLOBAL_RNG, ∇P, nsamples;
        q = zeros(n), p = ones(n),reporter = NoProgressReport())
    # Undo the transformation to obtain the posterior from the chain.
    posterior = transform.(trans, results.chain)
    chns = nptochain(results, posterior)
    return chns
end

function simulateLBA(;Nd, v=[1.0,1.5,2.0], A=.8, k=.2, tau=.4, kwargs...)
    return (rand(LBA(ν=v, A=A, k=k, τ=tau), Nd)..., N=Nd, Nc=length(v))
end


text:
using DashBootstrapComponents, DashHtmlComponents

spinners = html_div([
    dbc_spinner(color="primary"),
    dbc_spinner(color="secondary"),
    dbc_spinner(color="success"),
    dbc_spinner(color="warning"),
    dbc_spinner(color="danger"),
    dbc_spinner(color="info"),
    dbc_spinner(color="light"),
    dbc_spinner(color="dark"),
]);

max_stars_repo_name: harisbal/dash-bootstrap-components

max_issues_repo_name: harisbal/dash-bootstrap-components

max_forks_repo_name: harisbal/dash-bootstrap-components


text:
using Beancount
using Dates
using Test
using Decimals

"Extract type of transaction."
typ(::Transaction{T}) where T = T

@testset "Loading Files" begin
    # Load test file
    entries, errors, options = Beancount.load_file("testfile.beancount")

    # Check loaded data and options
    @test length(entries)               == 8
    @test length(errors)                == 0
    @test options["title"]              == "Beancount Example Ledger"
    @test options["operating_currency"] == ["EUR"]

    # Parse what can be parsed
    parsed = from_python.(entries)

    # Check parsed transactions and postings
    @test parsed[5].date                 == Date(2007,12,31)
    @test parsed[5] |> typ               == pad
    @test parsed[5].description          == "(Padding inserted for Balance of 1412.24 EUR for difference 1412.24 EUR)"
    @test parsed[5].payee                == nothing
    @test parsed[5].postings[1].account  == "Assets:TestAccount"
    @test parsed[5].postings[1].value    == 1412.24
    @test parsed[5].postings[1].currency == "EUR"
    @test parsed[5].postings[2].account  == "Equity:Opening-Balances"
    @test parsed[5].postings[2].value    == -1412.24
    @test parsed[5].postings[2].currency == "EUR"
    @test parsed[7].date                 == Date(2008,01,01)
    @test parsed[7] |> typ               == txn
    @test parsed[7].description          == "Else"
    @test parsed[7].payee                == "Something"
    @test parsed[7].postings[1].account  == "Assets:TestAccount"
    @test parsed[7].postings[1].value    == -30
    @test parsed[7].postings[1].currency == "EUR"
    @test parsed[7].postings[2].account  == "Expenses:Stuff"
    @test parsed[7].postings[2].value    == 5562
    @test parsed[7].postings[2].currency == "USD"
    @test parsed[7].tags                 == Set(["tagA", "tagB"])
    @test parsed[7].links                == Set(["linkA"])
end

@testset "Writing Files" begin
    # Write Transactions
    tx = Transaction{txn}(date=today(),description="This is the description",tags=Set(["foo", "bar"]))
    push!(tx,Posting(account="Equity:Opening-Balances"))
    push!(tx,Posting(account="Assets:TestAccount",value=decimal("100.3")))

    open("test.beancount","w") do f
        println(f,"1970-01-01 open Equity:Opening-Balances")
        println(f,"1970-01-01 open Assets:TestAccount")
        println(f,tx)
    end

    @test isfile("test.beancount")
    entries, errors, options = Beancount.load_file("test.beancount")
    isfile("test.beancount") && rm("test.beancount")

    @test length(errors) == 0

    parsed = from_python.(entries)
    @test parsed[end] == tx
end


text:
module CategoricalArrays
    export CategoricalPool, CategoricalValue
    export AbstractCategoricalArray, AbstractCategoricalVector, AbstractCategoricalMatrix,
           CategoricalArray, CategoricalVector, CategoricalMatrix
    export LevelsException

    export categorical, compress, decompress, droplevels!, levels, levels!, levelcode,
           isordered, ordered!
    export cut, recode, recode!

    import DataAPI: unwrap
    export unwrap

    using DataAPI
    using Missings
    using Printf
    using Requires: @require

    # JuliaLang/julia#36810
    if VERSION < v"1.5.2"
        Base.OrderStyle(::Type{Union{}}) = Base.Ordered()
    end

    include("typedefs.jl")

    include("pool.jl")
    include("value.jl")

    include("array.jl")
    include("missingarray.jl")
    include("subarray.jl")

    include("extras.jl")
    include("recode.jl")

    include("deprecated.jl")

    function __init__()
        @require JSON="682c06a0-de6a-54ab-a142-c8b1cf79cde6" begin
            # JSON of CategoricalValue is JSON of the value it refers to
            JSON.lower(x::CategoricalValue) = JSON.lower(unwrap(x))
        end

        @require RecipesBase="3cdcf5f2-1ef4-517c-9805-6587b60abb01" @eval begin
            RecipesBase.@recipe function f(::Type{T}, v::T) where T <: CategoricalValue
                level_strings = [map(string, levels(v)); missing]
                ticks --> eachindex(level_strings)
                v -> ismissing(v) ? length(level_strings) : Int(refcode(v)),
                i -> level_strings[Int(i)]
            end
        end

        @require SentinelArrays="91c51154-3ec4-41a3-a24f-3f23e20d615c" begin
            copyto!(dest::CatArrOrSub{<:Any, 1}, src::SentinelArrays.ChainedVector) =
                copyto!(dest, 1, src, 1, length(src))
            copyto!(dest::CatArrOrSub{<:Any, 1}, dstart::Union{Signed, Unsigned},
                    src::SentinelArrays.ChainedVector, sstart::Union{Signed, Unsigned},
                    n::Union{Signed, Unsigned}) =
                invoke(copyto!, Tuple{AbstractArray, Union{Signed, Unsigned},
                                    SentinelArrays.ChainedVector,
                                    Union{Signed, Unsigned}, Union{Signed, Unsigned}},
                    dest, dstart, src, sstart, n)
        end

        @require StructTypes="856f2bd8-1eba-4b0a-8007-ebc267875bd4" begin
            # define appropriate handlers for JSON3 interface
            StructTypes.StructType(x::CategoricalValue) = StructTypes.StructType(unwrap(x))
            StructTypes.StructType(::Type{<:CategoricalValue{T}}) where {T} = StructTypes.StructType(T)
            StructTypes.numbertype(::Type{<:CategoricalValue{T}}) where {T <: Number} = T
            StructTypes.construct(::Type{T}, x::CategoricalValue{T}) where {T} = T(unwrap(x))

            # JSON3 writing/reading
            StructTypes.StructType(::Type{<:CategoricalVector}) = StructTypes.ArrayType()

            StructTypes.construct(::Type{<:CategoricalArray}, A::AbstractVector) =
                constructgeneral(A)
            StructTypes.construct(::Type{<:CategoricalArray}, A::Vector) =
                constructgeneral(A)

            function constructgeneral(A)
                if eltype(A) === Any
                    # unlike `replace`, broadcast narrows the type, which allows us to return small
                    # union eltypes (e.g. Union{String,Missing})
                    categorical(ifelse.(A .=== nothing, missing, A))
                elseif eltype(A) >: Nothing
                    categorical(replace(A, nothing=>missing))
                else
                    categorical(A)
                end
            end

            StructTypes.construct(::Type{<:CategoricalArray{Union{Missing, T}}},
                                A::AbstractVector) where {T} =
                CategoricalArray{Union{Missing, T}}(replace(A, nothing=>missing))
            StructTypes.construct(::Type{<:CategoricalArray{Union{Missing, T}}},
                                A::Vector) where {T} =
                CategoricalArray{Union{Missing, T}}(replace(A, nothing=>missing))
        end
    end
end


text:
# This file was generated, do not modify it.

using MLJ
using PrettyPrinting
import DataFrames: DataFrame, select!, Not, describe
import Statistics
using Dates
using PyPlot
ioff() # hide
using UrlDownload

MLJ.color_off() # hide

df = DataFrame(urldownload("https://raw.githubusercontent.com/tlienart/DataScienceTutorialsData.jl/master/data/kc_housing.csv", true))
describe(df)

select!(df, Not([:id, :date]))
schema(df)

coerce!(df, :zipcode => Multiclass)
df.isrenovated  = @. !iszero(df.yr_renovated)
df.has_basement = @. !iszero(df.sqft_basement)
schema(df)

coerce!(df, :isrenovated => OrderedFactor, :has_basement => OrderedFactor);

unique(df.waterfront)

df.waterfront = (df.waterfront .!= "FALSE")
coerce!(df, :waterfront => OrderedFactor);

coerce!(df, autotype(df, :few_to_finite))
schema(df)

df.price = df.price ./ 1000;

select!(df, Not([:yr_renovated, :sqft_basement, :zipcode]));

plt.figure(figsize=(8,6))
plt.hist(df.price, color = "blue", edgecolor = "white", bins=50,
         density=true, alpha=0.5)
plt.xlabel("Price", fontsize=14)
plt.ylabel("Frequency", fontsize=14)
plt.savefig(joinpath(@OUTPUT, "hist_price.svg")) # hide

plt.figure(figsize=(8,6))
plt.hist(df.price[df.isrenovated .== true], color="blue", density=true,
        edgecolor="white", bins=50, label="renovated", alpha=0.5)
plt.hist(df.price[df.isrenovated .== false], color="red", density=true,
        edgecolor="white", bins=50, label="unrenovated", alpha=0.5)
plt.xlabel("Price", fontsize=14)
plt.ylabel("Frequency", fontsize=14)
plt.legend(fontsize=12)
plt.savefig(joinpath(@OUTPUT, "hist_price2.svg")) # hide

@load DecisionTreeRegressor

y, X = unpack(df, ==(:price), col -> true)
train, test = partition(eachindex(y), 0.7, shuffle=true, rng=5)

tree = machine(DecisionTreeRegressor(), X, y)

fit!(tree, rows=train);

rms(y[test], predict(tree, rows=test))

@load RandomForestRegressor pkg=ScikitLearn

coerce!(X, Finite => Count);

rf_mdl = RandomForestRegressor()
rf = machine(rf_mdl, X, y)
fit!(rf, rows=train)

rms(y[test], predict(rf, rows=test))

cv3 = CV(; nfolds=3)
res = evaluate(rf_mdl, X, y, resampling=CV(shuffle=true),
               measure=rms, verbosity=0)

@load XGBoostRegressor

coerce!(X, Count => Continuous)

xgb  = XGBoostRegressor()
xgbm = machine(xgb, X, y)
fit!(xgbm, rows=train)

rms(y[test], predict(xgbm, rows=test))

r1 = range(xgb, :max_depth, lower=3, upper=10)
r2 = range(xgb, :num_round, lower=1, upper=25);

tm = TunedModel(model=xgb, tuning=Grid(resolution=7),
                resampling=CV(rng=11), ranges=[r1,r2],
                measure=rms)
mtm = machine(tm, X, y)
fit!(mtm, rows=train)

rms(y[test], predict(mtm, rows=test))

PyPlot.close_figs() # hide


text:
println("Using Revise")
using Revise


text:
using Mill, JSON, BSON, Flux, JsonGrinder, Test, HierarchicalUtils
using JsonGrinder: suggestextractor, schema
using JsonGrinder: DictEntry, Entry, MultiEntry, ArrayEntry
using Mill: reflectinmodel

@testset "Basic behavior testing" begin
	j1 = JSON.parse("""{"a": 4, "b": {"a":[1,2,3],"b": 1},"c": { "a": {"a":[1,2,3],"b":[4,5,6]}}}""",inttype=Float64)
	j2 = JSON.parse("""{"a": 4, "c": { "a": {"a":[2,3],"b":[5,6]}}}""")
	j3 = JSON.parse("""{"a": 4, "b": {"a":[1,2,3],"b": 1}}""")
	j4 = JSON.parse("""{"a": 4, "b": {}}""")
	j5 = JSON.parse("""{"b": {}}""")
	j6 = JSON.parse("""{}""")

	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6])

	@test sch[:a].counts == Dict(4 => 4)
	@test sch[:a].updated == 4
	@test sch[:b].updated == 4
	@test sch[:b][:a].updated == 2
	@test sch[:b][:a].l == Dict(3 => 2)
	@test sch[:b][:a].items.counts == Dict(1 => 2, 2 => 2, 3 => 2)
	@test sch[:b][:a].items.updated == 6
	@test sch[:b][:b].counts == Dict(1 => 2)
	@test sch[:b][:b].updated == 2
	@test sch[:c].updated == 2
	@test sch[:c][:a].updated == 2
	@test sch[:c][:a][:a].updated == 2
	@test sch[:c][:a][:a].l == Dict(2 => 1, 3 => 1)
	@test sch[:c][:a][:a].items.counts == Dict(1 => 1, 2 => 2, 3 => 2)
	@test sch[:c][:a][:a].items.updated == 5
	@test sch[:c][:a][:b].updated == 2
	@test sch[:c][:a][:b].l == Dict(2 => 1, 3 => 1)
	@test sch[:c][:a][:b].items.counts == Dict(4 => 1, 5 => 2, 6 => 2)
	@test sch[:c][:a][:b].items.updated == 5

	@test keys(sch[:a]) == [4]
end

@testset "Empty string vs missing key testing" begin
	j1 = JSON.parse("""{"a": "b", "b": ""}""")
	j2 = JSON.parse("""{"a": "a", "b": "c"}""")
	sch1 = JsonGrinder.schema([j1,j2])
	@test sch1[:a].updated == 2
	@test sch1[:b].updated == 2
	@test sch1.updated == 2

	j1 = JSON.parse("""{"a": "b"}""")
	j2 = JSON.parse("""{"a": "a", "b": "c"}""")
	sch2 = JsonGrinder.schema([j1,j2])
	@test sch2[:a].updated == 2
	@test sch2[:b].updated == 1
	@test sch2.updated == 2

	ext1 = suggestextractor(sch1)
	ext2 = suggestextractor(sch2)

	m1 = reflectinmodel(sch1, ext1)
	m2 = reflectinmodel(sch2, ext2)

	@test m1[:b].m isa Dense
	@test m1[:b].m.W isa Matrix
	# this is for stable
	# @test m2[:b].m isa Dense
	# @test m2[:b].m.W isa PostImputingMatrix
	# this is for master
	@test m2[:b].m isa PostImputingDense
	@test m2[:b].m.W isa PostImputingMatrix
	@test buf_printtree(m1) != buf_printtree(m2)
end

@testset "Irregular schema" begin
	j1 = JSON.parse("""{"a": 4}""")
	j2 = JSON.parse("""{"a": { "a": "hello", "b":[5,6]}}""")
	j3 = JSON.parse("""{"a": [1, 2, 3 , 4]}""")
	# todo: add tests for array and empty dict, and which extractors it generates
	# basically test sth. like this
	# j1 = JSON.parse("""{"a": 4, "b": [{"a":[1,2,3],"b": 1}],"c": { "a": {"a":[1,2,3],"b":[4,5,6]}}}""",inttype=Float64)
	# j2 = JSON.parse("""{"a": 4, "c": {"a": {"a":[2,3],"b":[5,6]}}}""")
	# j3 = JSON.parse("""{"a": 4, "b": [{"a":[1,2,3],"b": 1}]}""")
	# j4 = JSON.parse("""{"a": 4, "b": [{}]}""")
	# j5 = JSON.parse("""{"b": {}}""")
	# j6 = JSON.parse("""{}""")

	sch = JsonGrinder.DictEntry()
	JsonGrinder.update!(sch, j1)
	@test typeof(sch[:a]) <: Entry
	JsonGrinder.update!(sch, j2)
	JsonGrinder.update!(sch, j3)
	@test typeof(sch[:a]) <: MultiEntry
	@test typeof(sch[:a][1]) <: Entry
	@test typeof(sch[:a][2]) <: DictEntry
	@test typeof(sch[:a][3]) <: ArrayEntry

	jsons = [j1, j2, j3]
	types = [Entry, DictEntry, ArrayEntry]
	for ii in [[1,2,3], [3,2,1], [2,3,1],[2,1,3] , [1,3,2]]
		sch = JsonGrinder.DictEntry()
		JsonGrinder.update!(sch, jsons[ii[1]])
		@test typeof(sch[:a]) <: types[ii[1]]
		JsonGrinder.update!(sch, jsons[ii[2]])
		JsonGrinder.update!(sch, jsons[ii[3]])
		@test typeof(sch[:a]) <: MultiEntry
		@test typeof(sch[:a][1]) <: types[ii[1]]
		@test typeof(sch[:a][2]) <: types[ii[2]]
		@test typeof(sch[:a][3]) <: types[ii[3]]
	end

	jsons = [Dict("a" => [j["a"]]) for j in jsons]
	for ii in [[1,2,3], [3,2,1], [2,3,1],[2,1,3] , [1,3,2]]
		sch = JsonGrinder.DictEntry()
		JsonGrinder.update!(sch, jsons[ii[1]])
		@test typeof(sch[:a]) <: ArrayEntry
		@test typeof(sch[:a].items) <: types[ii[1]]
		JsonGrinder.update!(sch, jsons[ii[2]])
		JsonGrinder.update!(sch, jsons[ii[3]])
		@test typeof(sch[:a].items)  <: MultiEntry
		@test typeof(sch[:a].items[1]) <: types[ii[1]]
		@test typeof(sch[:a].items[2]) <: types[ii[2]]
		@test typeof(sch[:a].items[3]) <: types[ii[3]]
	end

	j1 = JSON.parse("""{"a": 4}""")
	j2 = JSON.parse("""{"a": "hello"}""")
	sch = JsonGrinder.DictEntry()
	JsonGrinder.update!(sch, j1)
	@test typeof(sch[:a]) <: Entry
	JsonGrinder.update!(sch, j2)

end

@testset "Empty arrays" begin
	j1 = JSON.parse("""{"a": []}""")
	j2 = JSON.parse("""{"a": [{"a":1},{"b":2}]}""")
	j3 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a" : 1}]}""")
	j4 = JSON.parse("""{"a": [{"a":2,"b":3}]}""")

	sch1 = JsonGrinder.schema([j1])
	sch2 = JsonGrinder.schema([j1,j2,j3])
	sch3 = JsonGrinder.schema([j2,j3,j1])

	@test sch1.updated == 1
	@test keys(sch1) == Set([:a])
	ext1 = suggestextractor(sch1)
	@test isnothing(ext1)

	@test sch2.updated == sch3.updated
	@test sch2[:a].l == sch3[:a].l
	@test sch2[:a].updated == sch3[:a].updated
	@test sch2[:a].items[:a].updated == sch3[:a].items[:a].updated
	@test sch2[:a].items[:a].counts == sch3[:a].items[:a].counts

	ext2 = suggestextractor(sch2)
	ext3 = suggestextractor(sch3)
	@test ext2 == ext3
end

@testset "More empty arrays" begin
	j1 = JSON.parse("""{"a": [{"a":1},{"b":2}], "b":[]}""")
	j2 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a":1}], "b":[]}""")

	sch1 = JsonGrinder.schema([j1,j2])
	ext1 = suggestextractor(sch1)
	m = reflectinmodel(sch1, ext1)
	# todo: add tests that b is not there
	@test sch1.updated == 2
	@test sch1[:a].updated == 2
	@test :b ∈ keys(sch1)
	@test :b ∉ keys(ext1)
	@test :b ∉ keys(m)
end

@testset "Empty Arrays with multientry" begin
	j1 = JSON.parse("""{"a": [{"a":1},{"b":2}], "b":[]}""")
	j2 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a":1}], "b":{}}""")
	j3 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a":1}], "b":1}""")
	j4 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a":1}], "b":"string"}""")

	sch1 = JsonGrinder.schema([j1,j2,j3,j4])
	ext1 = suggestextractor(sch1)
	m = reflectinmodel(sch1, ext1)
	# todo: add tests that b is not there
	@test [1,2,3,4] == keys(sch1[:b])
	@test (:e1, :e2) == keys(ext1[:b])
	@test (:e1, :e2) == keys(m[:b])
end

@testset "Schema merging" begin
	j1 = JSON.parse("""{"a": [{"a":1},{"b":2}]}""")
	j2 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a" : 1}]}""")
	j3 = JSON.parse("""{"a": [{"a":2,"b":3}]}""")
	j4 = JSON.parse("""{"a": []}""")
	j5 = JSON.parse("""{}""")
	j6 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a" : 1}], "b": 1}""")

	# todo: test how newentry works with array with multiple elements
	sch1 = JsonGrinder.schema([j1,j2,j3])
	sch2 = JsonGrinder.schema([j4,j5,j6])

	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6])
	sch_merged = merge(sch1, sch2)

	@test sch.updated == sch_merged.updated
	@test sch[:a].updated == sch_merged[:a].updated
	@test sch[:a].l == sch_merged[:a].l
	@test sch[:a].items[:a].updated == sch_merged[:a].items[:a].updated
	@test sch[:a].items[:a].counts == sch_merged[:a].items[:a].counts
	@test sch[:a].items[:b].updated == sch_merged[:a].items[:b].updated
	@test sch[:a].items[:b].counts == sch_merged[:a].items[:b].counts
	@test sch[:b].updated == sch_merged[:b].updated
	@test sch[:b].counts == sch_merged[:b].counts
end

@testset "Bson and symbol keys testing" begin
	b1 = IOBuffer()
	j1 = BSON.bson(b1, Dict(:a=>4, :b=>Dict(:a=>[1,2,3], :b=>1), :c=>Dict(:a=>Dict(:a=>[1,2,3],:b=>[4,5,6]))))
	b2 = IOBuffer()
	j2 = BSON.bson(b2, Dict(:a=>4,:c=>Dict(:a=>Dict(:a=>[2,3],:b=>[5,6]))))
	b3 = IOBuffer()
	j3 = BSON.bson(b3, Dict(:a=>4,:b=>Dict(:a=>[1,2,3],:b=>1)))
	b4 = IOBuffer()
	j4 = BSON.bson(b4, Dict(:a=>4,:b=>Dict()))
	b5 = IOBuffer()
	j5 = BSON.bson(b5, Dict(:b=>Dict()))
	b6 = IOBuffer()
	j6 = BSON.bson(b6, Dict())
	bs = [(seek(b, 0); BSON.load(b)) for b in [b1,b2,b3,b4,b5,b6]]
	sch = JsonGrinder.schema(bs)

	@test sch[:a].counts == Dict(4 => 4)
	@test sch[:a].updated == 4
	@test sch[:b].updated == 4
	@test sch[:b][:a].updated == 2
	@test sch[:b][:a].l == Dict(3 => 2)
	@test sch[:b][:a].items.counts == Dict(1 => 2, 2 => 2, 3 => 2)
	@test sch[:b][:a].items.updated == 6
	@test sch[:b][:b].counts == Dict(1 => 2)
	@test sch[:b][:b].updated == 2
	@test sch[:c].updated == 2
	@test sch[:c][:a].updated == 2
	@test sch[:c][:a][:a].updated == 2
	@test sch[:c][:a][:a].l == Dict(2 => 1, 3 => 1)
	@test sch[:c][:a][:a].items.counts == Dict(1 => 1, 2 => 2, 3 => 2)
	@test sch[:c][:a][:a].items.updated == 5
	@test sch[:c][:a][:b].updated == 2
	@test sch[:c][:a][:b].l == Dict(2 => 1, 3 => 1)
	@test sch[:c][:a][:b].items.counts == Dict(4 => 1, 5 => 2, 6 => 2)
	@test sch[:c][:a][:b].items.updated == 5
end

@testset "Equals and hash test" begin
	j1 = JSON.parse("""{"a": []}""")
	j2 = JSON.parse("""{"a": [{"a":1},{"b":2}]}""")
	j3 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a" : 1}]}""")
	j4 = JSON.parse("""{"a": [{"a":2,"b":3}]}""")

	sch0 = JsonGrinder.DictEntry()
	sch1 = JsonGrinder.schema([j1, j2, j3, j4])
	sch2 = JsonGrinder.schema([j1, j2, j3, j4])
	sch3 = JsonGrinder.schema([j1, j2, j3])

	@test hash(sch1) === hash(sch2)
	@test hash(sch1) !== hash(sch3)
	@test hash(sch1) !== hash(sch0)
	@test hash(sch3) !== hash(sch0)

	@test sch1 == sch2
	@test sch1 != sch3
	@test sch1 != sch0
	@test sch3 != sch0
end

@testset "Schema merging with max keys" begin
	j1 = JSON.parse("""{"a": [{"a":1},{"b":2}]}""")
	j2 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a":1}]}""")
	j3 = JSON.parse("""{"a": [{"a":2,"b":3}]}""")
	j4 = JSON.parse("""{"a": []}""")
	j5 = JSON.parse("""{}""")
	j6 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a":1}], "b":1}""")
	j7 = JSON.parse("""{"a": [{"a":4,"b":5},{"b":6,"a":7}], "b":2}""")
	j8 = JSON.parse("""{"a": [{"a":9,"b":10},{"b":11,"a":12}], "b":2}""")
	j9 = JSON.parse("""{"a": [{"a":4,"b":3},{"b":2,"a":2}], "b":2}""")
	j10 = JSON.parse("""{"a": [{"a":11,"b":12},{"b":13,"a":14}], "b":2}""")
	j11 = JSON.parse("""{"a": [{"a":7,"b":5},{"b":6,"a":6}], "b":2}""")

	prev_keys = JsonGrinder.max_keys
	JsonGrinder.updatemaxkeys!(6)
	# todo: test how newentry works with array with multiple elements
	sch1 = JsonGrinder.schema([j1,j2,j3,j4,j5,j11])
	sch2 = JsonGrinder.schema([j6,j7,j8,j9,j10])

	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6,j7,j8,j9,j10,j11])
	sch_merged = merge(sch1, sch2)

	@test sch == sch_merged
	JsonGrinder.updatemaxkeys!(prev_keys)
end

@testset "Sample synthetic" begin
	j1 = JSON.parse("""{"a": []}""")
	j2 = JSON.parse("""{"a": [{"a":1},{"b":2}]}""")
	j3 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a":1}]}""")
	j4 = JSON.parse("""{"a": [{"a":2,"b":3}]}""")

	sch1 = JsonGrinder.schema([j1, j2, j3, j4])
	@test JsonGrinder.sample_synthetic(sch1) == Dict(:a=>[Dict(:a=>2,:b=>2), Dict(:a=>2,:b=>2)])

end

@testset "Sample synthetic with missing keys in dict" begin
	j1 = JSON.parse("""{"a": []}""")
	j2 = JSON.parse("""{"a": [{"a":"a","c":1},{"b":2,"c":1}]}""")
	j3 = JSON.parse("""{"a": [{"a":"b","b":3,"c":1},{"b":2,"a":"d","c":1}]}""")
	j4 = JSON.parse("""{"a": [{"a":"c","c":1}]}""")

	sch1 = JsonGrinder.schema([j1, j2, j3, j4])
	@test JsonGrinder.sample_synthetic(sch1, empty_dict_vals=false) == Dict(
		:a=>[Dict(:a=>"c",:b=>2,:c=>1), Dict(:a=>"c",:b=>2,:c=>1)]
	)
	@test isequal(JsonGrinder.sample_synthetic(sch1, empty_dict_vals=true), Dict(
		:a=>[Dict(:a=>missing,:b=>missing,:c=>1), Dict(:a=>missing,:b=>missing,:c=>1)]
	))

	ext1 = suggestextractor(sch1)
	m = reflectinmodel(sch1, ext1)
	# now I test that all outputs are numbers. If some output was missing, it would mean model does not have imputation it should have
	@test m(ext1(j1)).data isa Matrix{Float32}
	@test m(ext1(j2)).data isa Matrix{Float32}
	@test m(ext1(j3)).data isa Matrix{Float32}
	@test m(ext1(j4)).data isa Matrix{Float32}
end

@testset "Merge empty lists" begin
	j1 = JSON.parse("""{"a": [{"a":1},{"b":2}], "b": []}""")
	j2 = JSON.parse("""{"a": [{"a":3},{"b":4}], "b": []}""")
	j3 = JSON.parse("""{"a": [{"a":1},{"b":3}], "b": []}""")
	j4 = JSON.parse("""{"a": [{"a":2},{"b":4}], "b": [1]}""")

	sch = JsonGrinder.schema([j1, j2, j3, j4])
	sch123 = JsonGrinder.schema([j1, j2, j3])
	sch12 = JsonGrinder.schema([j1, j2])
	sch3 = JsonGrinder.schema([j3])
	sch4 = JsonGrinder.schema([j4])
	sch_merged123 = merge(sch12, sch3)
	sch_merged1234 = merge(sch12, sch3, sch4)
	@test sch == sch_merged1234
	@test sch123 == sch_merged123
end

@testset "Symmetry" begin
	j1 = JSON.parse("""{"a": [{"a":1},{"b":2}], "b": []}""")
	j2 = JSON.parse("""{"a": [{"a":3},{"b":4}], "b": [1]}""")
	j3 = JSON.parse("""{"a": [{"a":1},{"b":3}], "b": []}""")

	sch1 = JsonGrinder.schema([j1, j2, j3])
	sch2 = JsonGrinder.schema([j2, j1, j3])
	sch3 = JsonGrinder.schema([j3, j2, j1])
	sch4 = JsonGrinder.schema([j1, j3, j2])
	@test sch1 == sch2
	@test sch1 == sch3
	@test sch2 == sch3
end

@testset "Fail empty bag extractor" begin
	ex = JsonGrinder.newentry([])
	@test isnothing(suggestextractor(ex))
end

@testset "Delete in path" begin
	j1 = JSON.parse("""{"a": 4, "b": [{"a":[1,2,3],"b": 1}],"c": { "a": {"a":[1,2,3],"b":[4,5,6]}}}""",inttype=Float64)
	j2 = JSON.parse("""{"a": 4, "c": {"a": {"a":[2,3],"b":[5,6]}}}""")
	j3 = JSON.parse("""{"a": 4, "b": [{"a":[1,2,3],"b": 1}]}""")
	j4 = JSON.parse("""{"a": 4, "b": [{}]}""")
	j5 = JSON.parse("""{"b": {}}""")
	j6 = JSON.parse("""{}""")

	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6])
	@test children(sch[:c][:a]) == [:a=>sch[:c][:a][:a], :b=>sch[:c][:a][:b]]
	delete!(sch, ".c.a", "a")
	@test children(sch[:c][:a]) == [:b=>sch[:c][:a][:b]]
	@test children(sch[:b][1].items) == [:a=>sch[:b][1].items[:a], :b=>sch[:b][1].items[:b]]
	delete!(sch, ".b.1.[]", "a")
	@test children(sch[:b][1].items) == [:b=>sch[:b][1].items[:b]]

	suggestextractor(sch)
	# todo: add tests for this is identity is used for single-keyed multi-entry
	# todo: test indentity for multiple nested single-keyed dicts
end

@testset "Extractor from schema" begin
	j1 = JSON.parse("""{"a": 4, "b": {"a":[1,2,3],"b": 1},"c": { "a": {"a":[1,2,3],"b":[4,5,6]}}}""",inttype=Float64)
	j2 = JSON.parse("""{"a": 4, "c": {"a": {"a":[2,3],"b":[5,6]}}}""")
	j3 = JSON.parse("""{"a": 4, "b": {"a":[1,2,3],"b": 1}}""")
	j4 = JSON.parse("""{"a": 4, "b": {}}""")
	j5 = JSON.parse("""{"b": {}}""")
	j6 = JSON.parse("""{}""")

	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6])
	ext = suggestextractor(sch)

	@test ext[:a] isa ExtractScalar{Float32}
	@test ext[:b][:a] isa ExtractVector{Float32}
	@test ext[:b][:b] isa ExtractScalar{Float32}
	@test ext[:c][:a][:a] isa ExtractArray{ExtractScalar{Float32}}
	@test ext[:c][:a][:b] isa ExtractArray{ExtractScalar{Float32}}

	e1 = ext(j1)
	@test e1[:a].data ≈ [0]
	@test e1[:b][:a].data ≈ [1, 2, 3]
	@test e1[:b][:b].data ≈ [0]
	@test e1[:c][:a][:a].data.data ≈ [0. 0.5 1.]
	@test e1[:c][:a][:b].data.data ≈ [0. 0.5 1.]
end

@testset "Mixing substrings with strings" begin
	a, b = split("a b")
	sch = JsonGrinder.schema([Dict("k" => a), Dict("k" => b), Dict("k" => "a"), Dict("k" => "b")])
	@test sch.childs[:k].counts == Dict("a"=>2, "b"=>2)
end

@testset "Merge inplace" begin
	j1 = JSON.parse("""{"a": 1}""")
	j2 = JSON.parse("""{"a": 2}""")
	j3 = JSON.parse("""{"a": 3}""")
	j4 = JSON.parse("""{"a": 1}""")
	j5 = JSON.parse("""{"a": 2}""")
	j6 = JSON.parse("""{"a": 3}""")

	# todo: otestovat jak funguje newentry s víceprvkovám polem
	sch1 = JsonGrinder.schema([j1,j2,j3])
	sch2 = JsonGrinder.schema([j4,j5,j6])
	sch1[:a]
	sch2[:a]
	@test sch1[:a].counts == Dict(2=>1,3=>1,1=>1)
	@test sch1[:a].updated == 3
	@test sch2[:a].counts == Dict(2=>1,3=>1,1=>1)
	@test sch2[:a].updated == 3
	JsonGrinder.merge_inplace!(sch1[:a], sch2[:a])
	@test sch1[:a].counts == Dict(2=>2,3=>2,1=>2)
	@test sch1[:a].updated == 6
end

@testset "Merging of irregular schemas" begin
	j1 = JSON.parse("""{"a": [{"a":1},{"b":2}]}""")
	j2 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a":1}]}""")
	j3 = JSON.parse("""{"a": [{"a":2,"b":3}]}""")
	j4 = JSON.parse("""{"a": []}""")
	j5 = JSON.parse("""{}""")
	j6 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a":1}], "b":1}""")
	j7 = JSON.parse("""{"a": [{"a":4,"b":5},{"b":6,"a":7}], "b":2}""")
	j8 = JSON.parse("""{"a": [{"a":9,"b":10},{"b":11,"a":12}], "b":2}""")
	j9 = JSON.parse("""{"a": 4, "b":2}""")
	j10 = JSON.parse("""{"a": 11, "b":2}""")
	j11 = JSON.parse("""{"a": 7, "b":2}""")

	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6,j7,j8,j9,j10,j11])
	sch11 = JsonGrinder.schema([j1,j2,j3,j4,j5,j6])
	sch12 = JsonGrinder.schema([j7,j8,j9])
	sch13 = JsonGrinder.schema([j10,j11])
	sch21 = JsonGrinder.schema([j1,j2,j3,j4,j5,j6,j7,j8])
	sch22 = JsonGrinder.schema([j9,j10,j11])
	sch31 = JsonGrinder.schema([j1,j2,j3,j4,j5,j6,j9])
	sch32 = JsonGrinder.schema([j8,j10])
	sch33 = JsonGrinder.schema([j11,j7])

	sch1 = merge(sch11, sch12, sch13)
	sch2 = merge(sch21, sch22)
	sch3 = merge(sch31, sch32, sch33)

	@test sch == sch1
	@test sch == sch2
	@test sch == sch3

	@test hash(sch) === hash(sch1)
	@test hash(sch) === hash(sch2)
	@test hash(sch) === hash(sch3)
end

@testset "Merging strings with numbers" begin
	j1 = JSON.parse("""{"a": "4", "b": "2"}""")
	j2 = JSON.parse("""{"a": "11", "b": "2"}""")
	j3 = JSON.parse("""{"a": 7, "b": "3"}""")
	j4 = JSON.parse("""{"a": 4, "b": 3}""")
	j5 = JSON.parse("""{"a": "11", "b": 3}""")
	j6 = JSON.parse("""{"a": "7", "b": 4}""")

	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6])
	sch11 = JsonGrinder.schema([j1,j2])
	sch12 = JsonGrinder.schema([j3,j4])
	sch13 = JsonGrinder.schema([j5,j6])
	sch21 = JsonGrinder.schema([j1,j2,j3])
	sch22 = JsonGrinder.schema([j4,j5,j6])

	sch1 = merge(sch11, sch12, sch13)
	sch2 = merge(sch21, sch22)

	@test sch == sch1
	@test sch == sch2

	@test hash(sch) === hash(sch1)
	@test hash(sch) === hash(sch2)
end

@testset "Schema merging with max keys and irregularities" begin
	j1 = JSON.parse("""{"a": [{"a":1},{"b":2}]}""")
	j2 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a":1}]}""")
	j3 = JSON.parse("""{"a": [{"a":2,"b":3}]}""")
	j4 = JSON.parse("""{"a": [1, 2, 3, 4, 5, 6, 7]}""")
	j5 = JSON.parse("""{}""")
	j6 = JSON.parse("""{"a": [{"a":1,"b":3},{"b":2,"a":2}], "b":4}""")
	j7 = JSON.parse("""{"a": [{"a":4,"b":5},{"b":6,"a":7}], "b":2}""")
	j8 = JSON.parse("""{"a": [{"a":9,"b":10},{"b":11,"a":12}], "b":"1"}""")
	j9 = JSON.parse("""{"a": [{"a":4,"b":3},{"b":2,"a":1}], "b":"2"}""")
	j10 = JSON.parse("""{"a": [{"a":11,"b":12},{"b":13,"a":"14"}], "b":"3"}""")
	j11 = JSON.parse("""{"a": [{"a":7,"b":5},{"b":6,"a":"6"}], "b":"4"}""")

	prev_keys = JsonGrinder.max_keys
	JsonGrinder.updatemaxkeys!(4)
	# todo: otestovat jak funguje newentry s víceprvkovám polem
	sch1 = JsonGrinder.schema([j1,j2,j3,j4,j5,j11])
	sch2 = JsonGrinder.schema([j6,j7,j8,j9,j10])

	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6,j7,j8,j9,j10,j11])
	sch_merged = merge(sch1, sch2)

	@test sch == sch_merged
	JsonGrinder.updatemaxkeys!(prev_keys)
end

@testset "Schema with string shortening" begin
	j1 = JSON.parse("""{"a": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}""")
	j2 = JSON.parse("""{"a": "b"}""")
	j3 = JSON.parse("""{"a": "🅱"}""")
	j4 = JSON.parse("""{"a": "aaaaaaaaaaa"}""")
	j5 = JSON.parse("""{"a": "aaaaaaaaaa"}""")
	j6 = JSON.parse("""{"a": "aaaaaaaaa"}""")
	j7 = JSON.parse("""{"a": "$("a"^100)"}""")

	max_string_len = 10
	sha1len = 40
	shorten_suffix = 1 + 3 + 1 + sha1len

	prev_len = JsonGrinder.max_len
	JsonGrinder.updatemaxlen!(max_string_len)
	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6,j7])
	@test sch[:a].counts |> keys .|> length |> maximum <= max_string_len + shorten_suffix
	@test max_string_len + shorten_suffix < 100		# sanity check that we actually shortened it

	JsonGrinder.updatemaxlen!(prev_len)
	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6,j7])
	@test sch[:a].counts |> keys .|> length |> maximum == 100
end

@testset "Schema with string shortening" begin
	j1 = JSON.parse("""{"a": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}""")
	j2 = JSON.parse("""{"a": "b"}""")
	j3 = JSON.parse("""{"a": "🅱"}""")
	j4 = JSON.parse("""{"a": "aaaaaaaaaaa"}""")
	j5 = JSON.parse("""{"a": "aaaaaaaaaa"}""")
	j6 = JSON.parse("""{"a": "aaaaaaaaa"}""")
	j7 = JSON.parse("""{"a": "$("a"^100)"}""")

	max_string_len = 10
	sha1len = 40
	shorten_suffix = 1 + 3 + 1 + sha1len

	prev_len = JsonGrinder.max_len
	JsonGrinder.updatemaxlen!(max_string_len)
	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6,j7])
	@test sch[:a].counts |> keys .|> length |> maximum <= max_string_len + shorten_suffix
	@test max_string_len + shorten_suffix < 100		# sanity check that we actually shortened it

	JsonGrinder.updatemaxlen!(prev_len)
	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6,j7])
	@test sch[:a].counts |> keys .|> length |> maximum == 100
end

@testset "Schema with fails" begin
	j1 = JSON.parse("""{"a": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}""")
	j2 = Dict("a"=>JsonGrinder)

	sch = JsonGrinder.schema([j1,j2])
	@test sch[:a].updated == 1
end

@testset "Schema with raw jsons" begin
	j1 = """{"a": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}"""
	j2 = """{"a": "b"}"""
	j3 = """{"a": "🅱"}"""
	j4 = """{"a": "aaaaaaaaaaa"}"""
	j5 = """{"a": "aaaaaaaaaa"}"""
	j6 = """{"a": "aaaaaaaaa"}"""
	j7 = """{"a": "$("a"^100)"}"""

	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6,j7])
	@test sch[:a].updated == 7
end

@testset "prune_json" begin
	j1 = JSON.parse("""{"a": 4, "b": {"a":[1,2,3],"b": 1},"c": { "a": {"a":[1,2,3],"b":[4,5,6]}}}""",inttype=Float64)
	j2 = JSON.parse("""{"a": 4, "c": { "a": {"a":[2,3],"b":[5,6]}}}""")
	j3 = JSON.parse("""{"a": 4, "b": {"a":[1,2,3],"b": 1}}""")
	j4 = JSON.parse("""{"a": 4, "b": {}}""")
	j5 = JSON.parse("""{"b": {}}""")
	j6 = JSON.parse("""{}""")

	sch = JsonGrinder.schema([j1,j2,j3,j4,j5,j6])

	@test JsonGrinder.prune_json(j1, sch) == Dict(
		"c" => Dict("a"=>Dict("b"=>[4.0, 5.0, 6.0],"a"=>[1.0, 2.0, 3.0])),
		"b" => Dict("b"=>1.0,"a"=>[1.0, 2.0, 3.0]),
		"a" => 4.0)

	@test JsonGrinder.prune_json(j2, sch) == Dict(
		"c" => Dict("a"=>Dict("b"=>[5, 6],"a"=>[2, 3])),
		"a" => 4)

	delete!(sch.childs, :b)

	@test JsonGrinder.prune_json(j1, sch) == Dict(
  		"c" => Dict("a"=>Dict("b"=>[4.0, 5.0, 6.0],"a"=>[1.0, 2.0, 3.0])),
  		"a" => 4.0)

	@test JsonGrinder.prune_json(j2, sch) == Dict(
		"c" => Dict("a"=>Dict("b"=>[5, 6],"a"=>[2, 3])),
		"a" => 4)

	delete!(sch.childs, :c)

	@test JsonGrinder.prune_json(j1, sch) == Dict(
  		"a" => 4.0)

	@test JsonGrinder.prune_json(j2, sch) == Dict(
		"a" => 4)

	j1 = JSON.parse("""{"a": 4, "b": {"a":1, "b": 1}}""")
	j2 = JSON.parse("""{"a": 4, "b": {"a":1}}""")
	sch = JsonGrinder.schema([j1,j2])

	j3 = Dict(
		"a" => 4,
		"b" => Dict("a"=>1),
		"c" => 1,
		"d" => 2,
	)

	@test JsonGrinder.prune_json(j1, sch) == j1
	@test JsonGrinder.prune_json(j2, sch) == j2
	@test JsonGrinder.prune_json(j3, sch) == Dict(
		"a"=>4,
		"b"=>Dict(
			"a" => 1
		)
	)
end


text:
using JGE
using SafeTestsets
using Test

@testset "Core" begin include("core/runtests.jl") end
@testset "Utils" begin include("utils/runtests.jl") end


text:
function optimize!(mod::LLVM.Module, tm)
    # everying except unroll, slpvec, loop-vec
    # then finish Julia GC
    ModulePassManager() do pm
        add_library_info!(pm, triple(mod))
        add_transform_info!(pm, tm)

        propagate_julia_addrsp!(pm)
        scoped_no_alias_aa!(pm)
        type_based_alias_analysis!(pm)
        basic_alias_analysis!(pm)
        cfgsimplification!(pm)
        # TODO: DCE (doesn't exist in llvm-c)
        scalar_repl_aggregates_ssa!(pm) # SSA variant?
        mem_cpy_opt!(pm)
        always_inliner!(pm)
        alloc_opt!(pm)
        instruction_combining!(pm)
        cfgsimplification!(pm)
        scalar_repl_aggregates_ssa!(pm) # SSA variant?
        instruction_combining!(pm)
        jump_threading!(pm)
        instruction_combining!(pm)
        reassociate!(pm)
        early_cse!(pm)
        alloc_opt!(pm)
        loop_idiom!(pm)
        loop_rotate!(pm)
        lower_simdloop!(pm)
        licm!(pm)
        loop_unswitch!(pm)
        instruction_combining!(pm)
        ind_var_simplify!(pm)
        loop_deletion!(pm)
        loop_unroll!(pm)
        alloc_opt!(pm)
        scalar_repl_aggregates_ssa!(pm) # SSA variant?
        gvn!(pm)
        # This InstCombine needs to be after GVN
        # Otherwise it will generate load chains in GPU code...
        instruction_combining!(pm)
        mem_cpy_opt!(pm)
        sccp!(pm)
        instruction_combining!(pm)
        jump_threading!(pm)
        dead_store_elimination!(pm)
        alloc_opt!(pm)
        cfgsimplification!(pm)
        loop_idiom!(pm)
        loop_deletion!(pm)
        jump_threading!(pm)
        # SLP_Vectorizer -- not for Enzyme
        aggressive_dce!(pm)
        instruction_combining!(pm)
        # Loop Vectorize -- not for Enzyme
        # InstCombine

        # GC passes
        barrier_noop!(pm)
        lower_exc_handlers!(pm)
        gc_invariant_verifier!(pm, false)

        # FIXME: Currently crashes printing
        cfgsimplification!(pm)
        instruction_combining!(pm) # Extra for Enzyme
        API.EnzymeAddAttributorLegacyPass(pm)
        run!(pm, mod)
    end
    # @show "omod", mod
    # flush(stdout)
    # flush(stderr)
end

# https://github.com/JuliaLang/julia/blob/2eb5da0e25756c33d1845348836a0a92984861ac/src/aotcompile.cpp#L603
function addTargetPasses!(pm, tm)
    add_library_info!(pm, LLVM.triple(tm))
    add_transform_info!(pm, tm)
end

# https://github.com/JuliaLang/julia/blob/2eb5da0e25756c33d1845348836a0a92984861ac/src/aotcompile.cpp#L620
function addOptimizationPasses!(pm)
    constant_merge!(pm)

    propagate_julia_addrsp!(pm)
    scoped_no_alias_aa!(pm)
    type_based_alias_analysis!(pm)
    basic_alias_analysis!(pm)
    cfgsimplification!(pm)
    dce!(pm)
    scalar_repl_aggregates!(pm)

    # mem_cpy_opt!(pm)

    always_inliner!(pm) # Respect always_inline

    # Running `memcpyopt` between this and `sroa` seems to give `sroa` a hard time
    # merging the `alloca` for the unboxed data and the `alloca` created by the `alloc_opt`
    # pass.

    alloc_opt!(pm)
    # consider AggressiveInstCombinePass at optlevel > 2

    instruction_combining!(pm)
    cfgsimplification!(pm)
    scalar_repl_aggregates!(pm)
    instruction_combining!(pm) # TODO: createInstSimplifyLegacy
    jump_threading!(pm)

    reassociate!(pm)

    early_cse!(pm)

    # Load forwarding above can expose allocations that aren't actually used
    # remove those before optimizing loops.
    alloc_opt!(pm)
    loop_rotate!(pm)
    # moving IndVarSimplify here prevented removing the loop in perf_sumcartesian(10:-1:1)
    loop_idiom!(pm)

    # LoopRotate strips metadata from terminator, so run LowerSIMD afterwards
    lower_simdloop!(pm) # Annotate loop marked with "loopinfo" as LLVM parallel loop
    licm!(pm)
    julia_licm!(pm)
    # Subsequent passes not stripping metadata from terminator
    instruction_combining!(pm) # TODO: createInstSimplifyLegacy
    ind_var_simplify!(pm)
    loop_deletion!(pm)
    loop_unroll!(pm) # TODO: in Julia createSimpleLoopUnroll

    # Run our own SROA on heap objects before LLVM's
    alloc_opt!(pm)
    # Re-run SROA after loop-unrolling (useful for small loops that operate,
    # over the structure of an aggregate)
    scalar_repl_aggregates!(pm)
    instruction_combining!(pm) # TODO: createInstSimplifyLegacy

    gvn!(pm)
    mem_cpy_opt!(pm)
    sccp!(pm)

    # Run instcombine after redundancy elimination to exploit opportunities
    # opened up by them.
    # This needs to be InstCombine instead of InstSimplify to allow
    # loops over Union-typed arrays to vectorize.
    instruction_combining!(pm)
    jump_threading!(pm)
    dead_store_elimination!(pm)

    # More dead allocation (store) deletion before loop optimization
    # consider removing this:
    alloc_opt!(pm)

    # see if all of the constant folding has exposed more loops
    # to simplification and deletion
    # this helps significantly with cleaning up iteration
    cfgsimplification!(pm)
    loop_deletion!(pm)
    instruction_combining!(pm)
    loop_vectorize!(pm)
    # TODO: createLoopLoadEliminationPass
    cfgsimplification!(pm)
    slpvectorize!(pm)
    # might need this after LLVM 11:
    # TODO: createVectorCombinePass()

    aggressive_dce!(pm)
end

function addMachinePasses!(pm)
    combine_mul_add!(pm)
    # TODO: createDivRemPairs[]

    demote_float16!(pm)
    gvn!(pm)
end

function addJuliaLegalizationPasses!(pm, lower_intrinsics=true)
    if lower_intrinsics
        # LowerPTLS removes an indirect call. As a result, it is likely to trigger
        # LLVM's devirtualization heuristics, which would result in the entire
        # pass pipeline being re-exectuted. Prevent this by inserting a barrier.
        barrier_noop!(pm)
        lower_exc_handlers!(pm)
        gc_invariant_verifier!(pm, false)

        # Needed **before** LateLowerGCFrame on LLVM < 12
        # due to bug in `CreateAlignmentAssumption`.
        remove_ni!(pm)
        late_lower_gc_frame!(pm)
        final_lower_gc!(pm)
        # We need these two passes and the instcombine below
        # after GC lowering to let LLVM do some constant propagation on the tags.
        # and remove some unnecessary write barrier checks.
        gvn!(pm)
        sccp!(pm)
        # Remove dead use of ptls
        dce!(pm)
        lower_ptls!(pm, #=dump_native=# false)
        instruction_combining!(pm)
        # Clean up write barrier and ptls lowering
        cfgsimplification!(pm)
    else
        barrier_noop!(pm)
        remove_ni!(pm)
    end
end

function post_optimze!(mod, tm)
    # @show "pre_post", mod
    # flush(stdout)
    # flush(stderr)
    LLVM.ModulePassManager() do pm
        addTargetPasses!(pm, tm)
        addOptimizationPasses!(pm)
        run!(pm, mod)
    end
    LLVM.ModulePassManager() do pm
        addJuliaLegalizationPasses!(pm, true)
        addMachinePasses!(pm)
        run!(pm, mod)
    end
    # @show "post_mod", mod
    # flush(stdout)
    # flush(stderr)
end


text:
@syms Sum(t::Number, i::Int, a::Int, b::Int)::Number

# get it?
function gensum(t,i,a,b)
    new_i = Sym{Int}(gensym(:i))
    new_t = substitute(t, Dict(i => new_i))
    return Sum(new_t, new_i, a, b)
end

function _codegen(::Type{T},s::SymbolicUtils.Sym{SymbolicUtils.FnType{Tuple{E,Int64,Int64,Int64},X}}, args...) where {T, E<: Number, X <: Number}
    @assert s.name == :Sum

    @gensym sum
    @gensym Δsum
    @gensym lo
    @gensym hi

    (summand, ix, ixlo, ixhi) = _codegen.(args)

    ex = @q begin
        $Δsum = $summand
        $sum += $Δsum
    end

    # Originally this part was in a `for` loop to allow for nested sums
    ex = @q begin
        $lo = $(ixlo)
        $hi = $(ixhi)
        @inbounds @fastmath for $(ix) in $lo:$hi
            $ex
        end
    end

    ex = @q begin
        $sum = 0.0
        $ex
        $sum
    end

    return ex
end

function tryfactor(sumfactors,i,a,b)
    d = Dict([t => i ∈ atoms(t) for t in sumfactors])
    # Which factors are independent of the index?
    indep = filter(t -> !d[t], sumfactors)
    isempty(indep) && return nothing

    # Start by factoring out the independent factors
    result = prod(indep)

    # Which factors depend on the index?
    dep = filter(t -> d[t], sumfactors)
    # Maybe none do, so we're already done
    isempty(dep) && return result * (b - a + 1)

    # Otherwise, multiply those to the result
    result *= gensum(prod(dep), i, a, b)

    return result
end

SUMRULES = [
    @rule Sum(+(~~x), ~i, ~a, ~b) => sum([gensum(t, ~i, ~a, ~b) for t in (~~x)])
    @rule Sum(*(~~x), ~i, ~a, ~b) => tryfactor(~~x, ~i, ~a, ~b) # ifelse(!_contains(~x,~i) || !_contains(~y,~i), Sum(~x, ~i, ~a, ~b) * Sum(~y, ~i, ~a, ~b), nothing)
    @rule Sum(~x, ~i, ~a, ~b) => ifelse(~i ∈ atoms(~x), nothing, ((~b) - (~a) + 1) * (~x))
]


text:
@testset "tsptwtrajectorystate.jl" begin

    @testset "TsptwTrajectoryState" begin
        graph = Matrix(adjacency_matrix(random_regular_graph(6,3)))
        ts1 = SeaPearl.TsptwTrajectoryState(
            GeometricFlux.FeaturedGraph(graph; nf=rand(3,6)),
            2,
            collect(3:6)
        )

        @test isa(ts1, SeaPearl.GraphTrajectoryState)
    end
end


text:
using JuMP, EAGO

                     m = Model()

                     EAGO.register_eago_operators!(m)

                     @variable(m, -1 <= x[i=1:2] <= 1)

                     @variable(m, -37.92418043260588 <= q <= 19.457860872935036)

                     add_NL_constraint(m, :(1/(1 + exp(-(-0.5948345021955626 + 0.7909419235855366*1/(1 + exp(-(0.16572281076834727 + 0.7701313689304001*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.3960198610694281*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.006655112331988011*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.5604852483433853*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.5611932928423773*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + -0.9350053029495258*1/(1 + exp(-(0.6292157071886879 + 0.7447123643459479*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.8071073342022652*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + 0.7192221993944883*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.1911667575027427*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.30728485701247665*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + 0.6696024550400765*1/(1 + exp(-(-0.5341808090129341 + 0.4032770914369115*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.6061337759397625*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + 0.5534397384266061*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.16208042832895408*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.7376214833764658*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + -0.9733717899660395*1/(1 + exp(-(0.43846326345255404 + -0.02862602361460853*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + 0.5029397949622734*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.20686891235628035*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.9777195341190259*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.2597965376833278*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + -0.07261217388131413*1/(1 + exp(-(0.02496868545483233 + -0.04485855107115411*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + 0.47774406442351136*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.5903487696420049*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.1719483424118926*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.8023455576856944*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2])))))))))) + 1/(1 + exp(-(-0.7044745597894595 + -0.85619817917634*1/(1 + exp(-(0.16572281076834727 + 0.7701313689304001*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.3960198610694281*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.006655112331988011*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.5604852483433853*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.5611932928423773*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + 0.37830231556965854*1/(1 + exp(-(0.6292157071886879 + 0.7447123643459479*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.8071073342022652*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + 0.7192221993944883*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.1911667575027427*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.30728485701247665*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + -0.12156626196118481*1/(1 + exp(-(-0.5341808090129341 + 0.4032770914369115*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.6061337759397625*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + 0.5534397384266061*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.16208042832895408*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.7376214833764658*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + -0.3744591738356289*1/(1 + exp(-(0.43846326345255404 + -0.02862602361460853*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + 0.5029397949622734*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.20686891235628035*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.9777195341190259*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.2597965376833278*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + -0.4985780257939494*1/(1 + exp(-(0.02496868545483233 + -0.04485855107115411*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + 0.47774406442351136*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.5903487696420049*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.1719483424118926*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.8023455576856944*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2])))))))))) + 1/(1 + exp(-(-0.9683275825894269 + -0.7637278723892034*1/(1 + exp(-(0.16572281076834727 + 0.7701313689304001*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.3960198610694281*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.006655112331988011*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.5604852483433853*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.5611932928423773*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + 0.9143661573707975*1/(1 + exp(-(0.6292157071886879 + 0.7447123643459479*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.8071073342022652*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + 0.7192221993944883*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.1911667575027427*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.30728485701247665*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + -0.7824888723979511*1/(1 + exp(-(-0.5341808090129341 + 0.4032770914369115*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.6061337759397625*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + 0.5534397384266061*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.16208042832895408*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.7376214833764658*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + -0.6866950641423624*1/(1 + exp(-(0.43846326345255404 + -0.02862602361460853*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + 0.5029397949622734*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.20686891235628035*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.9777195341190259*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.2597965376833278*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + -0.09633196432882363*1/(1 + exp(-(0.02496868545483233 + -0.04485855107115411*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + 0.47774406442351136*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.5903487696420049*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.1719483424118926*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.8023455576856944*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2])))))))))) + 1/(1 + exp(-(-0.5143076265809325 + -0.11312975790997548*1/(1 + exp(-(0.16572281076834727 + 0.7701313689304001*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.3960198610694281*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.006655112331988011*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.5604852483433853*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.5611932928423773*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + 0.7215302240627146*1/(1 + exp(-(0.6292157071886879 + 0.7447123643459479*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.8071073342022652*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + 0.7192221993944883*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.1911667575027427*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.30728485701247665*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + 0.8436266175918532*1/(1 + exp(-(-0.5341808090129341 + 0.4032770914369115*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.6061337759397625*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + 0.5534397384266061*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.16208042832895408*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.7376214833764658*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + -0.8029468027318871*1/(1 + exp(-(0.43846326345255404 + -0.02862602361460853*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + 0.5029397949622734*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.20686891235628035*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.9777195341190259*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.2597965376833278*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + 0.07363029133667531*1/(1 + exp(-(0.02496868545483233 + -0.04485855107115411*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + 0.47774406442351136*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.5903487696420049*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.1719483424118926*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.8023455576856944*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2])))))))))) + 1/(1 + exp(-(-0.8626839052591899 + -0.6726505975051698*1/(1 + exp(-(0.16572281076834727 + 0.7701313689304001*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.3960198610694281*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.006655112331988011*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.5604852483433853*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.5611932928423773*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + -0.7431517048783043*1/(1 + exp(-(0.6292157071886879 + 0.7447123643459479*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.8071073342022652*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + 0.7192221993944883*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.1911667575027427*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.30728485701247665*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + 0.03587478002549904*1/(1 + exp(-(-0.5341808090129341 + 0.4032770914369115*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + -0.6061337759397625*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + 0.5534397384266061*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.16208042832895408*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.7376214833764658*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + 0.7126331133171049*1/(1 + exp(-(0.43846326345255404 + -0.02862602361460853*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + 0.5029397949622734*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.20686891235628035*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.9777195341190259*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.2597965376833278*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2]))))))) + 0.7945114361960401*1/(1 + exp(-(0.02496868545483233 + -0.04485855107115411*1/(1 + exp(-(0.4458559142916436 + -0.1548130095620479*$(x[1]) + 0.5123092826047779*$(x[2])))) + 0.47774406442351136*1/(1 + exp(-(0.5904352841714982 + -0.7465945594038805*$(x[1]) + -0.5086687599184327*$(x[2])))) + -0.5903487696420049*1/(1 + exp(-(-0.6551208030223536 + -0.40103431116018706*$(x[1]) + -0.6543625861790279*$(x[2])))) + -0.1719483424118926*1/(1 + exp(-(-0.8605921328376098 + -0.6449201156921633*$(x[1]) + -0.9058095618930082*$(x[2])))) + -0.8023455576856944*1/(1 + exp(-(-0.4569139457142626 + -0.5316223845746624*$(x[1]) + 0.2651376452711478*$(x[2])))))))))) - $q <= 0.0))

                     @objective(m, Min, q)

                     return m

max_stars_repo_name: PSORLab/RSActivationFunctions

max_issues_repo_name: PSORLab/RSActivationFunctions

max_forks_repo_name: PSORLab/RSActivationFunctions


text:
using BitBasis, Random, LuxurySparse, SparseArrays, LinearAlgebra

⊗ = kron
PA = pmrand(2)
PB = pmrand(2)
PC = pmrand(2)
@test reorder(PC ⊗ PB ⊗ PA, [3,1,2]) ≈ PB ⊗ PA ⊗ PC
@test invorder(PC ⊗ PB ⊗ PA) ≈ PA ⊗ PB ⊗ PC

DA = Diagonal(randn(2))
DB = Diagonal(randn(2))
DC = Diagonal(randn(2))
@test reorder(DC ⊗ DB ⊗ DA, [3,1,2]) ≈ DB ⊗ DA ⊗ DC
@test invorder(DC ⊗ DB ⊗ DA) ≈ DA ⊗ DB ⊗ DC

DA = sprand(2,2,0.5)
DB = sprand(2,2, 0.5)
DC = sprand(2,2, 0.5)
@test reorder(DC ⊗ DB ⊗ DA, [3,1,2]) ≈ DB ⊗ DA ⊗ DC
@test invorder(DC ⊗ DB ⊗ DA) ≈ DA ⊗ DB ⊗ DC


text: println("Building Snarl...")


text:
 #module MultilevelGenesis

#############################################
###### Dependencies #########################
#############################################
using Random
#using Plots
using DataFrames
using JSON
using CSV
using Combinatorics

#############################################
###### Rection Functions ####################
#############################################

function CalculatePropensities(concentrations, CRS)
    ########## NOT UNIT TESTED! ##########
    PropensityVec = zeros(length(CRS.reaction_list))
    for rxn in CRS.reaction_list
        Ap = 0.0
        if rxn.propensity == "STD"
            Ap = STD_propensity(rxn, concentrations)
        end
        PropensityVec[rxn.index] = Ap
    end

    return PropensityVec
end

function PickReactionID(PropensityVec)
    Ap_tot = sum(PropensityVec)
    dice_roll = Ap_tot*rand()
    checkpoint = 0.0

    rID = 0
    while checkpoint < dice_roll
        rID += 1
        checkpoint += PropensityVec[rID]
    end
   return rID
end

function ExecuteReaction(concentrations, CRS, rID)
    ########## NOT UNIT TESTED! ##########
    rxn = CRS.reaction_list[rID]
    for (i, r) in enumerate(rxn.reactants)
        concentrations[r] -= rxn.react_coef[i]
    end
    for (j, p) in enumerate(rxn.products)
        concentrations[p] += rxn.prod_coef[j]
    end
    if any(x->x<0, concentrations)
        error("Concentrations went negative on reaction $rID")
    end

    return concentrations
end

function STD_propensity(rxn, concentrations)
    ########## NOT UNIT TESTED! ##########
    Ap  = rxn.rate_constant
    for (ri,r) in enumerate(rxn.reactants)
        rconc = concentrations[r]
        r_order = rxn.react_coef[ri]
        Ap = Ap*(prod([(rconc-i) for i in 0:(r_order-1)]))
    end
    cat_enhance = 0.0
    for (ci,c) in enumerate(rxn.catalysts)
        cconc = concentrations[c]
        cat_enhance += cconc*rxn.cat_coef[ci]
    end
    Ap = Ap*(1 + cat_enhance)
    return Ap
end

#############################################
##### Stochastic Evolution Functions ########
#############################################

function Run_Binary_Ligation(nA::Int64, nB::Int64, max_L::Int64, kf::Float64, kb::Float64, volume::Float64, tau_max::Float64, tau_freq::Float64 = 0.1, T::Float64 = 300.0, R::Float64= 8.3144598, mA::Int64 = 100, mB::Int64 = 100)
    ### Unique Experiment number required

    ## Check for the correct output files and directories
    param_file = "../data/Binary_Ligation_Run_Parameters.csv"
    save_dir = "../data/Binary_Ligation_timeseries"

    if !isdir(save_dir)
        mkdir(save_dir)
    end

    ## Initialize Time values
    tau = 0.0
    freq_count = 0.0

    binary_CRS = generate_binary_ligation_CRS(max_L, kf, kb, volume, T)
    ### Redo output initialization using DataFrames
    output_DF = InitializeOutput(binary_CRS)

    nmolecules = length(binary_CRS.molecule_list)
    concentrations= zeros(nmolecules)

    Aindex = binary_CRS.molecule_dict["A"]
    Bindex = binary_CRS.molecule_dict["B"]
    concentrations[Aindex] = nA
    concentrations[Bindex] = nB

    #Initialize Propensities
    propensities = CalculatePropensities(concentrations, binary_CRS)

    ####### Main LOOP #######
    while tau < tau_max
        # Pick Reaction
        rID = PickReactionID(propensities)
        # Execute Reaction
        concentrations = ExecuteReaction(concentrations, binary_CRS, rID)
        # Calculate Propensities
        propensities = CalculatePropensities(concentrations, binary_CRS)
        # Record Data
        if tau >= freq_count
            output_DF[Symbol(freq_count)] = concentrations[:]
            freq_count += tau_freq
            println(tau)
        end

        #Update Time
        Ap_tot = sum(propensities)
        tau -= (log(rand())/Ap_tot)
    end

    # Save time series and params
    save_name, parameter_df = generate_output_data(binary_CRS, save_dir)#
    println(parameter_df)
    if !isfile(param_file)
        CSV.write(param_file, parameter_df)
    else
        runs_df = CSV.read(param_file)
        runs_df = [runs_df; parameter_df]
    end

    CSV.write(save_name, output_DF)
    return concentrations
end
#############################################
#end #### module #############################
#############################################