struct WeekdayHolidayObsModel{T,M,A,P,E,W,H,V} <: CovidSurvey.ObservationsModel
    ps::T
    μ::M
    α::A
    ϕ::P
	expecteds::E
    weekdayeffect::W
    holidayeffect::H
	vectorize::V
end

struct SimpleObsModel{T,M,A,P,E,V} <: CovidSurvey.ObservationsModel
    ps::T
    μ::M
    α::A
    ϕ::P
	expecteds::E
	vectorize::V
end

struct SimpleObsModel2{T,M,S,A,P,E,V} <: CovidSurvey.ObservationsModel
    ps::T
    μ::M
	σ::S
    α::A
    ϕ::P
	expecteds::E
	vectorize::V
end

rescale(xs, σ, μs) = @. xs * σ + μs'
# ============================================================================
# random walk model
function random_walks!(Rts, θ, predict, latent_Rts::AbstractVector, R0s, σ_rt)
	@unpack num_regions, rt_step_indices, lockdown_indices, num_observations, link, include_early_dynamic = θ #include_early_dynamic

	for m in 1:num_regions
		rt_step_index = rt_step_indices[m]
		Rt            = Rts[m]
		lockdown      = lockdown_indices[m]
		num_obs       = num_observations[m]
		latent_Rt     = latent_Rts[m]#latent_Rts[:,m] #include_early_dynamic ? latent_Rts[m] : latent_Rts[:,m]
		R0            = R0s[m]

		if include_early_dynamic
			Rt[1:num_obs] = link.(latent_Rt[rt_step_index])
		else
			Rt[1:lockdown] .= R0
			Rt[lockdown+1:num_obs] = link.(latent_Rt[rt_step_index])
		end
		if predict
			Rt[num_obs+1:end] .= Rt[num_obs]
		end
	end
end

function random_walks!(Rts, θ, predict, latent_Rts::AbstractMatrix, R0s, σ_rt)
	@unpack num_regions, rt_step_indices, lockdown_indices, num_observations, link, include_early_dynamic = θ

	for m in 1:num_regions
		rt_step_index = rt_step_indices[m]
		Rt            = Rts[m]
		lockdown      = lockdown_indices[m]
		num_obs       = num_observations[m]
		latent_Rt     = latent_Rts[:,m] #include_early_dynamic ? latent_Rts[m] : latent_Rts[:,m]
		R0            = R0s[m]

		if include_early_dynamic
			Rt[1:num_obs] = link.(latent_Rt[rt_step_index])
		else
			Rt[1:lockdown] .= R0
			Rt[lockdown+1:num_obs] = link.(latent_Rt[rt_step_index])
		end
		if predict
			Rt[num_obs+1:end] .= Rt[num_obs]
		end
	end
end

# function random_walks!(Rts, θ, predict, latent_Rts, R0s)
# 	@unpack num_regions, rt_step_indices, lockdown_indices, num_observations, link = θ
#
# 	for m in 1:num_regions
# 		rt_step_index = rt_step_indices[m]
# 		Rt            = Rts[m]
# 		lockdown      = lockdown_indices[m]
# 		num_obs       = num_observations[m]
# 		latent_Rt     = latent_Rts[:,m]
# 		R0            = R0s[m]
#
# 		Rt[1:lockdown] .= R0
# 		Rt[lockdown+1:num_obs] = link.(latent_Rt[rt_step_index])
# 		if predict
# 			Rt[num_obs+1:end] .= Rt[num_obs]
# 		end
# 	end
# end

