function beta(obj::GNSSAttitudeContext, arg0::AbsoluteDate)
    return jcall(obj, "beta", jdouble, (AbsoluteDate,), arg0)
end

function beta_ds(obj::GNSSAttitudeContext)
    return jcall(obj, "betaDS", DerivativeStructure, ())
end

function compute_phi(obj::GNSSAttitudeContext, arg0::jdouble, arg1::jdouble)
    return jcall(obj, "computePhi", jdouble, (jdouble, jdouble), arg0, arg1)
end

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

function get_delta_ds(obj::GNSSAttitudeContext)
    return jcall(obj, "getDeltaDS", DerivativeStructure, ())
end

function get_mu_rate(obj::GNSSAttitudeContext)
    return jcall(obj, "getMuRate", jdouble, ())
end

function get_orbit_angle_since_midnight(obj::GNSSAttitudeContext)
    return jcall(obj, "getOrbitAngleSinceMidnight", jdouble, ())
end

function get_secured_beta(obj::GNSSAttitudeContext)
    return jcall(obj, "getSecuredBeta", jdouble, ())
end

function get_sv_bcos(obj::GNSSAttitudeContext)
    return jcall(obj, "getSVBcos", jdouble, ())
end

function get_turn_span(obj::GNSSAttitudeContext)
    return jcall(obj, "getTurnSpan", TurnSpan, ())
end

function get_yaw_end(obj::GNSSAttitudeContext, arg0::jdouble)
    return jcall(obj, "getYawEnd", jdouble, (jdouble,), arg0)
end

function get_yaw_start(obj::GNSSAttitudeContext, arg0::jdouble)
    return jcall(obj, "getYawStart", jdouble, (jdouble,), arg0)
end

function in_orbit_plane_absolute_angle(obj::GNSSAttitudeContext, arg0::jdouble)
    return jcall(obj, "inOrbitPlaneAbsoluteAngle", jdouble, (jdouble,), arg0)
end

function in_sun_side(obj::GNSSAttitudeContext)
    return jcall(obj, "inSunSide", jboolean, ())
end

function in_turn_time_range(obj::GNSSAttitudeContext)
    return jcall(obj, "inTurnTimeRange", jboolean, ())
end

function linear_model_still_active(obj::GNSSAttitudeContext, arg0::jdouble, arg1::jdouble)
    return jcall(obj, "linearModelStillActive", jboolean, (jdouble, jdouble), arg0, arg1)
end

function nominal_yaw(obj::GNSSAttitudeContext, arg0::AbsoluteDate)
    return jcall(obj, "nominalYaw", TimeStampedAngularCoordinates, (AbsoluteDate,), arg0)
end

function orbit_normal_yaw(obj::GNSSAttitudeContext)
    return jcall(obj, "orbitNormalYaw", TimeStampedAngularCoordinates, ())
end

function set_half_span(obj::GNSSAttitudeContext, arg0::jdouble, arg1::jdouble)
    return jcall(obj, "setHalfSpan", void, (jdouble, jdouble), arg0, arg1)
end

function set_up_turn_region(obj::GNSSAttitudeContext, arg0::jdouble, arg1::jdouble)
    return jcall(obj, "setUpTurnRegion", jboolean, (jdouble, jdouble), arg0, arg1)
end

function time_since_turn_start(obj::GNSSAttitudeContext)
    return jcall(obj, "timeSinceTurnStart", jdouble, ())
end

function turn_corrected_attitude(obj::GNSSAttitudeContext, arg0::DerivativeStructure)
    return jcall(obj, "turnCorrectedAttitude", TimeStampedAngularCoordinates, (DerivativeStructure,), arg0)
end

function turn_corrected_attitude(obj::GNSSAttitudeContext, arg0::jdouble, arg1::jdouble)
    return jcall(obj, "turnCorrectedAttitude", TimeStampedAngularCoordinates, (jdouble, jdouble), arg0, arg1)
end

function yaw_rate(obj::GNSSAttitudeContext, arg0::jdouble)
    return jcall(obj, "yawRate", jdouble, (jdouble,), arg0)
end


struct Sum{H} end
struct LocalOperator{T, N, O, MT} end
struct SubHam{TT, OP} end
struct Vertex end

const Alias{T, MT} = Sum{SubHam{NTuple{1, Vertex}, LocalOperator{T, 1, 2, MT}}}

Alias{Float64}


# https://github.com/vtjnash/Glob.jl/issues/11
rglob(pat, topdir) = Base.Iterators.flatten(map(d -> glob(pat, d[1]), walkdir(topdir)))