# ============================================================================
# semi-parametric model
function semiparametric!(Rts, θ, predict, latent_Rts, R0s, σ_rt, effects)
	@unpack num_regions, lockdown_indices, num_observations, link, rt_step_indices, covariates, covariates_starts, num_total_days, include_early_dynamic= θ #rt_step_indices

	for m in 1:num_regions
		Rt            = Rts[m]
		lockdown      = lockdown_indices[m]
		rt_step_index = rt_step_indices[m]
		num_obs       = num_observations[m]
		latent_Rt     = include_early_dynamic ? latent_Rts[m] : latent_Rts[:,m] #latent_Rts[m]
		R0            = R0s[m]
		num_total     = num_total_days[m]
		covariate     = covariates[m]
		effect        = effects[m]
		covariates_start = covariates_starts[m]

		if include_early_dynamic
			Rt[1:covariates_start-1] = link.(latent_Rt[rt_step_index])
		else
			Rt[1:lockdown] .= R0
			Rt[lockdown+1:covariates_start-1] = link.(latent_Rt[rt_step_index])
		end
		Rt[covariates_start:num_obs] = link.(
			latent_Rt[end] .+ covariate * effect
		)
		if predict
			Rt[num_obs+1:num_total] .= Rt[num_obs]
			# @unpack num_total_days, invlink = θ
			# n  = num_total_days - num_observations
			# n < 1 && return nothing
			# rw = RandomWalk(n, σ_rt, invlink(Rt[num_observations]))
			# Rt[num_observations+1:end] = link.( rand( rw ) )
		end
	end
	return nothing
end

# ============================================================================
# semi-parametric model
function mixed!(Rts, θ, predict, latent_Rts, R0s, σ_rt, effects)
	@unpack (
		num_regions,
		lockdown_indices,
		num_observations,
		link,
		rt_step_indices,
		covariates,
		covariates_starts,
		num_total_days,
		include_early_dynamic
	) = θ #rt_step_indices

	for m in 1:num_regions
		Rt               = Rts[m]
		lockdown_index   = lockdown_indices[m]
		rt_step_index    = rt_step_indices[m]
		num_obs          = num_observations[m]
		latent_Rt        = include_early_dynamic ? latent_Rts[m] : latent_Rts[:,m]#latent_Rts[m]
		R0               = R0s[m]
		num_total        = num_total_days[m]
		covariate        = covariates[m]
		effect           = effects[m]
		covariates_start = covariates_starts[m]
		predictor        = covariate * effect

		if include_early_dynamic
			for i in 1:covariates_start-1
				Rt[i] = link(latent_Rt[rt_step_index[i]])
			end
			for (j,i) in enumerate(covariates_start:num_obs)
				Rt[i] = link(latent_Rt[rt_step_index[i]] + predictor[j])
			end
		else
			Rt[1:lockdown_index] .= R0
			for (j,i) in enumerate(lockdown_index+1:covariates_start-1)
				Rt[i] = link(latent_Rt[rt_step_index[j]])
			end
			n = covariates_start - lockdown_index - 1
			for (j,i) in enumerate(covariates_start:num_obs)
				Rt[i] = link(latent_Rt[rt_step_index[j+n]] + predictor[j])
			end
		end
		if predict
			Rt[num_obs+1:num_total] .= Rt[num_obs]
			# @unpack num_total_days, invlink = θ
			# n  = num_total_days - num_observations
			# n < 1 && return nothing
			# rw = RandomWalk(n, σ_rt, invlink(Rt[num_observations]))
			# Rt[num_observations+1:end] = link.( rand( rw ) )
		end
	end
	return nothing
end

# ============================================================================
# infection model
function infections!(newly_infecteds, cumulative_infecteds, θ, ys, Rts)
	@unpack num_regions, populations = θ
	for m in 1:num_regions
		regionaldata = (
			Rt                     = Rts[m],
			population             = populations[m],
			y                      = ys[m]
		)

		initepidemic!(newly_infecteds[m], cumulative_infecteds[m], θ, regionaldata)
		runepidemic!(newly_infecteds[m], cumulative_infecteds[m], θ, regionaldata)
	end
end

function initepidemic!(newly_infected, cumulative_infected, effective_Rt, θ, regionaldata)
	@unpack num_impute = θ
	@unpack y, Rt, population = regionaldata
	newly_infected[1]      = y
	cumulative_infected[1] = zero( eltype(cumulative_infected) )
	effective_Rt[1]        = Rt[1]
	for t in 2:num_impute
		newly_infected[t] = y
		cumulative_infected[t] = cumulative_infected[t-1] + y
		St = max(population - cumulative_infected[t], 0) / population
		effective_Rt[t] = St * Rt[t]
	end
end

function initepidemic!(newly_infected, cumulative_infected, θ, regionaldata)
	@unpack num_impute = θ
	@unpack y, population = regionaldata
	newly_infected[1]      = y
	cumulative_infected[1] = zero( eltype(cumulative_infected) )
	for t in 2:num_impute
		newly_infected[t] = y
		cumulative_infected[t] = cumulative_infected[t-1] + y
		# St = max(population - cumulative_infected[t], 0) / population
	end
end

function runepidemic!(newly_infected, cumulative_infected, effective_Rts, θ, regionaldata)
	@unpack num_impute, serial_interval, num_si = θ
	@unpack population, Rt = regionaldata
	num_time_step = length(newly_infected)

	for t = (num_impute + 1):num_time_step
		# Update cumulatively infected
		C = cumulative_infected[t] = cumulative_infected[t-1] + newly_infected[t - 1]
		susceptible = max(population - C, 0)

		# Adjusts for portion of population that is susceptible
		#St = max(population - cumulative_infected[t], 0) / population
		# effective number of infectious individuals
		effectively_infectious = sum(newly_infected[τ] * serial_interval[t - τ] for τ = (t - 1):-1:max(t-num_si,1))

		unadjusted_new_infections = effectively_infectious * Rt[t]

		# number of new infections (unobserved)
		# newly_infected[t] = susceptible * (1 - exp(- unadjusted_new_infections / population))
		newly_infected[t] = susceptible * (1 - exp(- unadjusted_new_infections / population))
		# newly_infected[t] = effective_Rt[t] * effectively_infectious

		effective_Rt[t] = susceptible / population * Rt[t]
	end
end

function runepidemic!(newly_infected, cumulative_infected, θ, regionaldata)
	@unpack num_impute, serial_interval, num_si = θ
	@unpack population, Rt = regionaldata
	num_time_step = length(newly_infected)

	for t = (num_impute + 1):num_time_step
		# Update cumulatively infected
		C = cumulative_infected[t] = cumulative_infected[t-1] + newly_infected[t - 1]
		susceptible = max(population - C, 0)

		# Adjusts for portion of population that is susceptible
		effectively_infectious = sum(newly_infected[τ] * serial_interval[t - τ] for τ = (t - 1):-1:max(t-num_si,1))

		unadjusted_new_infections = effectively_infectious * Rt[t]

		# number of new infections (unobserved)
		# newly_infected[t] = susceptible * (1 - exp(- unadjusted_new_infections / population))
		newly_infected[t] = susceptible * (1 - exp(- unadjusted_new_infections / population))

	end
	return nothing
end

# ============================================================================
# unbound infection model
function infections!(newly_infecteds, θ, ys, Rts)
	@unpack num_regions = θ
	for m in 1:num_regions
		regionaldata = (
			Rt                     = Rts[m],
			y                      = ys[m]
		)

		initepidemic!(newly_infecteds[m], θ, regionaldata)
		runepidemic!(newly_infecteds[m], θ, regionaldata)
	end
end

function initepidemic!(newly_infected, θ, regionaldata)
	@unpack num_impute = θ
	@unpack y, Rt = regionaldata
	newly_infected[1] = y
	for t in 2:num_impute
		newly_infected[t] = y
	end
end

function runepidemic!(newly_infected, θ, regionaldata)
	@unpack num_impute, serial_interval, num_si = θ
	@unpack Rt = regionaldata
	num_time_step = length(newly_infected)

	for t = (num_impute + 1):num_time_step
		effectively_infectious = sum(newly_infected[τ] * serial_interval[t - τ] for τ = (t - 1):-1:max(t-num_si,1))
		newly_infected[t] = effectively_infectious * Rt[t]
	end
	return nothing
end

# ============================================================================
# observations

function infection2observation(obsmodel::SimpleObsModel)
	@unpack ps, μ = obsmodel
	@unpack delay_dispersion, delay_length, delay_dist = ps
	i2o = pdf.( Ref(delay_dist(μ, delay_dispersion)), 1:delay_length )
	i2o /= sum(i2o)
	return i2o
end

function infection2observation(obsmodel::SimpleObsModel2)
	@unpack ps, μ, σ = obsmodel
	@unpack  delay_length, delay_dist = ps
	i2o = pdf.( Ref(delay_dist(μ, σ)), 1:delay_length )
	i2o /= sum(i2o)
	return i2o