function parse_obsxlsx(obs_path::String, input_dir::String)
    ws = readxlsheet(string(obs_path), "2017년")

    re_opened = r"신규"
    re_closed = r"폐쇄"
    re_moved = r"이전"
    re_hangul = r"[가-힣]+"
    re_opened_date = r"(\d+)\.\W*(\d+)\.?"
    #re_closed_date = r"(\d+)\.\W*(\d+)\.?\W*(\d+)\."
    re_closed_date = r"(\d+)\.\W*(\d+)\.?(\W*(\d+)\.?)*"
    KST = tz"Asia/Seoul"
    df = DataFrame(stationCode=[], stationName=[], oDate=[], cDate=[], lon=[], lat=[])
    cnt = 0

    #=
        row[1] = state (시, 도)
        row[2] = city (도시)
        row[3] = stationCode (측정소코드)
        row[4] = stationName (측정소명)
        row[5] = address (주소)
        row[6] = langitude (경도)
        row[7] = latitude (위도)
        row[8] = remakrs (비고)
    =#

    for i in 1:size(ws, 1)
        # just pick old date
        opened_date = ZonedDateTime(1970, 1, 1, tz"Asia/Seoul")
        # close value to maximum value in Int64
        closed_date = ZonedDateTime(2037, 12, 31, tz"Asia/Seoul")

        row = ws[i, :]
        # prev_row = ws[i - 1, :]
        # if stationCode is blank, skip, so no move
        if isa(row[3], DataValue) && isna(row[3])
            continue
        end

        # if there is not stationCode (i.e. wrong column), skip loop
        try
            if isa(row[3], String)
                station_code = parse(Int, row[3])
            elseif isa(row[3], AbstractFloat)
                station_code = Int(row[3])
            end
        catch e
            if isa(e, ArgumentError)
                continue
            end
        end

        # check lat / long
        if isa(row[6], Real) && isa(row[7], Real)
            station_code = 0
            station_name = ""
            long_X = float(row[6])
            lat_Y = float(row[7])

            cnt += 1

            # parse station code and name, doesn't support moved case
            if isa(row[3], String)
                station_code = parse(Int, row[3])
            elseif isa(row[3], AbstractFloat)
                station_code = Int(row[3])
            end

            if occursin(re_hangul, string(row[4]))
                station_name = string(row[4])
            end

            # parse date of station opened
            # check there is a stationCode and "신규"(opened new station) in remarks
            if occursin(re_opened, string(row[8]))
                # parse date (year & month only)
                m = match(re_opened_date, string(row[8]))
                opened_year = parse(Int, m.captures[1])
                opened_month = parse(Int, m.captures[2])

                opened_date = ZonedDateTime(opened_year, opened_month, 1, tz"Asia/Seoul")
            end

            # parse date of station closed
            if occursin(re_closed, string(row[8]))
                m = match(re_closed_date, string(row[8]))
                closed_year = parse(Int, m.captures[1])
                closed_month = parse(Int, m.captures[2])
                closed_day = m.captures[4]
                if closed_day == nothing
                    closed_day = day(Dates.lastdayofmonth(DateTime(closed_year, closed_month)))
                else
                    closed_day = parse(Int, m.captures[4])
                end

                closed_date = ZonedDateTime(closed_year, closed_month, closed_day, tz"Asia/Seoul")
            end

            # parse date of station moved
            #=
            if hasvalue(row[3]) == true && occursin(re_moved, string(row[8]))
                if row[3] !=
                    station_code = int(row[3])
                    station_name = string(row[4])
                else
                    station_code = int(prev_row[3])
                    station_name = string(prev_row[4])
                end

                m = match(re_closed_date, string(row[8])
                moved_year = m.captures[1]
                moved_month = m.captures[2]
                opened_date = ZonedDateTime(int(moved_year), int(moved_month), 1, 0, 0, tz"Asia/Seoul")

                # compute closed date of modified one
                closed_date_prev = opened_date - Minute(1)

                # find exising one and modify closed_date
                # TODO: not sure how to do if there are multiple movement of station
                prev = @from i in stn_df begin
                    @where i.statioCode == station_code
                    @select i
                    @collect DataFrame
                end

                station_code = Msrstn_list[prev_msrstn_idx][stationCode]
                station_name = Msrstn_list[prev_msrstn_idx][stationName]

                Msrstn_list[prev_Msrstn_idx][closedDate] = closed_date_ex.strftime(strftime_fmt)
            end
            =#

            # check type
            @assert isa(station_code, Int)
            @assert isa(long_X, Real)
            @assert isa(lat_Y, Real)
            push!(df, (station_code, station_name, opened_date, closed_date, long_X, lat_Y))
        end
    end

    # check path
    mkpath(input_dir)
    filename = joinpath(input_dir, "msrstn_korea.csv")
    CSV.write(filename, df)

    df
end

function parse_aerosols(aes_dir::String, input_dir::String)
    re_aes_fn = r"([0-9]+)년\W*([0-9]+)(분기|월).csv"
    re_ext_fn = r".*.csv"
    date_str = "yyyymmddHH"

    aes_paths = joinpath(input_dir, aes_dir)
    aes_globs = rglob([re_aes_fn], aes_paths)

    # check there is a input file
    @assert isempty(aes_globs) == false

    df = DataFrame(stationCode = [], date = [],
                   SO2 = [], CO = [], O3 = [], NO2 = [],
                   PM10 = [], PM25 = [])

    p = Progress(length(collect(aes_globs)), dt=1.0, barglyphs=BarGlyphs("[=> ]"), barlen=40, color=:yellow)
    for _aes in aes_globs
        #=
        if match(re_ext_fn, _aes)[1] == "csv"
            df_raw = CSV.read(_aes)
        elseif match(re_ext_fn, _aes)[1] == "xlsx"
            df_raw = ExcelReaders.readxlsheet("Filename.xlsx", "Sheet1")
        else
            continue
        end
        =#
        df_raw = CSV.read(_aes, copycols=true)

        DataFrames.rename!(df_raw, [:지역 => :region, :측정소코드 => :stationCode, :측정소명 => :stationName, :측정일시 => :date,
                   :주소 => :addr])
        dropmissing!(df_raw, :date)

        # convert to ZonedDateTime to parse time correctly (i.e. 20181231T24:00 and 20190101T00:00)
        dates = [ZonedDateTime(DateTime(string(d), date_str), tz"Asia/Seoul") for d in df_raw[!, :date]]

        df_tmp = DataFrame(stationCode = df_raw[!, :stationCode],
            date = dates,
            SO2 = df_raw[!, :SO2],
            CO = df_raw[!, :CO],
            O3 = df_raw[!, :O3],
            NO2 = df_raw[!, :NO2],
            PM10 = df_raw[!, :PM10],
            PM25 = df_raw[!, :PM25])

        df = vcat(df, df_tmp)

        ProgressMeter.next!(p; showvalues = [(:filename, _aes)])
        flush(stdout); flush(stderr)
    end

    sort!(df, :date)

    df
end

function parse_weathers(wea_dir::String, input_dir::String, wea_stn_code::Integer)
    re_wea_fn = Regex("SURFACE_ASOS_$(string(wea_stn_code))_HR_([0-9]+)_([0-9]+)_([0-9]+).csv")
    date_str = "yyyy-mm-dd HH:MM"

    wea_paths = joinpath(input_dir, wea_dir)
    wea_globs = rglob([re_wea_fn], wea_paths)
    @assert isempty(wea_globs) == false

    df = DataFrame(date = [], temp = [],
        u = [], v = [], wind_spd = [], wind_dir = [], wind_sdir = [], wind_cdir = [],
        pres = [], humid = [], prep = [], snow = [])

    p = Progress(length(collect(wea_globs)), dt=1.0, barglyphs=BarGlyphs("[=> ]"), barlen=40, color=:yellow)
    for _wea in wea_globs

        df_raw = CSV.read(_wea, copycols=true, normalizenames=true)

        DataFrames.rename!(df_raw, [Symbol("일시") => :date,
            Symbol("기온(°C)") => :temp,
            Symbol("강수량(mm)") => :prep,
            Symbol("풍속(m/s)") => :wind_spd,
            Symbol("풍향(16방위)") => :wind_dir,
            Symbol("습도(%)") => :humid,
            Symbol("현지기압(hPa)") => :pres,
            Symbol("적설(cm)") => :snow])
        dropmissing!(df_raw, :date)

        dates = [ZonedDateTime(DateTime(string(d), date_str), tz"Asia/Seoul") for d in df_raw[!, :date]]

        # missings to zero except temp, humid, and pres (pressure)
        df_raw[!, :wind_spd] = coalesce.(df_raw[!, :wind_spd], 0.0)
        df_raw[!, :wind_dir] = coalesce.(df_raw[!, :wind_dir], 0.0)
        df_raw[!, :prep] = coalesce.(df_raw[!, :prep], 0.0)
        df_raw[!, :snow] = coalesce.(df_raw[!, :snow], 0.0)

        # http://colaweb.gmu.edu/dev/clim301/lectures/wind/wind-uv

        dict_wind_dir = Dict([
            ("0", 90),
            ("360", 90 ), ("20" , 67.5 ), ("50" , 45 ), ("70", 22.5),
            ("90" , 0  ), ("110", 337.5), ("140", 315), ("160", 292.5),
            ("180", 270), ("200", 247.5), ("230", 225), ("250", 202.5),
            ("270", 180), ("290", 157.5), ("320", 135), ("340", 112.5)])

        _wind_cdir = [Base.Math.cos(Base.Math.deg2rad(-dict_wind_dir[string(Int(w))] + 90)) for w in df_raw[!, :wind_dir]]
        _wind_sdir = [Base.Math.sin(Base.Math.deg2rad(-dict_wind_dir[string(Int(w))] + 90)) for w in df_raw[!, :wind_dir]]

        _u = [w[1] * Base.Math.cos(Base.Math.deg2rad(-dict_wind_dir[string(Int(w[2]))] + 90)) for w in zip(df_raw[!, :wind_spd], df_raw[!, :wind_dir])]
        _v = [w[1] * Base.Math.sin(Base.Math.deg2rad(-dict_wind_dir[string(Int(w[2]))] + 90)) for w in zip(df_raw[!, :wind_spd], df_raw[!, :wind_dir])]

        df_tmp = DataFrame(
            date = dates,
            temp = df_raw[!, :temp],
            u = _u,
            v = _v,
            wind_spd = df_raw[!, :wind_spd],
            wind_dir = df_raw[!, :wind_dir],
            wind_sdir = _wind_sdir,
            wind_cdir = _wind_cdir,
            pres = df_raw[!, :pres],
            humid = df_raw[!, :humid],
            prep = df_raw[!, :prep],
            snow = df_raw[!, :snow])

        df = vcat(df, df_tmp)

        ProgressMeter.next!(p; showvalues = [(:filename, _wea)])
        flush(stdout); flush(stderr)
    end

    sort!(df, :date)

    df
end

"""
    join_data(input_dir, obs_path, aes_dir, wea_dir, start_date, end_date)
join dataframe from 3 input
input_dir: input directory  (i.e. "/input")
obs_path : station xlsx path (full path)
aes_dir : subdirectory name of aerosol data (i.e. "aerosol")
wea_dir : subdirectory name of weather data (i.e. joinpath("weather", "seoul"))
"""
function join_data(input_dir::String, obs_path::String, aes_dir::String, wea_dir::String, start_date::ZonedDateTime, end_date::ZonedDateTime)
    seoul_stn_code = 108

    @info "Parsing Station dataset..."
    flush(stdout); flush(stderr)
    df_obs = parse_obsxlsx(obs_path, input_dir)
    @info "Parsing Weather dataset..."
    flush(stdout); flush(stderr)
    df_wea = parse_weathers(wea_dir, input_dir, seoul_stn_code)
    @info "Parsing Aerosol dataset..."
    flush(stdout); flush(stderr)
    df_aes = parse_aerosols(aes_dir, input_dir)
    # @info "Parsing Vertical Wind dataset..."
    # flush(stdout); flush(stderr)
    # df_vwn = parse_vwind(vwind_dir, input_dir

    # filter in date range
    df_obs = df_obs[(df_obs.oDate .<= start_date) .& (df_obs.cDate .>= end_date), :]
    df_aes = df_aes[(df_aes.date .>= start_date) .& (df_aes.date .<= end_date), :]
    df_wea = df_wea[(df_wea.date .>= start_date) .& (df_wea.date .<= end_date), :]
    # df_vwn = df_vwn[(df_wea.date .>= start_date) .& (df_wea.date .<= end_date), :]

    df1 = join(df_obs, df_aes, on = :stationCode)
    df2 = join(df1, df_wea, on = :date, kind = :inner)
    # df3 = join(df2, df_vwn, on = :date, kind = :inner)

    # sort by stationCode and date
    sort!(df2, (:stationCode, :date))

    # scaling except :PM10, :PM25
    features = [:SO2, :CO, :O3, :NO2, :PM10, :PM25,
        :temp, :u, :v, :wind_sdir, :wind_cdir, :wind_spd, :wind_dir, :pres, :humid, :prep, :snow]

    df = df2[!, [:stationCode, :date, :lat, :lon,
                   :SO2, :CO, :O3, :NO2, :PM10, :PM25,
                   :temp, :pres,
                   :u, :v, :wind_spd, :wind_dir, :wind_sdir, :wind_cdir,
                   :humid, :prep, :snow]]

    # check path
    mkpath(input_dir)
    filename = joinpath(input_dir, "input_2021.csv")
    CSV.write(filename, df)

    df
end


abstract type Data end

struct IncludeExcludePairs{V}
    mode::Symbol
    point_ids::Vector{V}
    include_pairs::Matrix{V}
end
function IncludeExcludePairs(V)
    IncludeExcludePairs(:undef, V[], Matrix{V}(undef,0,0))
end

struct NetworkData{T,V} <: Data
    coords::Tuple{Vector{V},Vector{V},Vector{T}}
    fp::Vector{V}
    source_map::Matrix{T}
    ground_map::Matrix{T}
end

struct RasterMeta
    ncols::Int
    nrows::Int
    xllcorner::Float64
    yllcorner::Float64
    cellsize::Float64
    nodata::Float64
    transform::Array{Float64, 1}
    wkt::String
end

function RasterMeta()
    RasterMeta(0,0,0,0,0,0,[0.0],"")
end

struct RasData{T,V} <: Data
    cellmap::Matrix{T}
    polymap::Matrix{V}
    source_map::Matrix{T}
    ground_map::Matrix{T}
    points_rc::Tuple{Vector{V},Vector{V},Vector{V}}
    strengths::Matrix{T}
    included_pairs::IncludeExcludePairs{V}
    hbmeta::RasterMeta
end

function load_graph(V, gpath::String, ::Type{T}) where {T}
    g = readdlm(gpath, T)
    i = zeros(V, size(g, 1))
    j = zeros(V, size(g, 1))
    v = zeros(T, size(g, 1))
    for iter = 1:size(g, 1)
        i[iter] = g[iter,1] + 1
        j[iter] = g[iter,2] + 1
        v[iter] = g[iter,3]
    end
    i,j,v
end

read_focal_points(V, path::String) = V.(vec(readdlm(path)) .+ 1)

function read_point_strengths(T, path::String, inc = true)
    a = readdlm(path, T)
    if inc
        @. a[:,1] = a[:,1] + 1
    end
    a
end

function read_cellmap(habitat_file::String, is_res::Bool, ::Type{T}) where {T}

    cell_map, rastermeta = _grid_reader(T, habitat_file)

    gmap = similar(cell_map)
    ind = findall(x -> x == -9999, cell_map)
    if is_res
        if count(x -> x == 0, cell_map) > 0
            throw("Error: zero resistance values are not currently supported for habitat maps. Use a short-circuit region file instead.")
        else
            for i in eachindex(cell_map)
                gmap[i] = 1 ./ cell_map[i]
            end
            gmap[ind] .= 0
        end
    else
        copyto!(gmap, cell_map)
        gmap[ind] .= 0
    end
    gmap, rastermeta
end

function _grid_reader(T, file)
    endswith(file, ".gz") ? file = "/vsigzip/$(file)" : ()

    c, wkt, transform = read_raster(file, T)

    rastermeta = get_raster_meta(c, wkt, transform)

    return c, rastermeta

end

function get_raster_meta(habitat_file, wkt, transform)
    dims = size(habitat_file)
    ncols = dims[2]
    nrows = dims[1]
    xllcorner = transform[1]
    yllcorner = transform[4]  - (nrows * transform[2])
    cellsize = transform[2]
    nodata = -9999 # set in read_raster (overwrites old nodata val)
    RasterMeta(ncols, nrows, xllcorner, yllcorner, cellsize, nodata, transform, wkt)
end

function _guess_file_type(filename, f)
    hdr = readline(f)
    seek(f, 0) #TODO I think this is not necessary? -VL

    f2 = endswith(filename, "gz") ? GZip.open(filename, "r") : open(filename, "r")
    bytes = read(f2, 4)[3:4]
    close(f2)

    if bytes == [0x2a, 0x00]
        filetype = FILE_TYPE_GEOTIFF
    elseif startswith(hdr, FILE_HDR_NPY)
        filetype = FILE_TYPE_NPY
    elseif startswith(lowercase(hdr), FILE_HDR_AAGRID)
        filetype = FILE_TYPE_AAGRID
    elseif startswith(hdr, FILE_HDR_INCL_PAIRS_AAGRID)
        filetype = FILE_TYPE_INCL_PAIRS_AAGRID
    elseif startswith(hdr, FILE_HDR_INCL_PAIRS)
        filetype = FILE_TYPE_INCL_PAIRS
    else
        filetype = FILE_TYPE_TXTLIST
    end
    return filetype
end

function read_polymap(T, file::String, habitatmeta;
                      nodata_as = 0)

    polymap, rastermeta = _grid_reader(Float64, file)

    # Convert polymap to T. It's easier to do here and check for errors than
    # to add another Boolean flag argument to read_raster that specifies if the
    # file is for nodes points or polygons.
    try
        polymap = convert(Array{T}, polymap)
    catch e
        isa(e, InexactError) && @error string("Your node file (point_file in ",
                                       "the .ini) contains non-integer values.",
                                       " See the docs on specifying nodes for",
                                       " more information.")
    end

    ind = findall(x -> x == rastermeta.nodata, polymap)
    if nodata_as != -1
        polymap[ind] .= nodata_as
    end

    if rastermeta.cellsize != habitatmeta.cellsize
        cswarn("cellsize is not the same")
    elseif rastermeta.ncols != habitatmeta.ncols
        cswarn("ncols is not the same")
    elseif rastermeta.nrows != habitatmeta.nrows
        cswarn("nrows is not the same")
    elseif rastermeta.yllcorner != habitatmeta.yllcorner
        cswarn("yllcorner is not the same")
    elseif rastermeta.xllcorner != habitatmeta.xllcorner
        cswarn("xllcorner is not the same")
    end

    polymap
end

function read_point_map(V, file, habitatmeta)

    # Advanced mode
    if file == "none"
        return (V[], V[], V[])
    end

    f = endswith(file, ".gz") ? GZip.open(file, "r") : open(file, "r")
    filetype = _guess_file_type(file, f)
    _points_rc = filetype == FILE_TYPE_TXTLIST ? readdlm(file) :
                        read_polymap(V, file, habitatmeta)

    i = V[]
    j = V[]
    v = V[]
    if filetype == FILE_TYPE_TXTLIST
        I = _points_rc[:,2]
        J = _points_rc[:,3]
        v = _points_rc[:,1]
        i  = ceil.(V, habitatmeta.nrows .- (J .- habitatmeta.yllcorner) ./ habitatmeta.cellsize)
        j = ceil.(V, (I .- habitatmeta.xllcorner) ./ habitatmeta.cellsize)
    else
        _I = findall(!iszero, _points_rc)
        (i,j,v) =  (getindex.(_I, 1), getindex.(_I, 2), _points_rc[_I])
    end

    ind = findall(x -> x < 0, v)

    # Get rid of negative resistances
    for index in ind
        deleteat!(i, index)
        deleteat!(j, index)
        deleteat!(v, index)
    end

    # Sort them
    idx = sortperm(v)
    i = i[idx]
    j = j[idx]
    v = v[idx]

    if (minimum(i) < 0) || (minimum(j) < 0) ||
            (maximum(i) > (habitatmeta.nrows)) ||
            (maximum(j) > (habitatmeta.ncols))
         throw("At least one focal node location falls outside of habitat map")
    end

    if size(unique(v),1) < 2
        throw("Less than two valid focal nodes found. Please check focal node location file.")
    end

    close(f)
    V.(i), V.(j), V.(v)
end

function read_source_and_ground_maps(T, V, source_file, ground_file, habitatmeta,
                                        is_res)

    ground_map = Matrix{T}(undef,0,0)
    source_map = Matrix{T}(undef,0,0)

    f = endswith(ground_file, "gz") ? GZip.open(ground_file, "r") : open(ground_file, "r")
    filetype = _guess_file_type(ground_file, f)

    if filetype == FILE_TYPE_AAGRID || filetype == FILE_TYPE_GEOTIFF
        ground_map = read_polymap(T, ground_file, habitatmeta; nodata_as = -1)
        ground_map = map(T, ground_map)
    else
        rc = readdlm(ground_file, V)
        ground_map = -9999 * ones(T, habitatmeta.nrows, habitatmeta.ncols)
        ground_map[rc[:,2], rc[:,3]] = rc[:,1]
    end
    close(f)

    f = endswith(source_file, "gz") ? GZip.open(source_file, "r") : open(source_file, "r")
    filetype = _guess_file_type(source_file, f)

    if filetype == FILE_TYPE_AAGRID || filetype == FILE_TYPE_GEOTIFF
        source_map = read_polymap(T, source_file, habitatmeta)
        source_map = map(T, source_map)
    else
        rc = readdlm(source_file, V)
        source_map = -9999 * ones(T, habitatmeta.nrows, habitatmeta.ncols)
        source_map[rc[:,2], rc[:,3]] = rc[:,1]
    end
    close(f)

    if is_res
        ind = findall(x -> x == -9999, ground_map)
        ground_map = 1 ./ ground_map
        ground_map[ind] .= 0
    else
        ind = findall(x -> x == -9999, ground_map)
        ground_map[ind] .= 0
    end

    source_map, ground_map
end

function read_included_pairs(V, filename)

    f = endswith(filename, "gz") ? GZip.open(filename, "r") : open(filename, "r")
    filetype = _guess_file_type(filename, f)
    close(f)
    minval = 0
    maxval = 0
    mode = :undef

    if filetype == FILE_TYPE_INCL_PAIRS_AAGRID
        open(filename, "r") do f
            minval = parse(Float64, split(readline(f))[2])
            maxval = parse(Float64, split(readline(f))[2])
        end
        included_pairs = readdlm(filename, skipstart=2)
        point_ids = V.(included_pairs[:,1])
        deleteat!(point_ids, 1)
        included_pairs = included_pairs[2:end, 2:end]
        map!(x -> x > maxval ? 0 : x, included_pairs, included_pairs)
        idx = findall(x -> x >= minval, included_pairs)
        mode = :include
        bin = map(x -> x >= minval ? V(1) : V(0), included_pairs)
        return IncludeExcludePairs(mode, point_ids, bin)
    elseif filetype == FILE_TYPE_INCL_PAIRS
        open(filename, "r") do f
            mode = Symbol(split(readline(f))[2])
        end
        included_pairs = readdlm(filename, skipstart = 1)
        if size(included_pairs, 1) == 1
            pl = zeros(V, 1,2)
            pl[1,:] = included_pairs
            included_pairs = pl
        end
        point_ids = V.(sort!(unique(included_pairs)))
        idx = findall(x -> x == 0 , point_ids)
        if length(idx) > 0
            deleteat!(point_ids, idx)
            cswarn("Code to include pairs is activated, some entries did not match with focal node file. Some focal nodes may have been dropped")
        end

        mat = zeros(V, size(point_ids, 1), size(point_ids, 1))

        for i = 1:size(included_pairs, 1)
            idx1 = findfirst(x -> x == included_pairs[i, 1], point_ids)
            idx2 = findfirst(x -> x == included_pairs[i, 2], point_ids)
            if idx1 != nothing && idx2 != nothing
                mat[idx1,idx2] = 1
                mat[idx2,idx1] = 1
            end
        end

        return IncludeExcludePairs(mode, point_ids, mat)
    end

end

function get_network_data(T, V, cfg)::NetworkData{T,V}

    hab_is_res = cfg["habitat_map_is_resistances"] in TRUELIST
    hab_file = cfg["habitat_file"]
    fp_file = cfg["point_file"]
    source_file = cfg["source_file"]
    ground_file = cfg["ground_file"]

    is_pairwise = cfg["scenario"] in PAIRWISE

    i,j,v = load_graph(V, hab_file, T)
    if hab_is_res
        v = 1 ./ v
    end

    if is_pairwise
        fp = read_focal_points(V, fp_file)
    else
        fp = V[]
    end

    if !is_pairwise
        source_map = read_point_strengths(T, source_file)
        ground_map = read_point_strengths(T, ground_file)
    else
        source_map = Matrix{T}(undef,0,0)
        ground_map = Matrix{T}(undef,0,0)
    end

    NetworkData((i,j,v), fp, source_map, ground_map)
end

function load_raster_data(T, V, cfg)::RasData{T,V}

    # Habitat file
    hab_file = cfg["habitat_file"]
    hab_is_res = cfg["habitat_map_is_resistances"] in TRUELIST

    # Polygons
    use_polygons = cfg["use_polygons"] in TRUELIST
    polygon_file = cfg["polygon_file"]

    # Mask file
    use_mask = cfg["use_mask"] in TRUELIST
    mask_file = cfg["mask_file"]

    # Point file
    point_file = cfg["point_file"]

    # Variable source strengths
    use_var_source = cfg["use_variable_source_strengths"] in TRUELIST
    var_source_file = cfg["variable_source_file"]

    # Included Pairs
    use_inc_pairs = cfg["use_included_pairs"] in TRUELIST
    inc_pairs_file = cfg["included_pairs_file"]

    # Advanced mode
    is_pairwise = cfg["scenario"] in PAIRWISE
    is_advanced = cfg["scenario"] in ADVANCED
    source_file = cfg["source_file"]
    ground_file = cfg["ground_file"]
    ground_is_res = cfg["ground_file_is_resistances"] in TRUELIST

    csinfo("Reading maps", cfg["suppress_messages"] in TRUELIST)

    # Read cell map
    cellmap, hbmeta = read_cellmap(hab_file, hab_is_res, T)
    c = count(x -> x > 0, cellmap)
    csinfo("Resistance/Conductance map has $c nodes", cfg["suppress_messages"] in TRUELIST)

    # Read polymap
    if use_polygons
        polymap = read_polymap(V, polygon_file, hbmeta)
    else
        polymap = Matrix{V}(undef,0,0)
    end

    # Read and update cellmap with mask file
    if use_mask
        update!(cellmap, mask_file, hbmeta)
        sum(cellmap) == 0 && throw("Mask file deleted everything!")
    end

    # Read point file
    if !is_advanced
        points_rc = read_point_map(V, point_file, hbmeta)
    else
        points_rc = (V[], V[], V[])
    end

    # Advanced mode reading
    if is_advanced
        source_map, ground_map =
        read_source_and_ground_maps(T, V, source_file, ground_file,
                                    hbmeta, ground_is_res)
    else
        source_map, ground_map = Matrix{T}(undef,0,0), Matrix{T}(undef,0,0)
    end

    # Included Pairs
    if use_inc_pairs
        included_pairs = read_included_pairs(V, inc_pairs_file)
    else
        included_pairs = IncludeExcludePairs(V)
    end

    # Variable source strengths
    if use_var_source
        strengths = read_point_strengths(T, var_source_file)
    else
        strengths = Matrix{T}(undef, 0,0)
    end

    RasData(cellmap, polymap, source_map, ground_map, points_rc, strengths,
                    included_pairs, hbmeta)
end

function update!(cellmap::Matrix{T}, m::String, hbmeta) where {T}
    mask = read_polymap(T, m, hbmeta)
    map!(x -> x > 0 ? 1 : 0, mask, mask)
    cellmap .= cellmap .* mask
end

# Inspired by GeoArrays.read()
function read_raster(path::String, T)
    # Check if file exists (ArchGDAL error is cryptic)
    check_path = endswith(path, ".gz") ? path[10:lastindex(path)] : path
    !isfile(check_path) && error("the file \"$(check_path)\" does not exist")

    raw = ArchGDAL.unsafe_read(path)
    transform = ArchGDAL.getgeotransform(raw)
    wkt = ArchGDAL.getproj(raw)

    # Extract 1st band (should only be one band anyway)
    # to get a 2D array instead of 3D
    band = ArchGDAL.getband(raw, 1)

    # Extract the array
    array_t = ArchGDAL.read(band)

    # This handles UInt tiff rasters that can still have negative NoData values
    # Need to convert the NoData value to Int64 in these cases
    if eltype(array_t) <: Integer
        ras_type = Int64
    else
        ras_type = eltype(array_t)
    end

    # Extract no data value, first converting it to the proper type (based on
    # the raster). Then, need to convert to T. Weird, yes,
    # but it's the only way I could get it to work for all raster types... -VL
    nodata_val = convert(T, convert(ras_type, ArchGDAL.getnodatavalue(band)))

    # Transpose the array -- ArchGDAL returns a x by y array, need y by x
    array = convert(Array{T}, permutedims(array_t, [2, 1]))

    array[array .== nodata_val] .= -9999.0

    # Line to handle NaNs in datasets read from tifs
    array[isnan.(array)] .= -9999.0

    # Close connection to dataset
    ArchGDAL.destroy(raw)

    array, wkt, transform # wkt and transform are needed later for write_raster
end


#
#  Use git:                    git status ....;   git add <filenames>;   git commit -m "..";   git push;   git rm  <filenames>
#  Use Jupyter notebooks:      using IJulia;   notebook()
#  Activation:                 ];   pkg> up;   pkg> activate
#  Working with JAC:           using Revise;   using JAC;   include("../src/jac.jl");   pkg> test
#
#  Copy to desktop             scp -r JAC.jl/ fritzsch@10.140.119.236:~/fri/.
"""
`module JAC`
    ... Jena Atomic Calculator (JAC) provides tools for performing atomic (structure) calculations at various degrees of complexity
        and sophistication. It has been designed to not only calculate atomic level structures and properties [such as g-factors or
        hyperfine and isotope-shift parameters] but also transition amplitudes between bound-state levels [for the anapole moment, dipole
        operator, electron electric-dipole moment, parity non-conservation, etc.] and, in particular, (atomic) transition probabilities,
        Auger rates, photoionization cross sections, radiative and dielectronic recombination rates as well as cross sections for many
        other (elementary) processes. JAC also facilitates interactive computations, the simulation of atomic cascades, the time-evolution
        of statistical tensors, a few semi-empirical estimates of atomic properties as well as the simplification of symbolic expressions
        from Racah's algebra. -- In addition, the JAC module supports the display of level energies, electron and photon spectra,
        radial orbitals and other atomic data.

**`Perform (atomic) computations of different complexity:`**
    JAC will eventually support **ten kinds** of computations which can be summarized as follows:

   + Atomic computations, based on explicitly specified electron configurations.
   + Restricted active-space computations (RAS).
   + Interactive computations.
   + Atomic cascade computations (partly implemented).
   + Atomic representations (Green and close-coupling functions, complex rotation; partly implemented).
   + Atomic responses (partly implemented).
   + Atomic descriptors for machine learning algorithms (not yet implemented).
   + Time-evolution of statistical tensors in (intense) light pusles (not yet implemented).
   + Semi-empirical estimates of cross sections, etc. (partly implemented).
   + Symbolic evaluation of expressions from Racah's algebra, etc.

**`Further details and information`**

        + Kinds of atomic implementation                                       [cf. ? Details.kindsOfComputation]
        + Atomic amplitudes (partly) implemented in JAC                        [cf. ? Details.amplitudes]
        + Atomic level properties (partly) implemented in JAC                  [cf. ? Details.properties]
        + Atomic processes (partly) implemented in JAC                         [cf. ? Details.processes]
        + Interactive use of JAC procedures                                    [cf. ? Details.interactive]
        + Design principles and limitations of the JAC program                 [cf. ? Details.design]
        + Data types, structs and name conventions of the JAC module           [cf. ? Details.datatypes]
        + Atomic cascade computations and approximations                       [cf. ? Details.decayCascades]
        + Use of (em) light pulses in the time evolution of statist. tensors   [cf. ? Details.pulses]
        + Why Julia ?                                                          [cf. ? Details.whyJulia]

"""
module JAC

using  Dates, Printf,  LinearAlgebra, IJulia, SpecialFunctions, FortranFiles, QuadGK, GSL, JLD2, SymEngine,
       HypergeometricFunctions  ## , Interact, GaussQuadrature

export AbstractConfigurationRestriction, AbstractEeInteraction, AbstractCImethod, AbstractPotential, AbstractQedModel, AbstractStartOrbitals,
       AbstractProcessSettings, AbstractPropertySettings,
       add, analyze, AlphaX, AlphaVariation, AnapoleMoment,
       AngularJ64, AngularM64, AngularJ, AngularMomentum,
       AsfSettings, Atomic, AtomicState, AtomicStructure, Auger, AugerInPlasma, AutoIonization,
       Basics, Basis, BreitInteraction, Bsplines,
       CartesianVector, CiSettings, CiExpansion, CloseCoupling, compute, convertUnits, Cascade, Compton, Configuration, ConfigurationR,
       Continuum, CsfR, Coulex, CoulombExcitation, Coulion, CoulombIonization, CoulombBreit, CoulombInteraction, ClebschGordan,
       CorePolarization,
       diagonalize, Defaults, DecayYield, Details, Dielectronic, Dierec, Djpq, DoubleAutoIonization, DoubleAuger,
       Eimex, ElectronCapture, ElecCapture, estimate, ElectricDipoleMoment, Einstein, EinsteinX, EmMultipole, evaluate, ExpStokes,
       E1, M1, E2, M2, E3, M3, E4, M4,
       FormFactor, FormF, FullCIeigen,
       generate, GreenSettings, GreenChannel, GreenExpansion, getDefaults, Green, Gui,
       Hfs, HighHarmonic, HFS, HydrogenicIon,
       interpolate, integrate, Integral, ImpactExcAuto, ImpactExcitation, ImpactExcitationAutoion, ImpactIonization,
       InternalConv, InternalConversion, Isotope, IsotopeShift,
       Kronecker,
       LandeZeeman, LandeJ,  LandeF, Level, LevelSymmetry, LSjj, LSjjSettings, LevelSelection, LineSelection,
       ManyElectron, MeanFieldSettings, MeanFieldBasis, minus, Model, modify, MultiPhotonDE, MultiPhotonDeExcitation, MultiPhotonDoubleIon,
       MultiPI, MultiPDI, MultiPhotonIonization, MultipoleMoment, MultipolePolarizibility, Multiplet,
       NoAmplitude, NoProcess, Nuclear, NoneQed, NoProperty,
       OneElectronSettings, OneElectronSpectrum, Orbital,
       PathwaySelection, perform, provide, PairA1P, PairAnnihilation1Photon, PairAnnihilation2Photon,
       PairProduction, ParityNonConservation, PeriodicTable, Parity, plus,
       Photo, PhotoExc, PhotoExcAuto, PhotoExcFluor, PhotoEmission, PhotoExcitation, PhotoExcitationAutoion, PhotoExcitationFluores,
       PhotoIonAuto, PhotoIonFluor, PhotoDouble, PhotoIonization, PhotoDoubleIonization, PhotoIonizationFluores, PhotoIonizationAutoion,
       PhotoRecombination, PlasmaShift, Plasma, Polarity, Pulse,
       QedPetersburg, QedSydney,
       RacahAlgebra, RacahExpression, Radial, RadialIntegrals, Radiative, RadiativeAuger, RAuger, RasSettings, RasStep,
       RasExpansion, RayleighCompton, recast, Rec, REDA, READI, Representation, ReducedDensityMatrix, RadiativeOpacity,
       RestrictNoElectronsTo, RestrictParity, RestrictToShellDoubles, RequestMinimumOccupation, RequestMaximumOccupation,
       SchiffMoment, Semiempirical, setDefaults, Shell, SolidAngle, Spectroscopy, SpinAngular, SphericalTensor,
       StartFromHydrogenic, StartFromPrevious, StrongField, Subshell,
       tabulate, TestFrames, Triangle, tools,
       UseCoulomb, UseBabushkin, UseGauge,
       WeightedCartesian, W3j, W6j, W9j,
       Yields, Ylm,
       Zeeman

# Basic data and data structures
include("module-Basics.jl");            using ..Basics
include("module-Radial.jl");            using ..Radial
include("module-Math.jl");              using ..Math
include("module-Defaults.jl");          using ..Defaults
include("module-ManyElectron.jl");      using ..ManyElectron
include("module-Nuclear.jl");           using ..Nuclear
include("module-BiOrthogonal.jl");      using ..BiOrthogonal

# Specialized functions/methods to manipulate these data
include("module-AngularMomentum.jl")
## include("module-AngularCoefficients-Ratip2013.jl")  ## keep for internal test purposes only
include("module-SpinAngular.jl");       using ..SpinAngular
include("module-Bsplines.jl")
include("module-Continuum.jl")
include("module-Details.jl")
include("module-RadialIntegrals.jl")
include("module-HydrogenicIon.jl")
include("module-InteractionStrength.jl")
include("module-InteractionStrengthQED.jl")
include("module-PeriodicTable.jl")
include("module-TableStrings.jl")
include("module-Tools.jl")
include("module-AtomicState.jl");       using ..AtomicState
include("module-LSjj.jl")

# Functions/methods for atomic amplitudes
include("module-MultipoleMoment.jl")
include("module-ParityNonConservation.jl")

include("module-PhotoEmission.jl")

# Functions/methods for atomic properties
include("module-Einstein.jl")
include("module-Hfs.jl")
include("module-IsotopeShift.jl")
include("module-LandeZeeman.jl")
include("module-FormFactor.jl")
include("module-ReducedDensityMatrix.jl")
include("module-PlasmaShift.jl")
include("module-AlphaVariation.jl")
include("module-DecayYield.jl")
include("module-RadiativeOpacity.jl")
##x include("module-CloseCoupling.jl")
include("module-MultipolePolarizibility.jl")

# Functions/methods for atomic processes
include("module-PhotoExcitation.jl")
include("module-PhotoIonization.jl")
include("module-PhotoRecombination.jl")
include("module-AutoIonization.jl")
include("module-ElectronCapture.jl")
include("module-Dielectronic.jl")
include("module-PhotoExcitationFluores.jl")
include("module-PhotoExcitationAutoion.jl")
include("module-RayleighCompton.jl")
include("module-MultiPhotonDeExcitation.jl")
include("module-DoubleAutoIonization.jl")
include("module-PhotoDoubleIonization.jl")
include("module-CoulombExcitation.jl")
include("module-PhotoIonizationFluores.jl")
include("module-PhotoIonizationAutoion.jl")
include("module-CoulombIonization.jl")
include("module-ImpactExcitation.jl")
include("module-ImpactExcitationAutoion.jl")
include("module-RadiativeAuger.jl")
include("module-MultiPhotonIonization.jl")
include("module-MultiPhotonDoubleIon.jl")
include("module-InternalConversion.jl")
#= Further processes, not yet included into the code
include("module-REDA.jl")
include("module-READI.jl")
include("module-PairProduction.jl")
include("module-PairAnnihilation1Photon.jl")
include("module-PairAnnihilation2Photon.jl")  =#

# Functions/methods for atomic responses and time evolutions
include("module-Pulse.jl")
# include("module-Statistical.jl")

# Functions/methods for the computation of atomic responses
## include("module-HighHarmonic.jl")
include("module-StrongField.jl")

# Functions/methods for semi-empirical estimations
# include("module-ImpactIonization.jl")
include("module-Semiempirical.jl")

# Functions/methods for atomic and cascade computations
include("module-Atomic.jl");            using ..Atomic
include("module-Cascade.jl");           using ..Cascade

# Functions/methods for symbolic computations
include("module-RacahAlgebra.jl");      using ..RacahAlgebra
include("module-SphericalTensor.jl");   using ..SphericalTensor

# Basic functions/methods to manipulate these data
include("module-BasicsAZ.jl")
include("module-ManyElectronAZ.jl")

# Specialized macros
##x include("macro-racahsum.jl")

# All test functions/methods stay with the JAC root module
include("module-TestFrames.jl");        using ..TestFrames

function __init__()
    # The following variables need to be initialized at runtime to enable precompilation
    global JAC_SUMMARY_IOSTREAM = stdout
    global JAC_TEST_IOSTREAM    = stdout
end

println("\nWelcome to JAC:  A community approach to the computation of atomic structures, cascades and time evolutions " *
        "[(C) Copyright by Stephan Fritzsche, Jena (2018-2021)].")

end


using Base.Test
using BioSymbols
using BioSequences
using GenomicFeatures
using SuffixArrays
using Gadfly
using Automa

using BufferedStreams
using Libz

using PyCall, PyPlot

using ScikitLearn
using ScikitLearn.Utils: meshgrid

@sk_import ensemble: IsolationForest

@pyimport matplotlib.font_manager as fm
@pyimport scipy.stats as stats

import Automa
import Automa.RegExp: @re_str
import Compat: take!

importall BioSymbols
importall GenomicFeatures

include("../src/pairs.jl")
include("../src/energy.jl")
include("../src/rnaduplex.jl")
include("../src/intervals.jl")
include("../src/traverse.jl")
include("../src/mafreader.jl")
include("../src/gtrmodel.jl")
include("../src/newick.jl")
include("../src/suffix.jl")
include("../src/evoduplex.jl")
include("../src/io.jl")
include("../src/regions.jl")
include("../src/train.jl")

const pair_set   = [AU_PAIR, UA_PAIR, CG_PAIR, GC_PAIR, GU_PAIR, UG_PAIR]
const mismat_set = [AA_MISMATCH, AG_MISMATCH, AC_MISMATCH, CA_MISMATCH, CC_MISMATCH,
                    CU_MISMATCH, GA_MISMATCH, GG_MISMATCH, UC_MISMATCH, UU_MISMATCH]
const bulge_set  = [AB_BULGE, CB_BULGE, GB_BULGE, UB_BULGE, BA_BULGE, BC_BULGE, BG_BULGE, BU_BULGE]

@testset "Nucleotide Pairs" begin

   @test convert(RNAPair, RNA_A, RNA_U) == AU_PAIR
   @test convert(RNAPair, RNA_U, RNA_A) == UA_PAIR
   @test convert(RNAPair, RNA_C, RNA_G) == CG_PAIR
   @test convert(RNAPair, RNA_G, RNA_C) == GC_PAIR
   @test convert(RNAPair, RNA_G, RNA_U) == GU_PAIR
   @test convert(RNAPair, RNA_U, RNA_G) == UG_PAIR

   @test convert(RNAMismatch, RNA_A, RNA_A) == AA_MISMATCH
   @test convert(RNAMismatch, RNA_A, RNA_G) == AG_MISMATCH
   @test convert(RNAMismatch, RNA_A, RNA_C) == AC_MISMATCH
   @test convert(RNAMismatch, RNA_C, RNA_A) == CA_MISMATCH
   @test convert(RNAMismatch, RNA_C, RNA_C) == CC_MISMATCH
   @test convert(RNAMismatch, RNA_C, RNA_U) == CU_MISMATCH
   @test convert(RNAMismatch, RNA_G, RNA_A) == GA_MISMATCH
   @test convert(RNAMismatch, RNA_G, RNA_G) == GG_MISMATCH
   @test convert(RNAMismatch, RNA_U, RNA_C) == UC_MISMATCH
   @test convert(RNAMismatch, RNA_U, RNA_U) == UU_MISMATCH

   @test convert(RNABulge, RNA_A, RNA_Gap) == AB_BULGE
   @test convert(RNABulge, RNA_C, RNA_Gap) == CB_BULGE
   @test convert(RNABulge, RNA_G, RNA_Gap) == GB_BULGE
   @test convert(RNABulge, RNA_U, RNA_Gap) == UB_BULGE
   @test convert(RNABulge, RNA_Gap, RNA_A) == BA_BULGE
   @test convert(RNABulge, RNA_Gap, RNA_C) == BC_BULGE
   @test convert(RNABulge, RNA_Gap, RNA_G) == BG_BULGE
   @test convert(RNABulge, RNA_Gap, RNA_U) == BU_BULGE

   unique = Dict{Int,Bool}()
   ind_sum    = 0
   for val in [pair_set; mismat_set]
      i = index(val)
      @test !haskey(unique, i)
      unique[i] = true
      ind_sum += i
   end
   @test ind_sum == sum(collect(1:16))

   @test split(AU_PAIR) == (1,4)
   @test split(CG_PAIR) == (2,3)
   @test split(GU_PAIR) == (3,4)

   @test AU_PAIR == flip(UA_PAIR)
   @test UA_PAIR == flip(AU_PAIR)
   @test CG_PAIR == flip(GC_PAIR)
   @test GC_PAIR == flip(CG_PAIR)
   @test UG_PAIR == flip(GU_PAIR)
   @test GU_PAIR == flip(UG_PAIR)

   @test is_purine_pyrimidine( AU_PAIR ) == true
   @test is_purine_pyrimidine( GC_PAIR ) == true
   @test is_purine_pyrimidine( GU_PAIR ) == true
   @test is_purine_pyrimidine( UA_PAIR ) == false
   @test is_purine_pyrimidine( CG_PAIR ) == false
   @test is_purine_pyrimidine( UG_PAIR ) == false

   @test AA_MISMATCH == flip(AA_MISMATCH)
   @test AG_MISMATCH == flip(GA_MISMATCH)
   @test AC_MISMATCH == flip(CA_MISMATCH)
   @test CC_MISMATCH == flip(CC_MISMATCH)
   @test CU_MISMATCH == flip(UC_MISMATCH)
   @test GG_MISMATCH == flip(GG_MISMATCH)
   @test UU_MISMATCH == flip(UU_MISMATCH)

   @test AB_BULGE == flip(BA_BULGE)
   @test CB_BULGE == flip(BC_BULGE)
   @test GB_BULGE == flip(BG_BULGE)
   @test UB_BULGE == flip(BU_BULGE)
   @test BA_BULGE == flip(AB_BULGE)
   @test BC_BULGE == flip(CB_BULGE)
   @test BG_BULGE == flip(GB_BULGE)
   @test BU_BULGE == flip(UB_BULGE)

   @test isfiveprime( AB_BULGE ) == true
   @test isfiveprime( CB_BULGE ) == true
   @test isfiveprime( GB_BULGE ) == true
   @test isfiveprime( UB_BULGE ) == true
   @test isfiveprime( BA_BULGE ) == false
   @test isfiveprime( BC_BULGE ) == false
   @test isfiveprime( BG_BULGE ) == false
   @test isfiveprime( BU_BULGE ) == false

   for b in bulge_set
      @test !ispair(b)
      @test isbulge(b)
      @test !ismismatch(b)
   end
   @test nbulges(bulge_set) == length(bulge_set)
   @test nmismatches(bulge_set) == 0
   @test npairs(bulge_set) == 0

   for m in mismat_set
      @test !ispair(m)
      @test !isbulge(m)
      @test ismismatch(m)
   end
   @test nmismatches(mismat_set) == length(mismat_set)
   @test nbulges(mismat_set) == 0
   @test npairs(mismat_set) == 0

   for p in pair_set
      @test ispair(p)
      @test !isbulge(p)
      @test !ismismatch(p)
   end
   @test npairs(pair_set) == length(pair_set)
   @test nbulges(pair_set) == 0
   @test nmismatches(pair_set) == 0

   @test split(AB_BULGE) == 1
   @test split(CB_BULGE) == 2
   @test split(GB_BULGE) == 3
   @test split(UB_BULGE) == 4
   @test split(BA_BULGE) == 1
   @test split(BC_BULGE) == 2
   @test split(BG_BULGE) == 3
   @test split(BU_BULGE) == 4

   mypath = [AU_PAIR, CG_PAIR, AB_BULGE]
   @test five_three(mypath, 1:3) == (1,4)
   @test five_three(mypath, 1:3, last=true) == (1,3)
   mypath = [BC_BULGE, CG_PAIR, BA_BULGE]
   @test five_three(mypath, 1:3) == (2,2)
   @test five_three(mypath, 1:3, last=true) == (2,1)

end

@testset "Energy Parameters" begin
   for i in pair_set, j in pair_set
      if i == GU_PAIR && j == UG_PAIR || i == UG_PAIR && j == GU_PAIR
         @test TURNER_1998_STACK[index(i), index(j)] > 0.0
      else
         @test TURNER_1998_STACK[index(i), index(j)] < 0.0
      end

      @test length(TURNER_2004_INTERNAL_TWO[ index(i), index(j) ]) == 16
      @test sum(TURNER_2004_INTERNAL_TWO[ index(i), index(j) ]) > 0.0

      for k in 1:4
         @test length(TURNER_2004_INTERNAL_THREE[ index(i), index(j), k ]) == 16
         @test sum(TURNER_2004_INTERNAL_THREE[ index(i), index(j), k ]) > 0.0
      end

      @test length(TURNER_2004_INTERNAL_FOUR[ index(i), index(j) ]) == 256
      @test sum(TURNER_2004_INTERNAL_FOUR[ index(i), index(j) ]) > 0.0
   end
end

@testset "Duplex Energy Functions" begin

   duplex = RNADuplex()

   # Test simple 1x1 lookup
   duplex.path = [AU_PAIR, AA_MISMATCH, AU_PAIR]
   @test internal_1x1_energy( duplex, 1:3 ) == 1.9
   duplex.path = [UG_PAIR, UU_MISMATCH, UG_PAIR]
   @test internal_1x1_energy( duplex, 1:3 ) == 1.6
   @test motif_energy( duplex, 3 )          == 1.6

   # Test lookup with 1x2 table.
   duplex.path = [AU_PAIR, AA_MISMATCH, BA_BULGE, AU_PAIR]
   @test internal_1x2_energy( duplex, 1:4 ) == 3.7
   # Reverse bulge/mismatch order
   duplex.path = [AU_PAIR, BA_BULGE, AA_MISMATCH, AU_PAIR]
   @test internal_1x2_energy( duplex, 1:4 ) == 3.7
   # Invert bulge side
   duplex.path = [UA_PAIR, AB_BULGE, AA_MISMATCH, UA_PAIR]
   @test internal_1x2_energy( duplex, 1:4 ) == 3.7
   # Invert and reverse
   duplex.path = [UA_PAIR, AA_MISMATCH, AB_BULGE, UA_PAIR]
   @test internal_1x2_energy( duplex, 1:4 ) == 3.7
   # Test range
   duplex.path = [UG_PAIR, UU_MISMATCH, BU_BULGE, UG_PAIR]
   @test internal_1x2_energy( duplex, 1:4 ) == 3.0
   @test motif_energy( duplex, 4 )          == 3.0

   # Test lookup with 2x2 table.
   duplex.path = [AU_PAIR, AA_MISMATCH, AA_MISMATCH, AU_PAIR]
   @test internal_2x2_energy( duplex, 1:4 ) == 2.8
   duplex.path = [UG_PAIR, UU_MISMATCH, UU_MISMATCH, UG_PAIR]
   @test internal_2x2_energy( duplex, 1:4 ) == 3.1
   @test motif_energy( duplex, 4 )          == 3.1

   # Test helix symmetry
   duplex.path = [AU_PAIR, UA_PAIR, CG_PAIR, GC_PAIR] # no_symmetry
   @test helix_symmetry( duplex ) == 0.0
   duplex.path = [AU_PAIR, CG_PAIR, GC_PAIR, UA_PAIR] # even_symmetry
   @test helix_symmetry( duplex ) == TURNER_1998_HELICAL_SYMMETRY
   duplex.path = [AU_PAIR, CG_PAIR, GU_PAIR, GC_PAIR, UA_PAIR] # odd_symmetry
   @test helix_symmetry( duplex ) == TURNER_1998_HELICAL_SYMMETRY

end

@testset "Duplex Calculations" begin

   # Duplex unit-test examples from http://rna.urmc.rochester.edu/NNDB

   #= Non-self complimentary duplex

     5'GCACG 3'
     3'CGUGC 5'  =#

    duplex = RNADuplex()
    push!( duplex, [GC_PAIR, CG_PAIR, AU_PAIR, CG_PAIR, GC_PAIR] )
    @test length( duplex.path ) == 5
    @test length( duplex.energy ) == length( duplex.path ) + 1
    @test energy( duplex ) == -6.01

   #= Self complimentary duplex

     5'AGCGCU 3'
     3'UCGCGA 5'  =#

    duplex = RNADuplex()
    push!( duplex, [AU_PAIR, GC_PAIR, CG_PAIR, GC_PAIR, CG_PAIR, UA_PAIR] )
    @test length( duplex.path ) == 6
    @test length( duplex.energy ) == length( duplex.path ) + 1
    @test energy( duplex ) == -7.98

   #= 2x2 internal loop

         GA
     5'CA  CG
     3'GU  GC
         AG  =#

    duplex = RNADuplex()
    push!( duplex, [CG_PAIR,AU_PAIR,GA_MISMATCH,AG_MISMATCH,CG_PAIR,GC_PAIR] )
    @test length( duplex.path ) == 6
    @test length( duplex.energy ) == length( duplex.path ) + 1
    @test energy( duplex ) == -1.51

   #= 1x5 internal loop

          G
     5'CA     CG
     3'GU     GC
         GAAAG   =#

     duplex = RNADuplex()
     push!( duplex, [CG_PAIR, AU_PAIR, GG_MISMATCH, BA_BULGE, BA_BULGE, BA_BULGE,
                     BG_BULGE, CG_PAIR, GC_PAIR] )
     @test length( duplex.path ) == 9
     @test length( duplex.energy ) == length( duplex.path ) + 1
     @test energy( duplex ) == 4.69

   #= 2x3 internal loop with stabilizing mismatches

         GA
     5'CA   CG
     3'GU   GC
         GAG  =#

     duplex = RNADuplex()
     push!( duplex, [CG_PAIR, AU_PAIR, GG_MISMATCH, AA_MISMATCH, BG_BULGE, CG_PAIR,
                     GC_PAIR] )
     @test length( duplex.path ) == 7
     @test length( duplex.energy ) == length( duplex.path ) + 1
     @test energy( duplex ) == 0.89

   #= Single exception bulge with multiple states

          C
     5'GCC G
     3'CGG C  =#

     duplex = RNADuplex()
     push!( duplex, [GC_PAIR, CG_PAIR, CG_PAIR, CB_BULGE, GC_PAIR] )
     @test length( duplex.path ) == 5
     @test length( duplex.energy ) == length( duplex.path ) + 1
     @test energy( duplex ) == -2.7867

   #= Multiple nucleotide bulge

         ACA
     5'GA   G
     3'CU   C  =#

     duplex = RNADuplex()
     push!( duplex, [GC_PAIR, AU_PAIR, AB_BULGE, CB_BULGE, AB_BULGE, GC_PAIR] )
     @test length( duplex.path ) == 6
     @test length( duplex.energy ) == length( duplex.path ) + 1
     @test energy( duplex ) == 5.34

end

@testset "Duplex Interval Collection" begin

   # Test pushing, overwriting, and ordering
   dup = RNADuplex()
   push!(dup, [CG_PAIR,AU_PAIR,AB_BULGE,GC_PAIR])
   c = DuplexCollection{String}()
   i = DuplexInterval(Interval("c",1,6,'+',"genea"),
                      Interval("c",50,55,'+',"genea"), dup)
   push!( c, i )
   @test length(c.names["c"]) == 1
   @test length(c.names["c"][one(UInt64)]) == 1

   dup2 = deepcopy( dup )
   push!( dup2, CG_PAIR )
   @test energy(dup2) < energy(dup)
   i2 = DuplexInterval(Interval("c",2,7,'+',"genea"),
                       Interval("c",20,25,'+',"genea"), dup2)
   push!( c, i2 )
   @test length(c.names["c"]) == 1
   @test length(c.names["c"][one(UInt64)]) == 2

   i3 = DuplexInterval(Interval("c",1,7,'+',"genea"),
                       Interval("c",49,55,'+',"genea"), dup2)
   push!( c, i3 )
   @test length(c.names["c"]) == 1
   @test length(c.names["c"][one(UInt64)]) == 2

   @test isordered(c)

   # Test addition-of-larger overwriting duplex
   c = DuplexCollection{String}()
   dup = RNADuplex()
   push!(dup, [CG_PAIR,GC_PAIR,CG_PAIR])
   i = DuplexInterval(Interval("c",1,5,'+',"genea"),
                      Interval("c",100,105,'+',"genea"), dup)
   dup2 = deepcopy(dup)
   push!(dup2, [GC_PAIR,CG_PAIR])
   @test energy(dup2) < energy(dup)
   i2 = DuplexInterval(Interval("c",6,10,'+',"genea"),
                       Interval("c",106,110,'+',"genea"), dup2)
   push!( c, i  )
   push!( c, i2 )
   @test length(c.names["c"]) == 1
   @test length(c.names["c"][one(UInt64)]) == 2

   worse = RNADuplex()
   push!(worse, [GU_PAIR,AB_BULGE,GB_BULGE,UA_PAIR,UG_PAIR,CB_BULGE,UA_PAIR])
   @test energy(worse) > energy(dup)
   i3 = DuplexInterval(Interval("c",1,10,'+',"genea"),
                       Interval("c",100,110,'+',"genea"), worse)

   push!( c, i3 ) # less favorable energy should not get pushed.
   @test length(c.names["c"]) == 1
   @test length(c.names["c"][one(UInt64)]) == 2
   firstint = c.names["c"][one(UInt64)][1]
   @test firstint.first.first == 1 && firstint.first.last == 5

   better = RNADuplex()
   push!(better, [CG_PAIR,GC_PAIR,CG_PAIR,CG_PAIR,GC_PAIR,CG_PAIR])
   @test energy(better) < energy(dup2)
   i3 = DuplexInterval(Interval("c",1,10,'+',"genea"),
                       Interval("c",100,110,'+',"genea"), better)
   push!( c, i3 ) # better energy should overwrite overlapping
   @test length(c.names["c"]) == 1
   firstint = c.names["c"][one(UInt64)][1]
   @test firstint.first.first == 1 && firstint.first.last == 10
   @test length(c.names["c"][one(UInt64)]) == 1

   # Test addition of larger overwriting duplex, and subsequent deletion because of smaller higher energy duplex
   c = DuplexCollection{String}()
   push!( c, i  )
   push!( c, i2 )
   middle = RNADuplex()
   push!(middle, [CG_PAIR,GC_PAIR,CG_PAIR, GC_PAIR])
   @test energy(dup2) < energy(middle) < energy(dup)
   i3 = DuplexInterval(Interval("c",1,10,'+',"genea"),
                       Interval("c",100,110,'+',"genea"), middle)
   push!( c, i3 )
   @test length(c.names["c"]) == 1
   firstint = c.names["c"][one(UInt64)][1]
   @test firstint.first.first == 6 && firstint.first.last == 10
   @test length(c.names["c"][one(UInt64)]) == 1

end

@testset "Duplex Intervals Stitching" begin

   # test duplex strings function

   c = DuplexCollection{String}()
   dup = RNADuplex()
   push!(dup, [CG_PAIR,GC_PAIR,CG_PAIR,CG_PAIR,GC_PAIR,GC_PAIR])
   i = DuplexInterval(Interval("c",  1,  6,'+',"genea"),
                      Interval("c",105,110,'+',"genea"), dup)
   dup2 = RNADuplex()
   push!(dup2, [GC_PAIR,GC_PAIR,UG_PAIR,AU_PAIR,CG_PAIR,GC_PAIR])
   i2 = DuplexInterval(Interval("c",  5, 10,'+',"genea"),
                       Interval("c",101,106,'+',"genea"), dup2)

   @test !isnull( stitch(i, i2, 3, 3) )
   @test !isnull( stitch(i2, i, 3, 3) )

   # test stitching with bulges
   c = DuplexCollection{String}()
   dup = RNADuplex()
   push!(dup, [CG_PAIR,GC_PAIR,CG_PAIR,CG_PAIR,GC_PAIR,AB_BULGE,GC_PAIR])
   i = DuplexInterval(Interval("c",  1,  7,'+',"genea"),
                      Interval("c",105,110,'+',"genea"), dup)
   dup2 = RNADuplex()
   push!(dup2, [GC_PAIR,AB_BULGE,GC_PAIR,UG_PAIR,AU_PAIR,CG_PAIR,GC_PAIR])
   i2 = DuplexInterval(Interval("c",  5, 11,'+',"genea"),
                       Interval("c",101,106,'+',"genea"), dup2)

   @test !isnull( stitch(i, i2, 3, 3) )
   @test !isnull( stitch(i2, i, 3, 3) )

   # test negative stitching cases

end

@testset "Load Bed & BedGraph" begin
   g = loadbedgraph( "head.bedGraph.gz" )
   @test length(g) == 999
   i = loadbed( "small.bed" )
   g = loadbedgraph( "head.bedGraph.gz", regions=i, regionbool=true )
   @test length(g) == 10
end

@testset "MAF Parser" begin
   mafheader = """
##maf version=1 scoring=tba.v8
 # tba.v8 (((human chimp) baboon) (mouse rat))
 # multiz.v7
 # maf_project.v5 _tba_right.maf3 mouse _tba_C
 # single_cov2.v4 single_cov2 /dev/stdin
"""
   maflines = """
a score=23262.0
s hg16.chr7    27578828 38 + 158545518 AAA-GGGAATGTTAACCAAATGA---ATTGTCTCTTACGGTG
s panTro1.chr6 28741140 38 + 161576975 AAA-GGGAATGTTAACCAAATGA---ATTGTCTCTTACGGTG
s baboon.chr1    116834 38 +   4622798 AAA-GGGAATGTTAACCAAATGA---GTTGTCTCTTATGGTG
s mm4.chr6     53215344 38 + 151104725 -AATGGGAATGTTAAGCAAACGA---ATTGTCTCTCAGTGTG
s rn3.chr4     81344243 40 + 187371129 -AA-GGGGATGCTAAGCCAATGAGTTGTTGTCTCTCAATGTG

a score=5062.0
s hg16.chr7    27699739 6 + 158545518 TAAAGA
s panTro1.chr6 28862317 6 + 161576975 TAAAGA
s baboon.chr1    241163 6 +   4622798 TAAAGA
s mm4.chr6     53303881 6 + 151104725 TAAAGA
s rn3.chr4     81444246 6 + 187371129 taagga

a score=6636.0
s hg16.chr7    27707221 13 + 158545518 gcagctgaaaaca
s panTro1.chr6 28869787 13 + 161576975 gcagctgaaaaca
s baboon.chr1    249182 13 +   4622798 gcagctgaaaaca
s mm4.chr6     53310102 13 + 151104725 ACAGCTGAAAATA
"""

   maffile = mafheader * "\n" * maflines

   # Single buffer:

   headreader = MAFReader(BufferedInputStream(IOBuffer(maffile)))
   noheadreader = MAFReader(BufferedInputStream(IOBuffer(maflines)))

   discard_header!(headreader)
   @test headreader.stream.buffer[1:10] == noheadreader.stream.buffer[1:10]

   headreader = MAFReader(BufferedInputStream(IOBuffer(maffile)))

   while !done(headreader)
      headrec   = MAFRecord()
      noheadrec = MAFRecord()
      read!(headreader, headrec)
      read!(noheadreader, noheadrec)
      @test length(noheadrec) == length(headrec)
      @test length(noheadrec) > 1
      for i in 1:length(headrec)
         @test headrec[i] == noheadrec[i]
      end
   end
   @test done(headreader)
   @test done(noheadreader)

   # Multi buffer

   longreader = MAFReader(BufferedInputStream(IOBuffer(maffile)))
   shortreader = MAFReader(BufferedInputStream(IOBuffer(maffile), 512))

   while !done(longreader)
      longrec  = MAFRecord()
      shortrec = MAFRecord()
      read!(longreader, longrec)
      read!(shortreader, shortrec)
      @test length(shortrec) == length(longrec)
      @test length(shortrec) > 1
      for i in 1:length(shortrec)
         @test shortrec[i] == longrec[i]
      end
   end
   @test done(shortreader)
   @test done(longreader)

   # Bad Records
   maffile = """
##maf version=1 scoring=roast.v3.3
a score=781.000000
s hg19.chr22                16092278 1 +  51304566 C
s panTro4.chr22             14470458 1 +  49737984 C
s nomLeu3.chrUn_GL397501_2   1216368 1 -   1485088 C
s chlSab1.chrUn_KE147573       66869 1 -     85383 C
s monDom5.chr4              96780965 1 - 435153693 T
s danRer7.chr3                323838 1 -  63268876 C
s petMar2.GL501154             43910 1 -     71231 t

a score=-50707.000000
s hg19.chr22                     16092279 32 +  51304566 AGTCCCCAGGTGGT------CATGACACCTCAATTGGA
s panTro4.chr22                  14470459 32 +  49737984 AGTCCCCAGGTGGT------CATGACACCTCAATTGGA
s nomLeu3.chrUn_GL397501_2        1216369 32 -   1485088 AGTCCCCAGGTGGT------CATGACACTTCAACTGGA
s chlSab1.chrUn_KE147573            66870 32 -     85383 AGTCCCCAGGTGAT------CATGACACCTTAACTGGA
s monDom5.chr4                   96780966 24 - 435153693 TGACTCCAGCCAGA------GACGGAGCCC--------
s falChe1.KB397375                  11884 29 +     36355 GACCCTTGTGAAAA------CATGAGGGCTGTGAA---
s falPer1.KB390849                  40484 13 +     49439 AGCCCTTGTCTGA-------------------------
s ficAlb2.chrUn_KE165385           131290 31 -    141484 -cctccccgagtat------cCTGCGACCCCAGATCGA
s geoFor1.JH741012                   5677 32 -     10893 GGGCCCTTTGGGAA------GAGGACACTGCGACTGGA
s taeGut2.chr26_EQ833276_random      8281 32 -     85973 AGCCCCTCAGCTGT------CCCCACACCTCTCCTGCA
s pseHum1.KB221494                  45158 24 +     77128 gaccccacaatgac-------------cctcggttga-
s galGal4.chrUn_JH375878              275 32 -      4229 AGCCCAGTggggga------ccccggagccaaagggga
s pelSin1.JH208549                  94224 38 +    588315 GGCCCCAGGGCCAAATCCGTCCCTGGGTCCATATTCAA
s danRer7.chr3                     323839 32 -  63268876 ATTATCCAACTAGA------CTTGGCTGTTTAGAAGAC
s petMar2.GL501154                  43911 30 -     71231 --agttcacgtgag------ctctacaactcaggtgaa
"""

   noshort = MAFReader(BufferedInputStream(IOBuffer(maffile)))
   noshort.minlen = 5
   noshortrec = MAFRecord()
   read!(noshort, noshortrec)
   @test done(noshort)
   @test length(noshortrec) == 15

   noweak = MAFReader(BufferedInputStream(IOBuffer(maffile)))
   noweak.minscore = 0.0
   noweakrec = MAFRecord()
   read!(noweak, noweakrec)
   @test !done(noweak)
   @test length(noweakrec) == 7
   res = read!(noweak, MAFRecord())
   @test done(noweak)
   @test length(res) == 0

   #Trailing newline
   withnew = MAFReader(BufferedInputStream(IOBuffer(maffile * "\n")))
   withnew.minlen = 5
   withnewrec = MAFRecord()
   read!(withnew, withnewrec)
   @test done(withnew)
   @test length(withnewrec) == 15

   #Test MAFRecord gap deletion
   for i in 1:length(withnewrec)
      @test length(withnewrec[i].sequence) == length("AGTCCCCAGGTGGT------CATGACACCTCAATTGGA")
   end
   deletegaps!(withnewrec)
   for i in 1:length(withnewrec)
      @test length(withnewrec[i].sequence) == length("AGTCCCCAGGTGGTCATGACACCTCAATTGGA")
   end
end

@testset "Parsing MAF File & Stitching" begin
   # Stitching adjacent MAF records
   maflines = """
   a score=23262.0
   s hg16.chr7    27571001 38 + 158545518 AAA-GGGAATGTTAACCAAATGA---ATTGTCTCTTACGGTG
   s panTro1.chr6 28741140 38 + 161576975 AAA-GGGAATGTTAACCAAATGA---ATTGTCTCTTACGGTG
   s baboon.chr1    116834 38 +   4622798 AAA-GGGAATGTTAACCAAATGA---GTTGTCTCTTATGGTG
   s rn3.chr4     81344243 40 + 187371129 -AA-GGGGATGCTAAGCCAATGAGTTGTTGTCTCTCAATGTG

   a score=5062.0
   s hg16.chr7    27571039 6 + 158545518 TAAAGA
   s panTro1.chr6 28862317 6 + 161576975 TAAAGA
   s baboon.chr1    241163 6 +   4622798 TAAAGA
   s rn3.chr4     81444246 6 + 187371129 taagga

   a score=6636.0
   s hg16.chr7    27571045 13 + 158545518 gcagctgaaaaca
   s panTro1.chr6 28869787 13 + 161576975 gcagctgaaaaca
   s baboon.chr1    249182 13 +   4622798 gcagctgaaaaca
   s mm4.chr6     53310102 13 + 151104725 ACAGCTGAAAATA
   """

   tree = parsenewick("(((((hg16:0.006591,panTro1:0.006639):0.002184,baboon:0.009411):0.009942,
                            mm4:0.018342):0.014256,rn3:0.036199):0.021496,unused:0.04);")

   reader = MAFReader(BufferedInputStream(IOBuffer(maflines)))

   col = readmaf!( reader, tree.index )
   for i in col
      for j in i.metadata.species
         @test length(j.sequence) == length(i.metadata.species[1].sequence)
      end
   end

   println(col)

   #=MAFRecord(MAFSpecies[
   MAFSpecies("hg16","chr7",27571001,true,   AAAGGGAATGTTAACCAAATGAATTGTCTCTTACGGTGTAAAGAGCAGCTGAAAACA),
   MAFSpecies("panTro1","chr6",28741140,true,AAAGGGAATGTTAACCAAATGAATTGTCTCTTACGGTGTAAAGAGCAGCTGAAAACA),
   MAFSpecies("baboon","chr1",116834,true,   AAAGGGAATGTTAACCAAATGAGTTGTCTCTTATGGTGTAAAGAGCAGCTGAAAACA),
   MAFSpecies("mm4","chr6",53215344,true,    -AAGGGAATGTTAAGCAAACGAATTGTCTCTCAGTGTGTAAAGAACAGCTGAAAATA),
   MAFSpecies("rn3","chr4",81344243,true,    -AAGGGGATGCTAAGCCAATGAGTTGTCTCTCAATGTGTAAGGA-------------)
   =#