end

function expected!(obsmodel::Union{SimpleObsModel, SimpleObsModel2}, newly_infecteds)
	@unpack ps, α, expecteds = obsmodel
	@unpack num_regions, delay_length = ps

	i2o = infection2observation(obsmodel)

	for m in 1:num_regions

		expected       = expecteds[m]
		newly_infected = newly_infecteds[m]
		num_time_step  = length(expected)

		expected[1] = 1e-15*newly_infected[1]
		for t = 2:num_time_step
			expected[t] = α * sum(newly_infected[τ] * i2o[t - τ] for τ = (t - 1):-1:max(t-delay_length,1))
			# expected[t] = α * sum(newly_infected[τ] * i2o[t - τ] for τ = (t - 1):-1:max(t-delay_length,1))
		end
	end
	return nothing
end

function expected!(obsmodel::WeekdayHolidayObsModel, newly_infecteds)
	@unpack ps, α, expecteds, holidayeffect, weekdayeffect = obsmodel
	@unpack num_regions, delay_length, holidays, weekdays = ps

	i2o = infection2observation(obsmodel)

	for m in 1:num_regions

		expected       = expecteds[m]
		newly_infected = newly_infecteds[m]
		num_time_step  = length(expected)
		holiday        = holidays[m]
		weekday        = weekdays[m]

		expected[1] = 1e-15*newly_infected[1]
		for t = 2:num_time_step
			weekday_holiday_effect = (1 - holidayeffect * holiday[t]) * weekdayeffect[weekday[t]]
			expected[t] = α * weekday_holiday_effect * sum(newly_infected[τ] * i2o[t - τ] for τ = (t - 1):-1:max(t-delay_length,1))
		end
	end
	return nothing
end

function _fastinference(ys, μs, ϕ)
	dist = arraydist(NegativeBinomial2.(μs, Ref(ϕ)))
	return logpdf(dist, ys)
end

function _slowinference(ys, μs, ϕ)
	ℓ = zero(typeof(ϕ))
	for (μ,y) in zip(μs,ys)
		dist = NegativeBinomial2(μ, ϕ)
		ℓ   += logpdf(dist, y)
	end
	return ℓ
end

function _prediction(ys, μs, ϕ)
	ℓ = zero(typeof(ϕ))
	for μ in μs
		dist = NegativeBinomial2(μ, ϕ)
		y    = rand(dist)
		ℓ   += logpdf(dist, y)
	end
	return ℓ
end

function Turing.logpdf(obsmodel::CovidSurvey.ObservationsModel, observed)
	@unpack ps, ϕ, expecteds, vectorize = obsmodel
	@unpack populations, starts, stops, num_regions = ps

	# _logpdf = if vectorize
	# 	_fastinference
	# else
	# 	if eltype(ϕ) <: Real
	# 		_prediction
	# 	else
	# 		_slowinference
	# 	end
	# end
	_logpdf = _fastinference

	T = typeof(ϕ)
	ℓ = zero(T)
	for m in 1:num_regions
		ts    = starts[m]:stops[m]
		μs = expecteds[m][ts]
		ys = observed[m][ts]

		#_out_of_bounds(μs, populations[m]) && (@warn "out of bounds"; return T(Inf))
		ℓ += _logpdf(ys, μs, ϕ)
	end
	return ℓ
end

_out_of_bounds(x, upper_lim) = !all(@. 0. < x < upper_lim )

# ============================================================================
# pointwise_loglikelihoods
function Turing.pointwise_loglikelihoods(obsmodel::CovidSurvey.ObservationsModel, observed)
	@unpack ps, ϕ, expecteds, vectorize = obsmodel
	@unpack populations, starts, stops, num_regions = ps

	logliks = Vector{Float64}[]
	for m in 1:num_regions
		ts    = starts[m]:stops[m]
		μs = expecteds[m][ts]
		ys = observed[m][ts]

		loglik = [ logpdf(NegativeBinomial2(μ, ϕ), y) for (μ,y) in zip(μs,ys) ]
		push!(logliks, loglik)
	end
	return logliks
end

# ============================================================================
# predictions
function prediction(obsmodel::CovidSurvey.ObservationsModel)
	@unpack ps, ϕ, expecteds = obsmodel
	@unpack starts, stops, num_regions = ps

	ys = Vector{Float64}[]
	for m in 1:num_regions
		μs = expecteds[m]
		μs[μs .< 1e-2] .= 1e-2
		y = [ rand(NegativeBinomial2(μ, ϕ)) for μ in μs ]
		push!(ys, y)
	end
	return ys
end

# ============================================================================
# weekday effect

function weekdayeffect!(weekdayeffect, weekdayeffect_simplex)
	num_days = 7
	for i in eachindex(weekdayeffect)
		@inbounds weekdayeffect[i] = weekdayeffect_simplex[i] * num_days
	end
end

# ============================================================================
# observe sero positive

struct SimpleSeroObsModel{T,E}# <: CovidSurvey.ObservationsModel
    ps::T
	expecteds::E
end

_unrealistic(ŷ,y) = any( ŷ .> 20*y )
# _unrealistic(y,ŷ) = any( ŷ .> 1.5*y )
function Turing.logpdf(obsmodel::SimpleSeroObsModel, observeds)
	@unpack ps, expecteds = obsmodel
	@unpack indexs, delay, populations, std, num_regions = ps

	T = eltype(expecteds[1])
	ℓ = zero(T)
	for m in 1:num_regions
		index      = indexs[m]
		expected   = expecteds[m]
		population = populations[m]
		observed   = observeds[m]

		μ    = expected[index .- delay] / population
		dist = MvNormal(μ, std) # select only early observations
		#_unrealistic(μ, observed) && (return T(Inf))
		ℓ += logpdf(dist, observed)
	end
	return ℓ
end


#Test Values
test_values = (vec([852 845 846 851 846 846 840 825 823 821 836 854 854 850 822 805 802 804 804 823 815 799 802 793 792 807 821 823 809 809 823 840 858 847 823 799 788 788 789 799 805 805 817 853 873 896 900 886 862 840 845 854 858 861 830]))
# Ideal Case (No changes will be performed)
@testset "Ideal Case" begin
    fixed_signal, outliers, nonnormal = adaptiveHRVFilter(test_values)
    @test test_values == fixed_signal
    @test sum(outliers) == 0
    @test sum(nonnormal) == 0
end
fixed_signal, outliers, nonnormal = adaptiveHRVFilter(test_values)

# Changes but possibly physiological
changed_idxs = [20, 21]
changed_values = [630, 1340]
changed_test_values = copy(test_values)
changed_test_values[changed_idxs] = changed_values

@testset "Physiological Changes" begin
    fixed_signal, outliers, nonnormal = adaptiveHRVFilter(changed_test_values)
    notchanged = broadcast(!, nonnormal)
    @test sum(outliers) == 0
    @test changed_test_values[notchanged] == fixed_signal[notchanged]
end

# Changes to non physiological values
changed_idxs = [20, 21]
changed_values = [180, 2500]
changed_test_values = copy(test_values)
changed_test_values[changed_idxs] = changed_values

@testset "Non Physiological Changes" begin
    fixed_signal, outliers, nonnormal = adaptiveHRVFilter(changed_test_values)
    notchanged = broadcast(!, outliers)
    @test sum(nonnormal) == 0
    @test sum(outliers) == 2
    @test length(fixed_signal) + sum(outliers) == length(changed_test_values)
end


#!/usr/bin/env julia
const MOD = 123
const YES = "yes"
const NO = "NO"

function solve(N::Int, M::Int, H::Matrix{String}, A::Vector{Int}, B::Vector{Float64}, Q::Int, X::Vector{Int})
    println("$N $M")
    @assert size(H) == (N-1, M-2)
    for i in 1:size(H,1)
        println(join(H[i, :], " "))
    end
    @assert length(A) == N - 1
    @assert length(B) == N - 1
    for (a, b) in zip(A, B)
        println("$a $b")
    end
    println(Q)
    @assert length(X) == M + Q
    foreach(println, X)

    println(YES)
    println(NO)
    println(MOD)
end