end

@testset "Newick tree building and Felsenstein's algorithm" begin
   tree = parsenewick("(((((hg19:0.006591,panTro2:0.006639):0.002184,gorGor1:0.009411):0.009942,
                            ponAbe2:0.018342):0.014256,rheMac2:0.036199):0.021496,papHam1:0.04);")
   names = collect(tree.root)
   expnames = ["hg19", "panTro2", "gorGor1", "ponAbe2", "rheMac2", "papHam1"]
   @test length(names) == length(expnames) == length(tree.order)
   for i in 1:length(names)
      @test names[i] == expnames[i] == tree.order[i]
   end
   # Test rate matrix -> probability
   Q = [-3.0 1.0 1.0 1.0
        1.0 -3.0 1.0 1.0
        1.0 1.0 -3.0 1.0
        1.0 1.0 1.0 -3.0]
   set_prob_mat!( tree, Q )
   hg19_node = tree.root.left.value.left.value.left.value.left.value.left.value
   @test hg19_node.label == "hg19"
   @test hg19_node.prob == expm(Q*0.006591)

   # Test likelihood function
   tree   = parsenewick("((hg19:0.006591,panTro2:0.006639):0.002184,gorGor1:0.5);")
   set_prob_mat!( tree, GTR_SINGLE_Q )

   #=
   hg19 length=0.006591
   A->G 0.0125902
   C->G 0.00378768
   G->G 0.985462
   T->G 0.00314851
   =#

   hg19 = expm(GTR_SINGLE_Q*0.006591) * [0,0,1,0]
   @test length(hg19) == 4

   #=
   panTro length=0.006639
   A->G 0.0126803
   C->G 0.00381511
   G->G 0.985357
   T->G 0.00317139
   =#

   panTro = expm(GTR_SINGLE_Q*0.006639) * [0,0,1,0]
   @test length(panTro) == 4

   #=
   hgpanAnc length=0.002184
   A->A 0.000158486
   A->C 2.07831e-7
   A->G 4.64153e-7
   A->T 1.8804e-7

   C->A 2.28759e-8
   C->C 1.43597e-5
   C->G 1.51679e-8
   C->T 5.20155e-8

   G->A 0.00409759
   G->C 0.00122328
   G->G 0.966317
   G->T 0.0010147

   T->A 1.47604e-8
   T->C 3.7086e-8
   T->G 8.97429e-9
   T->T 9.92702e-6

   *->A 0.0042561136363
   *->C 0.001237884617
   *->G 0.9663174882951899
   *->T 0.0010248670755000002
   =#

   hgpanAnc = expm(0.002184*GTR_SINGLE_Q) * (hg19 .* panTro)
   @test length(hgpanAnc) == 4

   @test round(0.0042561136363, 6) == round(hgpanAnc[1], 6)
   @test round(0.001237884617, 6) == round(hgpanAnc[2], 6)
   @test round(0.9663174882951899, 6) == round(hgpanAnc[3], 6)
   @test round(0.0010248670755000002, 6) == round(hgpanAnc[4], 6)

   @test round(likelihood( tree, DNA[DNA_G, DNA_G, DNA_A] ),4) == 0.0539

   smtree   = parsenewick("((hg19:0.006591,panTro2:0.006639):0.002184,gorGor1:0.15);")
   pairtree = deepcopy(smtree)
   set_prob_mat!( smtree,   GTR_SINGLE_Q )
   set_prob_mat!( pairtree, GTR_PAIRED_Q )

   single_p = likelihood( smtree, DNA[DNA_G, DNA_G, DNA_G] ) * likelihood( smtree, DNA[DNA_C, DNA_C, DNA_G] )
   paired_p = likelihood( pairtree, DNA[DNA_G, DNA_G, DNA_G], DNA[DNA_C, DNA_C, DNA_G] )
   @test single_p > paired_p

   single_p = likelihood( smtree, DNA[DNA_G, DNA_G, DNA_G] ) * likelihood( smtree, DNA[DNA_C, DNA_C, DNA_T] )
   paired_p = likelihood( pairtree, DNA[DNA_G, DNA_G, DNA_G], DNA[DNA_C, DNA_C, DNA_T] )
   @test paired_p > single_p