function main()
    tokens = Channel{String}(32)
    Task() do
        for line in eachline(@static VERSION < v"0.6" ? STDIN : stdin)
            for token in split(chomp(line))
                put!(tokens, token)
            end
        end
        close(tokens)
    end |> schedule
    N = parse(Int, take!(tokens))
    M = parse(Int, take!(tokens))
    H = similar(Matrix{String}, N-2+1, M-1-2+1)
    for i in 1:N-2+1
        for j in 1:M-1-2+1
            H[i,j] = take!(tokens)
        end
    end
    A = similar(Vector{Int}, N-2+1)
    B = similar(Vector{Float64}, N-2+1)
    for i in 1:N-2+1
        A[i] = parse(Int, take!(tokens))
        B[i] = parse(Float64, take!(tokens))
    end
    Q = parse(Int, take!(tokens))
    X = similar(Vector{Int}, M+Q)
    for i in 1:M+Q
        X[i] = parse(Int, take!(tokens))
    end
    solve(N, M, H, A, B, Q, X)
end

isempty(ARGS) && main()


# Needleman-Wunsch Algorithm
# ==========================
#
# The Needleman-Wunsch algorithm.
#
# This file is a part of BioJulia.
# License is MIT: https://github.com/BioJulia/BioAlignments.jl/blob/master/LICENSE.md

mutable struct NeedlemanWunsch{T<:Union{Signed,AbstractFloat}}
    trace::Matrix{Trace}
    H::Vector{T}
    E::Vector{T}

    function NeedlemanWunsch{T}(m::Integer, n::Integer) where T
        trace = Matrix{Trace}(undef, (m + 1, n + 1))
        fill!(trace, 0xff)
        H = Vector{T}(undef, m + 1)
        E = Vector{T}(undef, m)
        return new{T}(trace, H, E)
    end

    function NeedlemanWunsch{T}() where T
        return NeedlemanWunsch{T}(0, 0)
    end
end

function ensureroom!(nw::NeedlemanWunsch, m, n)
    if size(nw.trace, 1) < m + 1 || size(nw.trace, 2) < n + 1
        # TODO: resize the trace matrix if possible
        nw.trace = Matrix{Trace}(undef, (m + 1, n + 1))
        nw.H = resize!(nw.H, m + 1)
        nw.E = resize!(nw.E, m)
    end
    return nw
end

# Generate a code block that updates the current score and trace.
macro update()
    esc(quote
        e = E[i]
        g = h_diag + submat[a[i],b_j]
        h = max(e, f, g)
        h_diag = H[i+1]
        H[i+1] = h
        t = trace[i+1,j+1] | ft
        e == h && (t |= TRACE_DELETE)
        f == h && (t |= TRACE_INSERT)
        g == h && (t |= TRACE_MATCH)
        trace[i+1,j+1] = t
    end)
end

macro update_nextcol(gap_init, gap_extend)
    esc(quote
        e′ = e + $gap_extend
        e = max(e′, h + $gap_init)
        E[i] = e
        et = TRACE_NONE
        e == e′ && (et |= TRACE_EXTDEL)
        trace[i+1,j+2] = et
    end)
end

macro update_nextrow(gap_init, gap_extend)
    esc(quote
        f′ = f + $gap_extend
        f = max(f′, h + $gap_init)
        ft = TRACE_NONE
        f == f′ && (ft |= TRACE_EXTINS)
    end)
end

#    start   middle  end gaps of a
#    |       |       |
# a: ----ACCA---ATGTG---
# b: AAATACGATTGATGAGGGT