end

@testset "RNADuplexArray Building and Traversal" begin

   # test depth and sai composition
   seq = dna"AAATGATGCCGCAGGGGGGGGGGTGCGGCAATCATTT"
   rsuf = RNASuffixArray{DNAAlphabet{2},UInt8,UInt8}( seq, 10 )
   for i in 1:length(rsuf.sai)-1
      @test <=( rsuf.depth, i, rsuf.depth, i+1 )
   end

   push!( rsuf, reverse(seq) )
   for i in 1:length(rsuf.sai)-1
      @test <=( rsuf.depth, i, rsuf.depth, i+1 )
   end

   # test basic duplex array building and traversal
   rda = RNADuplexArray{DNAAlphabet{2},UInt8,UInt8}( seq, 25 )
   @test length(collect(traverse( rda, 10:50, bulge_max=0 ), minenergy=-15.0)) == 0
   @test length(collect(traverse( rda, 10:50, bulge_max=1 ), minenergy=-15.0)) == 1
   @test length(traverse( rda, 10:50, bulge_max=1 )) == 1
   res = shift!(collect(traverse( rda, 10:50, bulge_max=1 )))
   @test first(res.first) == 1 && last(res.first) == 13
   @test first(res.last) == 24 && last(res.last) == length(seq)

   # test stitching of smaller duplex collection
   rda = RNADuplexArray{DNAAlphabet{2},UInt8,UInt8}( seq, 10 )
   res = collect(traverse(rda, bulge_max=1, minfold=-6.0))
   @test length(res) == 5
   i = 1
   for j in res
      @test j.first.first == i
      i += 1
   end
   sti = stitch(traverse(rda, bulge_max=1, minfold=-6.0))
   @test length(collect(sti)) == 1

   # TODO
   # test fwd and rev sequences for intermolecular constructor
   rda  = RNADuplexArray{DNAAlphabet{2},UInt8,UInt8}( seq, seq, 25 )
   res  = collect(traverse( rda, bulge_max=1 ), minenergy=-15.0)

   # test push! additional sequences
   prda = RNADuplexArray{DNAAlphabet{2},UInt8,UInt8}( seq, 25 )
   push!( prda, seq )
   pres = collect(traverse( prda, bulge_max=1 ), minenergy=-15.0)

   # test vector constructor

   # test larger scale example hnRNP D and genome offsets

   # test recursive stitching
   # test iterval collection and filtering

end

@testset "RNADuplexArray EvoDuplexes: building, traversal, stitching, scoring" begin
      maffile = """
##maf version=1 scoring=roast.v3.3
a score=-50707.000000
s hg19.chr22                     16092279 32 +  51304566 AAATGATGCCGCAGGGG------GGGGGGTGCGGCAATCATTT
s panTro4.chr22                  14470459 32 +  49737984 AAATGATGCCGCAGGGG------GGGGGGTGCGGCAATCATTT
"""
   mafread = MAFReader(BufferedInputStream(IOBuffer(maffile * "\n")))
   mafrec = MAFRecord()
   read!(mafread, mafrec)
   @test done(mafread)
   @test length(mafrec) == 2

   deletegaps!(mafrec)
   tree = parsenewick("(hg19:0.006591,panTro4:0.006639);")

   # correct match of tree species and maf file
   phylosuf = RNASuffixArray{DNAAlphabet{4}, UInt16, UInt8}( mafrec, tree, 10 )
   @test phylosuf.depth == phylosuf.species[1]
   @test length(phylosuf.species) == 1

   # missing species end of tree from maf
   tree = parsenewick("((hg19:0.006591,panTro4:0.006639):0.002184,gorGor1:0.009411);")
   phylosuf = RNASuffixArray{DNAAlphabet{4}, UInt16, UInt8}( mafrec, tree, 10 )
   @test phylosuf.depth == phylosuf.species[1]
   @test phylosuf.depth != phylosuf.species[2]
   @test length(phylosuf.species) == 2

   # missing species middle of tree from maf
   mafrec[2].name = "gorGor1"
   tree = parsenewick("((hg19:0.006591,panTro4:0.006639):0.002184,gorGor1:0.009411);")
   phylosuf = RNASuffixArray{DNAAlphabet{4}, UInt16, UInt8}( mafrec, tree, 10 )
   @test phylosuf.depth != phylosuf.species[1]
   @test phylosuf.depth == phylosuf.species[2]
   @test length(phylosuf.species) == 2

   # compare to single seq
   seq = dna"AAATGATGCCGCAGGGGGGGGGGTGCGGCAATCATTT"
   rda = RNADuplexArray{DNAAlphabet{2},UInt8,UInt8}( seq, 10 )
   fwd_dep = rda.fwd.depth
   @test fwd_dep == phylosuf.depth

   # test push! MAF record
   push!( phylosuf, mafrec, tree )
   @test length(phylosuf.depth[1]) == length(phylosuf.species[2][1])
   for i in 1:length(phylosuf.sai)-1
      @test <=( phylosuf.depth, i, phylosuf.depth, i+1 )
      @test <=( phylosuf.species[2], i, phylosuf.species[2], i+1 )
   end

   # test build RNADuplexArray from MAF file...
   smtree   = parsenewick("((hg19:0.006591,panTro4:0.006639):0.002184,gorGor1:0.15);")
   pairtree = deepcopy(smtree)
   set_prob_mat!( smtree, GTR_SINGLE_Q )
   set_prob_mat!( pairtree, GTR_PAIRED_Q )

   # test stitching of EvoDuplexes with toy case
   rda = RNADuplexArray{DNAAlphabet{2},UInt8,UInt8}( mafrec, smtree, 10 )
   res = collect(traverse( rda, bulge_max=1, single=smtree, minfold=-6.0 ))
   for i in 1:length(res)-1
      sti = stitch( res[i], res[i+1], 3, 3 )
      println(sti)
      @test !isnull(sti)
      left,right = strings(sti.value.duplex)
      left_nts   = collect(DNASequence(RNASequence(left)))
      right_nts  = collect(DNASequence(RNASequence(right)))
      @test sti.value.duplex.alignment[1,1:length(left_nts)] == left_nts
      @test sti.value.duplex.alignment[1,length(left_nts)+1:end] == reverse(right_nts)
      @test length(sti.value.duplex.bracket) == length(left_nts) + length(right_nts)
      @test sti.value.duplex.first == length(left_nts)
   end

   # try stitching of EvoDuplexes with complex real-world test case
   maffile = """
##maf version=1 scoring=roast.v3.3
a score=-50707.000000
s hg19.chr22                     16092279 32 +  51304566 ACTCTATCTTAAGCTTTTCTGCTTTTTAATTATCCTGAAGTAAAGATCTTTGCTGATCTTCTGACTTTAGTGAACCTATTAATGTGCTGCAGGCCCCAGTCAAAACTGGAACCAGGGATATAGTAACTATTGGAATCAAG--------------AGACCAGTTCTTGGAGTTATATCCTTTCTTAGGTGACTAGGCCTGCTGCACAATAATAGGTTAATTAAAGTCAGAAGAAGGTCAGCAAAGATGGATTGGGTGAGATTGGGGCCCTTTTCTTAGAAGGGCAGAGATACTAAGCACTG
s panTro4.chr22                  14470459 32 +  49737984 ACTCTATCTTAAGCTTTTCTGCTTTTTAATTATCCTGAAGTAAAGATCTTTGCTGATCTTCTGACTTTAGTGAACCTATTAATGTGCTGCAGGCCCCAGTCAAAACTGGAACCAGGGATATAGTAACTATTGGAATCAAG--------------AGACCAGTTCTTGGAGTTATATCCTTTCTTAGGTGACTAGGCCTGCTGCACAATAATAGGTTAATTAAAGTCAGAAGAAGGTCAGCAAAGATGGATTGGGTGAGATTGGGGCCCTTTTCTTAGAAGGGCAGAGATACTAAGCACTG
"""
   mafread = MAFReader(BufferedInputStream(IOBuffer(maffile * "\n")))
   mafrec = MAFRecord()
   read!(mafread, mafrec)
   @test done(mafread)
   @test length(mafrec) == 2

   rda = RNADuplexArray{DNAAlphabet{2},UInt16,UInt16}( mafrec, smtree, 50 )
   res = collect(traverse( rda, bulge_max=3, mismatch_max=3, single=smtree, minfold=-6.0 ))
   for i in 1:length(res)-1
      sti = stitch( res[i], res[i+1], 3, 3 )
      isnull(sti) && continue
      println(res[i])
      println(res[i+1])
      println(sti)
      left,right = strings(sti.value.duplex)
      left_nts   = collect(DNASequence(RNASequence(left)))
      right_nts  = collect(DNASequence(RNASequence(right)))
      @test sti.value.duplex.alignment[1,1:length(left_nts)] == left_nts
      @test sti.value.duplex.alignment[1,length(left_nts)+1:end] == reverse(right_nts)
      @test length(sti.value.duplex.bracket) == length(left_nts) + length(right_nts)
      @test sti.value.duplex.first == length(left_nts)
   end

   # score using EvoFold phylo-likelihood model
   # str,unstr = score!(res[1].duplex, smtree, pairtree)
   # @test str - unstr > 0