function run!(
        nw::NeedlemanWunsch{T},
        a, b,
        submat::AbstractSubstitutionMatrix{T},
        # a
        start_gap_open_a::T,
        start_gap_extend_a::T,
        middle_gap_open_a::T,
        middle_gap_extend_a::T,
        end_gap_open_a::T,
        end_gap_extend_a::T,
        # b
        start_gap_open_b::T,
        start_gap_extend_b::T,
        middle_gap_open_b::T,
        middle_gap_extend_b::T,
        end_gap_open_b::T,
        end_gap_extend_b::T) where T

    m = length(a)
    n = length(b)
    ensureroom!(nw, m, n)
    H = nw.H
    E = nw.E
    trace = nw.trace

    start_gap_init_a = start_gap_open_a + start_gap_extend_a
    middle_gap_init_a = middle_gap_open_a + middle_gap_extend_a
    end_gap_init_a = end_gap_open_a + end_gap_extend_a

    start_gap_init_b = start_gap_open_b + start_gap_extend_b
    middle_gap_init_b = middle_gap_open_b + middle_gap_extend_b
    end_gap_init_b = end_gap_open_b + end_gap_extend_b

    # ↓: insertion
    # →: deletion
    # ↘: match/mismatch
    H[1] = T(0)
    trace[1,1] = TRACE_NONE
    if m == 0 && n == 0
        return H[1]
    elseif m == 0
        for j in 1:n
            trace[1,j+1] = TRACE_DELETE
        end
        return start_gap_open_a + start_gap_extend_a * n
    elseif n == 0
        for i in 1:m
            trace[i+1,1] = TRACE_INSERT
        end
        return start_gap_open_b + start_gap_extend_b * m
    else
        # initialize the first column of H, E, and trace
        @inbounds for i in 1:m
            H[i+1] = start_gap_open_b + start_gap_extend_b * i
            E[i] = H[i+1] + (i == m ? end_gap_init_a : middle_gap_init_a)
            trace[i+1,1] = TRACE_INSERT
            trace[i+1,2] = TRACE_NONE
        end

        # run dynamic programming column by column (except the last column)
        @inbounds for j in 1:n-1
            b_j = b[j]
            h_diag = H[1]
            H[1] = start_gap_open_a + start_gap_extend_a * j
            f = H[1] + middle_gap_init_b
            ft = TRACE_NONE
            trace[1,j+1] = TRACE_DELETE
            # inner loop along the sequence a
            for i in 1:m-1
                @update
                @update_nextcol middle_gap_init_a middle_gap_extend_a
                @update_nextrow middle_gap_init_b middle_gap_extend_b
            end
            # fill the final row
            let i = m
                @update
                @update_nextcol end_gap_init_a end_gap_extend_a
            end
        end

        # fill the last column
        let j = n
            b_j = b[j]
            h_diag = H[1]
            H[1] = start_gap_open_a + start_gap_extend_a * j
            f = H[1] + end_gap_init_b
            ft = TRACE_NONE
            trace[1,j+1] = TRACE_DELETE
            # inner loop along the sequence a
            @inbounds for i in 1:m-1
                @update
                @update_nextrow end_gap_init_b end_gap_extend_b
            end
            # finally, fill the last element of the DP matrices
            let i = m
                @update
            end
        end

        return H[m+1]
    end
end

function run!(nw::NeedlemanWunsch, a, b, submat, gap_open, gap_extend)
    return run!(nw, a, b, submat,
        # a
        gap_open,
        gap_extend,
        gap_open,
        gap_extend,
        gap_open,
        gap_extend,
        # b
        gap_open,
        gap_extend,
        gap_open,
        gap_extend,
        gap_open,
        gap_extend
    )
end

function traceback(nw::NeedlemanWunsch, a, b, endpos)
    anchors = Vector{AlignmentAnchor}()
    i, j = endpos
    was_extins = false
    was_extdel = false
    @start_traceback
    while i ≥ 1 || j ≥ 1
        t = nw.trace[i+1,j+1]
        @assert !(was_extins && was_extdel)
        if was_extins
            was_extins = t & TRACE_EXTINS > 0
            @anchor OP_INSERT
        elseif was_extdel
            was_extdel = t & TRACE_EXTDEL > 0
            @anchor OP_DELETE
        elseif t & TRACE_INSERT > 0
            was_extins = t & TRACE_EXTINS > 0
            @anchor OP_INSERT
        elseif t & TRACE_DELETE > 0
            was_extdel = t & TRACE_EXTDEL > 0
            @anchor OP_DELETE
        elseif t & TRACE_MATCH > 0
            if a[i] == b[j]
                @anchor OP_SEQ_MATCH
            else
                @anchor OP_SEQ_MISMATCH
            end
        else
            error("failed to trace back")
        end
    end
    @finish_traceback
    return AlignedSequence(a, anchors)
end