end

@testset "Distance-specific IsolationForests: train, predict" begin

   data = open(readdlm, "hnrnpd.jlt")
   df = DistanceForest()

   train!( df, data )
   p = predict( df, data )

   @test length(p)/size(data, 1) < 0.02
   @test data[p[1], 1] == 859

end


@testset "Peptide" begin
    n_steps = 100
    temp = 298.0u"K"
    s = System(
        joinpath(data_dir, "5XER", "gmx_coords.gro"),
        joinpath(data_dir, "5XER", "gmx_top_ff.top");
        loggers=Dict(
            "temp"   => TemperatureLogger(10),
            "coords" => CoordinateLogger(10),
            "energy" => TotalEnergyLogger(10),
            "writer" => StructureWriter(10, temp_fp_pdb),
        ),
    )
    simulator = VelocityVerlet(dt=0.0002u"ps", coupling=AndersenThermostat(temp, 10.0u"ps"))

    true_n_atoms = 5191
    @test length(s.atoms) == true_n_atoms
    @test length(s.coords) == true_n_atoms
    @test size(s.neighbor_finder.nb_matrix) == (true_n_atoms, true_n_atoms)
    @test size(s.neighbor_finder.matrix_14) == (true_n_atoms, true_n_atoms)
    @test length(s.pairwise_inters) == 2
    @test length(s.specific_inter_lists) == 3
    @test s.box_size == SVector(3.7146, 3.7146, 3.7146)u"nm"
    show(devnull, first(s.atoms))

    s.velocities = [velocity(a.mass, temp) .* 0.01 for a in s.atoms]
    @time simulate!(s, simulator, n_steps; parallel=false)

    traj = read(temp_fp_pdb, BioStructures.PDB)
    rm(temp_fp_pdb)
    @test BioStructures.countmodels(traj) == 10
    @test BioStructures.countatoms(first(traj)) == 5191
end

@testset "Peptide Float32" begin
    n_steps = 100
    temp = 298.0f0u"K"
    s = System(
        Float32,
        joinpath(data_dir, "5XER", "gmx_coords.gro"),
        joinpath(data_dir, "5XER", "gmx_top_ff.top");
        loggers=Dict(
            "temp"   => TemperatureLogger(typeof(1.0f0u"K"), 10),
            "coords" => CoordinateLogger(typeof(1.0f0u"nm"), 10),
            "energy" => TotalEnergyLogger(typeof(1.0f0u"kJ * mol^-1"), 10),
        ),
    )
    simulator = VelocityVerlet(dt=0.0002f0u"ps", coupling=AndersenThermostat(temp, 10.0f0u"ps"))

    s.velocities = [velocity(a.mass, Float32(temp)) .* 0.01f0 for a in s.atoms]
    @time simulate!(s, simulator, n_steps; parallel=false)
end

@testset "OpenMM protein comparison" begin
    openmm_dir = joinpath(data_dir, "openmm_6mrr")
    ff = OpenMMForceField(joinpath.(ff_dir, ["ff99SBildn.xml", "tip3p_standard.xml", "his.xml"])...)
    sys = System(joinpath(data_dir, "6mrr_equil.pdb"), ff; centre_coords=false)
    neighbors = find_neighbors(sys, sys.neighbor_finder)

    for inter in ("bond", "angle", "proptor", "improptor", "lj", "coul", "all")
        if inter == "all"
            pin = sys.pairwise_inters
        elseif inter == "lj"
            pin = sys.pairwise_inters[1:1]
        elseif inter == "coul"
            pin = sys.pairwise_inters[2:2]
        else
            pin = ()
        end

        if inter == "all"
            sils = sys.specific_inter_lists
        elseif inter == "bond"
            sils = sys.specific_inter_lists[1:1]
        elseif inter == "angle"
            sils = sys.specific_inter_lists[2:2]
        elseif inter == "proptor"
            sils = sys.specific_inter_lists[3:3]
        elseif inter == "improptor"
            sils = sys.specific_inter_lists[4:4]
        else
            sils = ()
        end

        sys_part = System(
            atoms=sys.atoms,
            pairwise_inters=pin,
            specific_inter_lists=sils,
            coords=sys.coords,
            box_size=sys.box_size,
            neighbor_finder=sys.neighbor_finder,
        )

        forces_molly = forces(sys_part, neighbors; parallel=false)
        forces_openmm = SVector{3}.(eachrow(readdlm(joinpath(openmm_dir, "forces_$(inter)_only.txt"))))u"kJ * mol^-1 * nm^-1"
        # All force terms on all atoms must match at some threshold
        @test !any(d -> any(abs.(d) .> 1e-6u"kJ * mol^-1 * nm^-1"), forces_molly .- forces_openmm)

        E_molly = potential_energy(sys_part, neighbors)
        E_openmm = readdlm(joinpath(openmm_dir, "energy_$(inter)_only.txt"))[1] * u"kJ * mol^-1"
        # Energy must match at some threshold
        @test E_molly - E_openmm < 1e-5u"kJ * mol^-1"
    end

    # Run a short simulation with all interactions
    n_steps = 100
    simulator = VelocityVerlet(dt=0.0005u"ps")
    velocities_start = SVector{3}.(eachrow(readdlm(joinpath(openmm_dir, "velocities_300K.txt"))))u"nm * ps^-1"
    sys.velocities = deepcopy(velocities_start)
    @test kinetic_energy(sys) ≈ 65521.87288132431u"kJ * mol^-1"
    @test temperature(sys) ≈ 329.3202932884933u"K"

    simulate!(sys, simulator, n_steps; parallel=true)

    coords_openmm = SVector{3}.(eachrow(readdlm(joinpath(openmm_dir, "coordinates_$(n_steps)steps.txt"))))u"nm"
    vels_openmm   = SVector{3}.(eachrow(readdlm(joinpath(openmm_dir, "velocities_$(n_steps)steps.txt" ))))u"nm * ps^-1"

    coords_diff = sys.coords .- wrap_coords_vec.(coords_openmm, (sys.box_size,))
    vels_diff = sys.velocities .- vels_openmm
    # Coordinates and velocities at end must match at some threshold
    @test maximum(maximum(abs.(v)) for v in coords_diff) < 1e-9u"nm"
    @test maximum(maximum(abs.(v)) for v in vels_diff  ) < 1e-6u"nm * ps^-1"

    # Test with no units
    ff_nounits = OpenMMForceField(
        joinpath.(ff_dir, ["ff99SBildn.xml", "tip3p_standard.xml", "his.xml"])...;
        units=false,
    )
    sys_nounits = System(
        joinpath(data_dir, "6mrr_equil.pdb"),
        ff_nounits;
        velocities=deepcopy(ustrip_vec.(velocities_start)),
        units=false,
        centre_coords=false,
    )
    simulator_nounits = VelocityVerlet(dt=0.0005)
    @test kinetic_energy(sys_nounits)u"kJ * mol^-1" ≈ 65521.87288132431u"kJ * mol^-1"
    @test temperature(sys_nounits)u"K" ≈ 329.3202932884933u"K"

    simulate!(sys_nounits, simulator_nounits, n_steps; parallel=true)

    coords_diff = sys_nounits.coords * u"nm" .- wrap_coords_vec.(coords_openmm, (sys.box_size,))
    vels_diff = sys_nounits.velocities * u"nm * ps^-1" .- vels_openmm
    @test maximum(maximum(abs.(v)) for v in coords_diff) < 1e-9u"nm"
    @test maximum(maximum(abs.(v)) for v in vels_diff  ) < 1e-6u"nm * ps^-1"

    params_dic = extract_parameters(sys_nounits, ff_nounits)
    @test length(params_dic) == 639
    atoms_grad, pis_grad, sis_grad = inject_gradients(sys_nounits, params_dic)
    @test atoms_grad == sys_nounits.atoms
    @test pis_grad == sys_nounits.pairwise_inters

    # Test the same simulation on the GPU
    if run_gpu_tests
        sys = System(
            joinpath(data_dir, "6mrr_equil.pdb"),
            ff;
            velocities=cu(deepcopy(velocities_start)),
            gpu=true,
            centre_coords=false,
        )
        simulate!(sys, simulator, n_steps)

        coords_diff = Array(sys.coords) .- wrap_coords_vec.(coords_openmm, (sys.box_size,))
        vels_diff = Array(sys.velocities) .- vels_openmm
        @test maximum(maximum(abs.(v)) for v in coords_diff) < 1e-9u"nm"
        @test maximum(maximum(abs.(v)) for v in vels_diff  ) < 1e-6u"nm * ps^-1"

        sys_nounits = System(
            joinpath(data_dir, "6mrr_equil.pdb"),
            ff_nounits;
            velocities=cu(deepcopy(ustrip_vec.(velocities_start))),
            units=false,
            gpu=true,
            centre_coords=false,
        )
        simulate!(sys_nounits, simulator_nounits, n_steps)

        coords_diff = Array(sys_nounits.coords * u"nm") .- wrap_coords_vec.(coords_openmm, (sys.box_size,))
        vels_diff = Array(sys_nounits.velocities * u"nm * ps^-1") .- vels_openmm
        @test maximum(maximum(abs.(v)) for v in coords_diff) < 1e-9u"nm"
        @test maximum(maximum(abs.(v)) for v in vels_diff  ) < 1e-6u"nm * ps^-1"

        params_dic_gpu = extract_parameters(sys_nounits, ff_nounits)
        @test params_dic == params_dic_gpu
        atoms_grad, pis_grad, sis_grad = inject_gradients(sys_nounits, params_dic_gpu)
        @test atoms_grad == sys_nounits.atoms
        @test pis_grad == sys_nounits.pairwise_inters
    end
end

@testset "Implicit solvent" begin
    openmm_dir = joinpath(data_dir, "openmm_6mrr")
    ff = OpenMMForceField(joinpath.(ff_dir, ["ff99SBildn.xml", "his.xml"])...)
    sys = System(
        joinpath(data_dir, "6mrr_nowater.pdb"),
        ff;
        box_size=SVector(100.0, 100.0, 100.0)u"nm",
        implicit_solvent="obc2",
        dist_cutoff=5.0u"nm",
        nl_dist=5.0u"nm",
    )
    neighbors = find_neighbors(sys)

    forces_molly = forces(sys, neighbors)
    forces_openmm = SVector{3}.(eachrow(readdlm(joinpath(openmm_dir, "forces_obc2.txt"))))u"kJ * mol^-1 * nm^-1"
    @test !any(d -> any(abs.(d) .> 1e-3u"kJ * mol^-1 * nm^-1"), forces_molly .- forces_openmm)

    E_molly = potential_energy(sys, neighbors)
    E_openmm = readdlm(joinpath(openmm_dir, "energy_obc2.txt"))[1] * u"kJ * mol^-1"
    @test E_molly - E_openmm < 1e-2u"kJ * mol^-1"
end


function constructnetwork!(m::JuMP.AbstractModel, branch_models::Array{NamedTuple{(:device, :formulation), Tuple{DataType,DataType}}}, netinjection::BalanceNamedTuple, system_formulation::Type{S}, sys::PowerSystems.PowerSystem; args...) where {S <: CopperPlatePowerModel}

    copperplatebalance(m, netinjection, sys.time_periods)

end

function constructnetwork!(m::JuMP.AbstractModel, branch_models::Array{NamedTuple{(:device, :formulation), Tuple{DataType,DataType}}}, netinjection::BalanceNamedTuple, system_formulation::Type{S}, sys::PowerSystems.PowerSystem; args...) where {S <: AbstractFlowForm}

    for category in branch_models
        constructdevice!(m, netinjection, category.device, category.formulation, system_formulation, sys; args...)
    end

    nodalflowbalance(m, netinjection, system_formulation, sys)

end

function constructnetwork!(m::JuMP.AbstractModel, branch_models::Array{NamedTuple{(:device, :formulation), Tuple{DataType,DataType}}}, netinjection::BalanceNamedTuple, system_formulation::Type{StandardPTDF}, sys::PowerSystems.PowerSystem; args...)
    if :PTDF in keys(args)
        PTDF = args[:PTDF]
    else
        PTDF = nothing
    end

    if !isa(PTDF,PTDFArray)
        @warn "no PTDF supplied"
        PTDF,  A = PowerSystems.buildptdf(sys.branches, sys.buses)
    end

    for category in branch_models
        constructdevice!(m, netinjection, category.device, category.formulation, system_formulation, sys; args..., PTDF=PTDF)
    end

    nodalflowbalance(m, netinjection, system_formulation, sys)

end

function constructnetwork!(m::JuMP.AbstractModel, branch_models::Array{NamedTuple{(:device, :formulation), Tuple{DataType,DataType}}}, netinjection::BalanceNamedTuple, system_formulation::Type{S}, sys::PowerSystems.PowerSystem; args...) where {S <: AbstractDCPowerModel}

    #= TODO: Needs to be generalized later for other branch models not covered by PM.
    for category in branch_models
        constructdevice!(m, netinjection, category.device, category.formulation, system_formulation, sys; args...)
    end
    =#

    nodalflowbalance(m, netinjection, system_formulation, sys)

    PM_F = (data::Dict{String,Any}; kwargs...) -> PM.GenericPowerModel(data, system_formulation; kwargs...)

    PM_object = PS.build_nip_expr_model(m.ext[:PM_object], PM_F, jump_model=m);

    m = PM_object.model

end

function constructnetwork!(m::JuMP.AbstractModel, branch_models::Array{NamedTuple{(:device, :formulation), Tuple{DataType,DataType}}}, netinjection::BalanceNamedTuple, system_formulation::Type{S}, sys::PowerSystems.PowerSystem; args...) where {S <: AbstractACPowerModel}

    #= TODO: Needs to be generalized later for other branch models not covered by PM.
    for category in branch_models
        constructdevice!(m, netinjection, category.device, category.formulation, system_formulation, sys; args...)
    end
    =#

    nodalflowbalance(m, netinjection, system_formulation, sys)

    PM_F = (data::Dict{String,Any}; kwargs...) -> PM.GenericPowerModel(data, system_formulation; kwargs...)

    PM_object = PS.build_nip_expr_model(m.ext[:PM_object], PM_F, jump_model=m);

    m.ext[:PM_object] = PM_object

end