text:
function prune_system_devices!(
    sys::PSY.System,
    prune_dict::Dict{Type{<:PSY.Component}, Array{AbstractString}},
)
    for (type, device_names) in prune_dict
        for name in device_names
            device = PSY.get_components_by_name(supertype(type), sys, name)
            if !isempty(device)
                PSY.remove_component!(sys, device[1])
            end
        end
    end
    return
end

function get_rts_data(; kwargs...)
    dir = get(kwargs, :rts_dir, nothing)
    if isnothing(dir)
        if PSY.UtilsData.os == PSY.UtilsData.Windows
            rts_tempdir = download("https://github.com/GridMod/RTS-GMLC/archive/master.zip")
        else
            rts_tempdir =
                download("https://github.com/GridMod/RTS-GMLC/archive/master.tar.gz")
        end

        PSY.UtilsData.unzip(PSY.UtilsData.os, rts_tempdir, dirname(rts_tempdir))
        rts_dir = joinpath(dirname(rts_tempdir), "RTS-GMLC-master")
        return rts_dir
    else
        return dir
    end
end

function read_rts_data(rts_dir)
    rts_src_dir = joinpath(rts_dir, "RTS_Data", "SourceData")
    rts_siip_dir = joinpath(rts_dir, "RTS_Data", "FormattedData", "SIIP")

    rawsys = PSY.PowerSystemTableData(
        rts_src_dir,
        100.0,
        joinpath(rts_siip_dir, "user_descriptors.yaml"),
        timeseries_metadata_file = joinpath(rts_siip_dir, "timeseries_pointers.json"),
        generator_mapping_file = joinpath(rts_siip_dir, "generator_mapping.yaml"),
    )
    return rawsys
end

function make_tempdir(n::Int64)
    if n > 1
        outdir = [mktempdir() for ix in n]
    else
        outdir = [mktempdir()]
    end
    return outdir
end

function filter_devices(sys::PSY.System, ::Type{T}, f::Function) where {T <: PSY.Component}
    components = PSY.get_components(T, sys)
    filtered_list = filter(x -> f(x), components)
    names = PSY.get_name.(filtered_list)
    return names
end

function combvec(set_a...)
    return vec(collect(Iterators.product(set_a...)))
end

function combvec_samples(no_samples::Int, set_a...)
    set = combvec(set_a...)
    return rand(set, no_samples)
end

function uniform_sample(dict, key, size)
    data = dict[key]
    devices = length(data)
    samples = Array{AbstractString}[]
    for i in 1:devices
        for comb in collect(Combinatorics.combinations(data, i))
            push!(samples, vcat(repeat(data, size - 1), comb))
        end
    end
    return samples
end

function all_combinations(dict, key)
    data = dict[key]
    devices = length(data)
    samples = collect(Combinatorics.combinations(data))
    return samples
end

function calculate_revenue(
    results_cont,
    category_mapping,
    capacity_dict,
    capacity_mapping,
    bus_mapping,
    res_mapping,
    reg_capacity_dict,
    optimizer,
)
    nodal_prices =
        results_cont.dual_values[Symbol("dual_nodal_balance_active__PowerSystems.Bus")]
    reserve_up_prices =
        results_cont.dual_values[Symbol("dual_requirement__PowerSystems.VariableReserve_PowerSystems.ReserveUp")]
    reserve_dw_prices =
        results_cont.dual_values[Symbol("dual_requirement__PowerSystems.VariableReserve_PowerSystems.ReserveDown")]
    dispatch = hcat(
        results_cont.variable_values[Symbol("P__PowerSystems.ThermalStandard")],
        results_cont.variable_values[Symbol("P__PowerSystems.HydroEnergyReservoir")],
        results_cont.variable_values[Symbol("P__PowerSystems.RenewableDispatch")],
        results_cont.variable_values[Symbol("Pout__PowerSystems.GenericBattery")],
    )
    dispatch_rs_up = hcat(
        results_cont.variable_values[Symbol("Reg_Up_R1__PowerSystems.VariableReserve_PowerSystems.ReserveUp")],
        results_cont.variable_values[Symbol("Reg_Up_R2__PowerSystems.VariableReserve_PowerSystems.ReserveUp")],
        results_cont.variable_values[Symbol("Reg_Up_R3__PowerSystems.VariableReserve_PowerSystems.ReserveUp")],
    )
    charge = results_cont.variable_values[Symbol("Pin__PowerSystems.GenericBattery")]
    dispatch_rs_dn = hcat(
        results_cont.variable_values[Symbol("Reg_Down_R1__PowerSystems.VariableReserve_PowerSystems.ReserveDown")],
        results_cont.variable_values[Symbol("Reg_Down_R2__PowerSystems.VariableReserve_PowerSystems.ReserveDown")],
        results_cont.variable_values[Symbol("Reg_Down_R3__PowerSystems.VariableReserve_PowerSystems.ReserveDown")],
    )
    variable_cost = results_cont.variable_values[:PWL_cost_vars]
    training_data = DataFrames.DataFrame(Category = [], Revenue = [], Capacity = [])
    reg_training_data =
        DataFrames.DataFrame(Category = [], Bus = [], Revenue = [], Capacity = [])
    capex_market_bids = Vector{Vector{Union{String, Float64}}}()
    for (cat, devices) in category_mapping
        rev = Dict("Cabell" => 0.0, "Abel" => 0.0, "Bach" => 0.0)
        for d in devices
            _var_cost = 0.0
            if cat in [:ST, :CT, :CC]
                for ix in 1:4
                    _var_cost = +variable_cost[:, Symbol((d, ix))]
                end
            end
            _dispatch = dispatch[:, Symbol(d)]
            _price = abs.(nodal_prices[:, Symbol(bus_mapping[d])])
            _res_revenue = 0.0
            for res_name in res_mapping[d]
                if occursin("Reg_Up", res_name)
                    _rs_price = abs.(reserve_up_prices[:, Symbol(res_name)])
                    _res_revenue += sum(_rs_price .* dispatch_rs_up[:, Symbol(d)])
                elseif occursin("Reg_Down", res_name)
                    _rs_price = abs.(reserve_dw_prices[:, Symbol(res_name)])
                    _res_revenue += sum(_rs_price .* dispatch_rs_dn[:, Symbol(d)])
                end
            end
            if cat == :BA
                _charge = charge[:, Symbol(d)]
                rev[bus_mapping[d]] +=
                    sum(_dispatch .* _price) + _res_revenue - sum(_var_cost) -
                    sum(_charge .* _price)
                _revenue = max(
                    0,
                    capacity_mapping[d] * capex_cost[cat] - sum(_dispatch .* _price) -
                    sum(_charge .* _price) + _res_revenue - sum(_var_cost),
                )
            else
                _revenue = max(
                    0,
                    capacity_mapping[d] * capex_cost[cat] - sum(_dispatch .* _price) +
                    _res_revenue - sum(_var_cost),
                )
                rev[bus_mapping[d]] +=
                    sum(_dispatch .* _price) + _res_revenue - sum(_var_cost)
            end
            push!(
                capex_market_bids,
                [d, capacity_mapping[d] * 100.0, _revenue, derating_factors[cat]],
            )
        end
        push!(training_data, (cat, sum(values(rev)), capacity_dict[cat]))
        for bus in keys(rev)
            push!(reg_training_data, (cat, bus, rev[bus], reg_capacity_dict[cat, bus]))
        end
    end
    capacity_demand_curve = create_capacity_demand_curve("./capacity_mkt_param.csv", 8191.0)
    cap_price, cap_accepted_bid =
        capacity_market_clearing(capacity_demand_curve, capex_market_bids, optimizer)
    for row in eachrow(training_data)
        _rev = 0.0
        for d in category_mapping[row.Category]
            _rev = _rev + cap_accepted_bid[d] * 100.0 * cap_price
        end
        row.Revenue = row.Revenue + _rev
    end
    return training_data, reg_training_data, cap_price, cap_accepted_bid
end


text:
""" Categories of C-sets and attributed C-sets.
"""
module CSets
export ACSetTransformation, CSetTransformation, SubACSet, SubCSet,
  components, force, is_natural, homomorphism, homomorphisms, is_homomorphic,
  isomorphism, isomorphisms, is_isomorphic,
  generate_json_acset, parse_json_acset, read_json_acset, write_json_acset

using Base.Meta: quot
using AutoHashEquals
using JSON
using Reexport
using StaticArrays: SVector
using Tables

@reexport using ...CSetDataStructures
using ...GAT, ..FreeDiagrams, ..Limits, ..Subobjects, ..Sets, ..FinSets
import ..Limits: limit, colimit, universal, pushout_complement,
  can_pushout_complement
import ..Subobjects: Subobject, SubobjectBiHeytingAlgebra,
  implies, ⟹, subtract, \, negate, ¬, non, ~
import ..FinSets: FinSet, FinFunction, FinDomFunction, force, predicate
using ...Theories: SchemaDescType, CSetSchemaDescType, Category,
  attr, adom, acodom, acodom_nums
import ...Theories: dom, codom, compose, ⋅, id,
  ob, hom, meet, ∧, join, ∨, top, ⊤, bottom, ⊥

# FinSets interop
#################

""" Create `FinSet` for part of attributed C-set.
"""
FinSet(X::StructACSet, type::Symbol) = FinSet{Int,Int}(nparts(X, type))

""" Create `FinFunction` for part or subpart of attributed C-set.

The subpart must be of kind `Ob`. For indexed subparts, the index is included.
"""
FinFunction(X::StructACSet, name::Symbol) = fin_function(X, Val{name})

@generated function fin_function(X::StructACSet{S,Ts,Idxed},
    ::Type{Val{name}}) where {Ts,S,Idxed,name}
  if name ∈ ob(S)
    quote
      FinFunction(identity, FinSet(X, $(QuoteNode(name))))
    end
  elseif name ∈ hom(S)
    quote
      FinFunction(subpart(X, $(QuoteNode(name))),
                  FinSet(X, $(QuoteNode(codom(S, name)))),
                  index=$(Idxed[name] ? :(X.hom_indices.$name) : false))
    end
  else
    throw(ArgumentError("$(repr(name)) not in $(ob(S)) or $(hom(S))"))
  end
end

""" Create `FinDomFunction` for part or subpart of attributed C-set.

The codomain is always of type `TypeSet`, regardless of whether the subpart is
of kind `Ob` or `Data`. For indexed subparts, the index is included.
"""
FinDomFunction(X::StructACSet, name::Symbol) = fin_dom_function(X, Val{name})

@generated function fin_dom_function(X::StructACSet{S,Ts,Idxed},
    ::Type{Val{name}}) where {S,Ts,Idxed,name}
  if name ∈ ob(S)
    quote
      n = nparts(X, $(QuoteNode(name)))
      FinDomFunction(1:n, FinSet(n), TypeSet{Int}())
    end
  elseif name ∈ hom(S) || name ∈ attr(S)
    index_name = name ∈ hom(S) ? :hom_indices : :attr_indices
    quote
      FinDomFunction(subpart(X, $(QuoteNode(name))),
                     index=$(Idxed[name] ? :(X.$index_name.$name) : false))
    end
  else
    throw(ArgumentError(
      "$(repr(name)) not in $(ob(S)), $(hom(S)), or $(attr(S))"))
  end
end

# C-set transformations
#######################

""" Transformation between attributed C-sets.

A homomorphism of C-sets is a natural transformation: a transformation between
functors C → Set satisfying the naturality axiom for all morphisms in C. This
struct records the data of a transformation; it does not enforce naturality, but
see [`is_natural`](@ref).

A C-set transformation has a component for every object in C. When C-sets have
attributes, the data types are assumed to be fixed. Thus, the naturality axiom
for data attributes is a commutative triangle, rather than a commutative square.
"""
@auto_hash_equals struct ACSetTransformation{S, Comp <: NamedTuple,
                                             Dom <: StructACSet{S}, Codom <: StructACSet{S}}
  components::Comp
  dom::Dom
  codom::Codom
  function ACSetTransformation{S}(components::NamedTuple, X::Dom, Y::Codom) where
      {S, Dom <: StructACSet{S}, Codom <: StructACSet{S}}
    @assert keys(components) ⊆ ob(S)
    coerced_components = NamedTuple{Tuple(ob(S))}(
      coerce_component(ob, get(components, ob) do; 1:0 end, X, Y)
      for ob in ob(S))
    new{S,typeof(coerced_components),Dom,Codom}(coerced_components, X, Y)
  end
end

function coerce_component(ob::Symbol, f::FinFunction{Int,Int}, X, Y)
  length(dom(f)) == nparts(X,ob) || error("Domain error in component $ob")
  length(codom(f)) == nparts(Y,ob) || error("Codomain error in component $ob")
  return f
end
function coerce_component(ob::Symbol, f, X, Y)::FinFunction{Int,Int}
  FinFunction(f, nparts(X,ob), nparts(Y,ob))
end

ACSetTransformation(components, X::Dom, Y::Codom) where
    {S, Dom <: StructACSet{S}, Codom <: StructACSet{S}} =
  ACSetTransformation{S}(components, X, Y)
ACSetTransformation(X::Dom, Y::Codom; components...) where
    {S, Dom <: StructACSet{S}, Codom <: StructACSet{S}} =
  ACSetTransformation{S}((; components...), X, Y)

const CSetTransformation{S<:CSetSchemaDescType, Comp,
                         Dom <: StructCSet{S}, Codom <: StructCSet{S}} =
  ACSetTransformation{S,Comp,Dom,Codom}

CSetTransformation(components, X::StructCSet, Y::StructCSet) =
  ACSetTransformation(components, X, Y)
CSetTransformation(X::StructCSet, Y::StructCSet; components...) =
  ACSetTransformation(X, Y; components...)

components(α::ACSetTransformation) = α.components
Base.getindex(α::ACSetTransformation, ob) = α.components[ob]

""" Is the transformation between C-sets a natural transformation?

Uses the fact that to check whether a transformation is natural, it suffices to
check the naturality equation on a generating set of morphisms.
"""
function is_natural(α::ACSetTransformation{S}) where {S}
  X, Y = dom(α), codom(α)
  for (f, c, d) in zip(hom(S), dom(S), codom(S))
    Xf, Yf, α_c, α_d = subpart(X,f), subpart(Y,f), α[c], α[d]
    all(Yf[α_c(i)] == α_d(Xf[i]) for i in eachindex(Xf)) || return false
  end
  for (f, c) in zip(attr(S), adom(S))
    Xf, Yf, α_c = subpart(X,f), subpart(Y,f), α[c]
    all(Yf[α_c(i)] == Xf[i] for i in eachindex(Xf)) || return false
  end
  return true
end

map_components(f, α::ACSetTransformation) =
  ACSetTransformation(map(f, components(α)), dom(α), codom(α))
force(α::ACSetTransformation) = map_components(force, α)

# Finding C-set transformations
###############################

""" Find a homomorphism between two attributed ``C``-sets.

Returns `nothing` if no homomorphism exists. For many categories ``C``, the
``C``-set homomorphism problem is NP-complete and thus this procedure generally
runs in exponential time. It works best when the domain object is small.

This procedure uses the classic backtracking search algorithm for a
combinatorial constraint satisfaction problem (CSP). As is well known, the
homomorphism problem for relational databases is reducible to CSP. Since the
C-set homomorphism problem is "the same" as the database homomorphism problem
(insofar as attributed C-sets are "the same" as relational databases), it is
also reducible to CSP. Backtracking search for CSP is described in many computer
science textbooks, such as (Russell & Norvig 2010, *Artificial Intelligence*,
Third Ed., Chapter 6: Constraint satisfaction problems, esp. Algorithm 6.5). In
our implementation, the search tree is ordered using the popular heuristic of
"minimum remaining values" (MRV), also known as "most constrained variable."

To restrict to *monomorphisms*, or homomorphisms whose components are all
injective functions, set the keyword argument `monic=true`. To restrict only
certain components to be injective or bijective, use `monic=[...]` or
`iso=[...]`. For example, setting `monic=[:V]` for a graph homomorphism ensures
that the vertex map is injective but imposes no constraints on the edge map.

To restrict the homomorphism to a given partial assignment, set the keyword
argument `initial`. For example, to fix the first source vertex to the third
target vertex in a graph homomorphism, set `initial=(V=Dict(1 => 3),)`.

See also: [`homomorphisms`](@ref), [`isomorphism`](@ref).
"""
function homomorphism(X::StructACSet, Y::StructACSet; kw...)
  result = nothing
  homomorphisms(X, Y; kw...) do α
    result = α; return true
  end
  result
end

""" Find all homomorphisms between two attributed ``C``-sets.

This function is at least as expensive as [`homomorphism`](@ref) and when no
homomorphisms exist, it is exactly as expensive.
"""
function homomorphisms(X::StructACSet{S}, Y::StructACSet{S};
                       kw...) where {S}
  results = ACSetTransformation{S}[]
  homomorphisms(X, Y; kw...) do α
    push!(results, map_components(deepcopy, α)); return false
  end
  results
end
homomorphisms(f, X::StructACSet, Y::StructACSet;
              monic=false, iso=false, initial=(;)) =
  backtracking_search(f, X, Y, monic=monic, iso=iso, initial=initial)

""" Is the first attributed ``C``-set homomorphic to the second?

A convenience function based on [`homomorphism`](@ref).
"""
is_homomorphic(X::StructACSet, Y::StructACSet; kw...) =
  !isnothing(homomorphism(X, Y; kw...))

""" Find an isomorphism between two attributed ``C``-sets, if one exists.

See [`homomorphism`](@ref) for more information about the algorithms involved.
"""
isomorphism(X::StructACSet, Y::StructACSet; initial=(;)) =
  homomorphism(X, Y, iso=true, initial=initial)

""" Find all isomorphisms between two attributed ``C``-sets.

This function is at least as expensive as [`isomorphism`](@ref) and when no
homomorphisms exist, it is exactly as expensive.
"""
isomorphisms(X::StructACSet, Y::StructACSet; initial=(;)) =
  homomorphisms(X, Y, iso=true, initial=initial)
isomorphisms(f, X::StructACSet, Y::StructACSet; initial=(;)) =
  homomorphisms(f, X, Y, iso=true, initial=initial)

""" Are the two attributed ``C``-sets isomorphic?

A convenience function based on [`isomorphism`](@ref).
"""
is_isomorphic(X::StructACSet, Y::StructACSet; kw...) =
  !isnothing(isomorphism(X, Y; kw...))

""" Internal state for backtracking search for ACSet homomorphisms.
"""
struct BacktrackingState{S <: SchemaDescType,
    Assign <: NamedTuple, PartialAssign <: NamedTuple,
    Dom <: StructACSet{S}, Codom <: StructACSet{S}}
  """ The current assignment, a partially-defined homomorphism of ACSets. """
  assignment::Assign
  """ Depth in search tree at which assignments were made. """
  assignment_depth::Assign
  """ Inverse assignment for monic components or if finding a monomorphism. """
  inv_assignment::PartialAssign
  """ Domain ACSet: the "variables" in the CSP. """
  dom::Dom
  """ Codomain ACSet: the "values" in the CSP. """
  codom::Codom
end

function backtracking_search(f, X::StructACSet{S}, Y::StructACSet{S};
                             monic=false, iso=false, initial=(;)) where {Ob, S<:SchemaDescType{Ob}}
  # Fail early if no monic/isos exist on cardinality grounds.
  if iso isa Bool
    iso = iso ? Ob : ()
  end
  for c in iso
    nparts(X,c) == nparts(Y,c) || return false
  end
  if monic isa Bool
    monic = monic ? Ob : ()
  end
  # Injections between finite sets are bijections, so reduce to that case.
  monic = unique([iso..., monic...])
  for c in monic
    nparts(X,c) <= nparts(Y,c) || return false
  end

  # Initialize state variables for search.
  assignment = NamedTuple{Ob}(zeros(Int, nparts(X, c)) for c in Ob)
  assignment_depth = map(copy, assignment)
  inv_assignment = NamedTuple{Ob}(
    (c in monic ? zeros(Int, nparts(Y, c)) : nothing) for c in Ob)
  state = BacktrackingState(assignment, assignment_depth, inv_assignment, X, Y)

  # Make any initial assignments, failing immediately if inconsistent.
  for (c, c_assignments) in pairs(initial)
    for (x, y) in partial_assignments(c_assignments)
      assign_elem!(state, 0, Val{c}, x, y) || return false
    end
  end

  # Start the main recursion for backtracking search.
  backtracking_search(f, state, 1)
end

function backtracking_search(f, state::BacktrackingState, depth::Int)
  # Choose the next unassigned element.
  mrv, mrv_elem = find_mrv_elem(state, depth)
  if isnothing(mrv_elem)
    # No unassigned elements remain, so we have a complete assignment.
    return f(ACSetTransformation(state.assignment, state.dom, state.codom))
  elseif mrv == 0
    # An element has no allowable assignment, so we must backtrack.
    return false
  end
  c, x = mrv_elem

  # Attempt all assignments of the chosen element.
  Y = state.codom
  for y in parts(Y, c)
    assign_elem!(state, depth, Val{c}, x, y) &&
      backtracking_search(f, state, depth + 1) &&
      return true
    unassign_elem!(state, depth, Val{c}, x)
  end
  return false
end

""" Find an unassigned element having the minimum remaining values (MRV).
"""
function find_mrv_elem(state::BacktrackingState{S}, depth) where S
  mrv, mrv_elem = Inf, nothing
  Y = state.codom
  for c in ob(S), (x, y) in enumerate(state.assignment[c])
    y == 0 || continue
    n = count(can_assign_elem(state, depth, Val{c}, x, y) for y in parts(Y, c))
    if n < mrv
      mrv, mrv_elem = n, (c, x)
    end
  end
  (mrv, mrv_elem)
end

""" Check whether element (c,x) can be assigned to (c,y) in current assignment.
"""
function can_assign_elem(state::BacktrackingState, depth,
                         ::Type{Val{c}}, x, y) where c
  # Although this method is nonmutating overall, we must temporarily mutate the
  # backtracking state, for several reasons. First, an assignment can be a
  # consistent at each individual subpart but not consistent for all subparts
  # simultaneously (consider trying to assign a self-loop to an edge with
  # distinct vertices). Moreover, in schemas with non-trivial endomorphisms, we
  # must keep track of which elements we have visited to avoid looping forever.
  ok = assign_elem!(state, depth, Val{c}, x, y)
  unassign_elem!(state, depth, Val{c}, x)
  return ok
end

""" Attempt to assign element (c,x) to (c,y) in the current assignment.

Returns whether the assignment succeeded. Note that the backtracking state can
be mutated even when the assignment fails.
"""
@generated function assign_elem!(state::BacktrackingState{S}, depth,
                                 ::Type{Val{c}}, x, y) where {S, c}
  quote
    y′ = state.assignment.$c[x]
    y′ == y && return true  # If x is already assigned to y, return immediately.
    y′ == 0 || return false # Otherwise, x must be unassigned.
    if !isnothing(state.inv_assignment.$c) && state.inv_assignment.$c[y] != 0
      # Also, y must unassigned in the inverse assignment.
      return false
    end

    # Check attributes first to fail as quickly as possible.
    X, Y = state.dom, state.codom
    $(map(out_attr(S, c)) do f
        :(subpart(X,x,$(quot(f))) == subpart(Y,y,$(quot(f))) || return false)
      end...)

    # Make the assignment and recursively assign subparts.
    state.assignment.$c[x] = y
    state.assignment_depth.$c[x] = depth
    if !isnothing(state.inv_assignment.$c)
      state.inv_assignment.$c[y] = x
    end
    $(map(out_hom(S, c)) do (f, d)
        :(assign_elem!(state, depth, Val{$(quot(d))}, subpart(X,x,$(quot(f))),
                       subpart(Y,y,$(quot(f)))) || return false)
      end...)
    return true
  end
end

""" Unassign the element (c,x) in the current assignment.
"""
@generated function unassign_elem!(state::BacktrackingState{S}, depth,
                                   ::Type{Val{c}}, x) where {S, c}
  quote
    state.assignment.$c[x] == 0 && return
    assign_depth = state.assignment_depth.$c[x]
    @assert assign_depth <= depth
    if assign_depth == depth
      X = state.dom
      if !isnothing(state.inv_assignment.$c)
        y = state.assignment.$c[x]
        state.inv_assignment.$c[y] = 0
      end
      state.assignment.$c[x] = 0
      state.assignment_depth.$c[x] = 0
      $(map(out_hom(S, c)) do (f, d)
          :(unassign_elem!(state, depth, Val{$(quot(d))},
                           subpart(X,x,$(quot(f)))))
        end...)
    end
  end
end

""" Get assignment pairs from partially specified component of C-set morphism.
"""
partial_assignments(x::AbstractDict) = pairs(x)
partial_assignments(x::AbstractVector) =
  ((i,y) for (i,y) in enumerate(x) if !isnothing(y) && y > 0)

# FIXME: Should these accessors go elsewhere?
in_hom(S, c) = [dom(S,f) => f for f in hom(S) if codom(S,f) == c]
out_hom(S, c) = [f => codom(S,f) for f in hom(S) if dom(S,f) == c]
out_attr(S, c) = [f for f in attr(S) if dom(S, f) == c]

# Category of C-sets
####################

@instance Category{StructACSet, ACSetTransformation} begin
  dom(α::ACSetTransformation) = α.dom
  codom(α::ACSetTransformation) = α.codom

  id(X::StructACSet) = ACSetTransformation(map(id, fin_sets(X)), X, X)

  function compose(α::ACSetTransformation, β::ACSetTransformation)
    # Question: Should we incur cost of checking that codom(β) == dom(α)?
    ACSetTransformation(map(compose, components(α), components(β)),
                        dom(α), codom(β))
  end
end

fin_sets(X::StructACSet{S}) where {S} = NamedTuple(A => FinSet(nparts(X,A)) for A in ob(S))

@cartesian_monoidal_instance ACSet ACSetTransformation
@cocartesian_monoidal_instance ACSet ACSetTransformation

# Limits and colimits
#####################

""" Limit of C-sets that stores the pointwise limits in FinSet.
"""
struct CSetLimit{Ob <: StructCSet, Diagram, Cone <: Multispan{Ob},
                 Limits <: NamedTuple} <: AbstractLimit{Ob,Diagram}
  diagram::Diagram
  cone::Cone
  limits::Limits
end

""" Colimit of attributed C-sets that stores the pointwise colimits in FinSet.
"""
struct ACSetColimit{Ob <: StructACSet, Diagram, Cocone <: Multicospan{Ob},
                    Colimits <: NamedTuple} <: AbstractColimit{Ob,Diagram}
  diagram::Diagram
  cocone::Cocone
  colimits::Colimits
end

# Compute limits and colimits of C-sets by reducing to those in FinSet using the
# "pointwise" formula for (co)limits in functor categories.

function limit(diagram::AbstractFreeDiagram{ACS}) where
    {S <: CSetSchemaDescType, ACS <: StructCSet{S}}
  limits = map(limit, unpack_diagram(diagram))
  Xs = cone_objects(diagram)
  Y = ACS()
  for (c, lim) in pairs(limits)
    add_parts!(Y, c, length(ob(lim)))
  end
  for (f, c, d) in zip(hom(S), dom(S), codom(S))
    Yfs = map(legs(limits[c]), Xs) do π, X
      compose(π, FinFunction(subpart(X, f), nparts(X, d)))
    end
    Yf = universal(limits[d], Multispan(ob(limits[c]), Yfs))
    set_subpart!(Y, f, collect(Yf))
  end
  πs = pack_components(map(legs, limits), map(X -> Y, Xs), Xs)
  CSetLimit(diagram, Multispan(Y, πs), limits)
end

function universal(lim::CSetLimit, cone::Multispan)
  components = map(universal, lim.limits, unpack_diagram(cone))
  CSetTransformation(components, apex(cone), ob(lim))
end

function colimit(diagram::AbstractFreeDiagram{ACS}) where
    {S, Ts, ACS <: StructACSet{S,Ts}}
  # Colimit of C-set without attributes.
  colimits = map(colimit, unpack_diagram(diagram))
  Xs = cocone_objects(diagram)
  Y = ACS()
  for (c, colim) in pairs(colimits)
    add_parts!(Y, c, length(ob(colim)))
  end
  for (f, c, d) in zip(hom(S), dom(S), codom(S))
    Yfs = map(legs(colimits[d]), Xs) do ι, X
      compose(FinFunction(subpart(X, f), nparts(X, d)), ι)
    end
    Yf = universal(colimits[c], Multicospan(ob(colimits[d]), Yfs))
    set_subpart!(Y, f, collect(Yf))
  end
  ιs = pack_components(map(legs, colimits), Xs, map(X -> Y, Xs))

  # Set data attributes by canonical inclusion from attributes in diagram.
  for (attr, c, d) in zip(attr(S), adom(S), acodom_nums(S))
    T = Ts.parameters[d]
    data = Vector{Union{Some{T},Nothing}}(nothing, nparts(Y, c))
    for (ι, X) in zip(ιs, Xs)
      for i in parts(X, c)
        j = ι[c](i)
        if isnothing(data[j])
          data[j] = Some(subpart(X, i, attr))
        else
          val1, val2 = subpart(X, i, attr), something(data[j])
          val1 == val2 || error(
            "ACSet colimit does not exist: $attr attributes $val1 != $val2")
        end
      end
    end
    set_subpart!(Y, attr, map(something, data))
  end

  ACSetColimit(diagram, Multicospan(Y, ιs), colimits)
end

function universal(colim::ACSetColimit, cocone::Multicospan)
  components = map(universal, colim.colimits, unpack_diagram(cocone))
  ACSetTransformation(components, ob(colim), apex(cocone))
end

""" Diagram in C-Set → named tuple of diagrams in FinSet
"""
unpack_diagram(discrete::DiscreteDiagram{<:ACSet}) =
  map(DiscreteDiagram, unpack_sets(ob(discrete)))
unpack_diagram(span::Multispan{<:ACSet}) =
  map(Multispan, fin_sets(apex(span)), unpack_components(legs(span)))
unpack_diagram(cospan::Multicospan{<:ACSet}) =
  map(Multicospan, fin_sets(apex(cospan)), unpack_components(legs(cospan)))
unpack_diagram(para::ParallelMorphisms{<:ACSet}) =
  map(ParallelMorphisms, unpack_components(hom(para)))
unpack_diagram(comp::ComposableMorphisms{<:ACSet}) =
  map(ComposableMorphisms, unpack_components(hom(comp)))

function unpack_diagram(d::Union{FreeDiagram{ACS},BipartiteFreeDiagram{ACS}}) where
    {Ob, S <: SchemaDescType{Ob}, ACS <: StructACSet{S}}
  NamedTuple{Ob}([ map(d, Ob=X -> FinSet(X, ob), Hom=α -> α[ob]) for ob in Ob ])
end

""" Vector of C-sets → named tuple of vectors of FinSets
"""
unpack_sets(Xs::AbstractVector{<:StructACSet{S}}) where
    {Ob, S <: SchemaDescType{Ob}} =
  NamedTuple{Ob}([ map(X -> FinSet(X, ob), Xs) for ob in Ob ])

""" Vector of C-set transformations → named tuple of vectors of FinFunctions
"""
unpack_components(αs::AbstractVector{<:ACSetTransformation{S}}) where
    {Ob, S <: SchemaDescType{Ob}} =
  NamedTuple{Ob}([ map(α -> α[ob], αs) for ob in Ob ])

""" Named tuple of vectors of FinFunctions → vector of C-set transformations
"""
function pack_components(fs::NamedTuple{Ob}, doms, codoms) where Ob
  components = map((x...) -> NamedTuple{Ob}(x), fs...) # XXX: Is there a better way?
  map(ACSetTransformation, components, doms, codoms)
end

""" Objects in diagram that will have explicit legs in limit cone.

Encodes common conventions such as, when taking a pullback of a cospan, not
explicitly including a cone leg for the cospan apex since it can be computed
from the other legs.

FIXME: Should this function be part of the official limits interface?
"""
cone_objects(diagram) = ob(diagram)
cone_objects(diagram::BipartiteFreeDiagram) = ob₁(diagram)
cone_objects(cospan::Multicospan) = feet(cospan)
cone_objects(para::ParallelMorphisms) = SVector(dom(para))

""" Objects in diagram that will have explicit legs in colimit cocone.
"""
cocone_objects(diagram) = ob(diagram)
cocone_objects(diagram::BipartiteFreeDiagram) = ob₂(diagram)
cocone_objects(span::Multispan) = feet(span)
cocone_objects(para::ParallelMorphisms) = SVector(codom(para))

""" Compute pushout complement of attributed C-sets, if possible.

The pushout complement is constructed pointwise from pushout complements of
finite sets. If any of the pointwise identification conditions fail (in FinSet),
this method will raise an error. If the dangling condition fails, the resulting
C-set will be only partially defined. To check all these conditions in advance,
use the function [`can_pushout_complement`](@ref).
"""
function pushout_complement(pair::ComposablePair{<:ACSet})
  # Compute pushout complements pointwise in FinSet.
  components = map(pushout_complement, unpack_diagram(pair))
  k_components, g_components = map(first, components), map(last, components)

  # Reassemble components into natural transformations.
  g = hom(Subobject(codom(pair), g_components))
  k = ACSetTransformation(k_components, dom(pair), dom(g))
  return ComposablePair(k, g)
end

function can_pushout_complement(pair::ComposablePair{<:ACSet})
  all(can_pushout_complement, unpack_diagram(pair)) &&
    isempty(dangling_condition(pair))
end

"""
Check the dangling condition for a pushout comlement: m doesn't map a deleted
element d to an element m(d) ∈ G if m(d) is connected to something outside the
image of m.

For example, in the C-Set of graphs,

   e1
v1 --> v2

if e1 is not matched but either v1 or v2 are deleted, then e1 is dangling.
"""
function dangling_condition(pair::ComposablePair{<:StructACSet{S}}) where S
  l, m = pair
  orphans = map(components(l), components(m)) do l_comp, m_comp
    image = Set(collect(l_comp))
    Set([ m_comp(x) for x in codom(l_comp) if x ∉ image ])
  end
  # check that for all morphisms in C, we do not map to an orphan
  results = Tuple{Symbol,Int,Int}[]
  for (morph, src_obj, tgt_obj) in zip(hom(S), dom(S), codom(S))
    n_src = parts(codom(m), src_obj)
    unmatched_vals = setdiff(n_src, collect(m[src_obj]))
    unmatched_tgt = map(x -> codom(m)[x,morph], collect(unmatched_vals))
    for unmatched_val in setdiff(n_src, collect(m[src_obj]))  # G/m(L) src
      unmatched_tgt = codom(m)[unmatched_val,morph]
      if unmatched_tgt in orphans[tgt_obj]
        push!(results, (morph, unmatched_val, unmatched_tgt))
      end
    end
  end
  results
end

# Sub-C-sets
############

""" Sub-C-set of a C-set.
"""
const SubCSet{S} = Subobject{<:StructCSet{S}}
const SubACSet{S} = Subobject{<:StructACSet{S}}

components(A::SubACSet) = map(Subobject, components(hom(A)))
force(A::SubACSet) = Subobject(force(hom(A)))

""" Sub-C-set represented componentwise as a collection of subsets.
"""
@auto_hash_equals struct SubACSetComponentwise{
    Ob<:ACSet, Comp<:NamedTuple} <: Subobject{Ob}
  ob::Ob
  components::Comp

  function SubACSetComponentwise(X::Ob, components::NamedTuple) where Ob<:ACSet
    sets = fin_sets(X)
    @assert keys(components) ⊆ keys(sets)
    coerced_components = NamedTuple{keys(sets)}(
      coerce_subob_component(set, get(components, ob) do; 1:0 end)
      for (ob, set) in pairs(sets))
    new{Ob,typeof(coerced_components)}(X, coerced_components)
  end
end

Subobject(X::ACSet, components::NamedTuple) =
  SubACSetComponentwise(X, components)
Subobject(X::ACSet; components...) = Subobject(X, (; components...))

function coerce_subob_component(X::FinSet, subset::SubFinSet)
  X == ob(subset) ? subset :
    error("Set $X in C-set does not match set of subset $subset")
end
function coerce_subob_component(X::FinSet, f::FinFunction)
  X == codom(f) ? Subobject(f) :
    error("Set $X in C-set does not match codomain of inclusion $f")
end
coerce_subob_component(X::FinSet, f) = Subobject(X, f)

ob(A::SubACSetComponentwise) = A.ob
components(A::SubACSetComponentwise) = A.components

function hom(A::SubACSetComponentwise{T}) where T <: ACSet
  U, X = T(), ob(A)
  hom_components = map(collect∘hom, components(A))
  copy_parts!(U, X, hom_components)
  ACSetTransformation(hom_components, U, X)
end

@instance SubobjectBiHeytingAlgebra{ACSet,SubACSet} begin
  @import ob
  meet(A::SubACSet, B::SubACSet) = meet(A, B, SubOpBoolean())
  join(A::SubACSet, B::SubACSet) = join(A, B, SubOpBoolean())
  top(X::ACSet) = top(X, SubOpWithLimits())
  bottom(X::ACSet) = bottom(X, SubOpWithLimits())

  implies(A::SubACSet, B::SubACSet) = implies(A, B, SubOpBoolean())
  subtract(A::SubACSet, B::SubACSet) = subtract(A, B, SubOpBoolean())
  negate(A::SubACSet) = implies(A, bottom(ob(A)), SubOpBoolean())
  non(A::SubACSet) = subtract(top(ob(A)), A, SubOpBoolean())
end

function meet(A::SubACSet, B::SubACSet, ::SubOpBoolean)
  Subobject(common_ob(A, B), map(components(A), components(B)) do A₀, B₀
    meet(A₀, B₀, SubOpBoolean())
  end)
end
function join(A::SubACSet, B::SubACSet, ::SubOpBoolean)
  Subobject(common_ob(A, B), map(components(A), components(B)) do A₀, B₀
    join(A₀, B₀, SubOpBoolean())
  end)
end
top(X::ACSet, ::SubOpBoolean) =
  Subobject(X, map(X₀ -> top(X₀, SubOpBoolean()), fin_sets(X)))
bottom(X::ACSet, ::SubOpBoolean) =
  Subobject(X, map(X₀ -> bottom(X₀, SubOpBoolean()), fin_sets(X)))

""" Implication of sub-C-sets.

By (Reyes et al 2004, Proposition 9.1.5), the implication ``A ⟹ B`` for two
sub-``C``-sets ``A,B ↪ X`` is given by

``x ∈ (A ⟹ B)(c) iff ∀f: c → c′, x⋅f ∈ A(c′) ⟹ x⋅f ∈ B(c′)``

for all ``c ∈ C`` and ``x ∈ X(c)``. By the definition of implication and De
Morgan's law in classical logic, this is equivalent to

``x ∉ (A ⟹ B)(c) iff ∃f: c → c′, x⋅f ∈ A(c′) ∧ x⋅f ∉ B(c′)``.

In this form, we can clearly see the duality to formula and algorithm for
subtraction of sub-C-sets ([`subtract`](@ref)).
"""
function implies(A::SubACSet{S}, B::SubACSet{S}, ::SubOpBoolean) where S
  X = common_ob(A, B)
  A, B = map(predicate, components(A)), map(predicate, components(B))
  D = map(X₀ -> trues(length(X₀)), fin_sets(X))

  function unset!(c, x)
    D[c][x] = false
    for (c′,x′) in all_incident(X, Val{c}, x)
      if D[c′][x′]; unset!(c′,x′) end
    end
  end

  for c in ob(S), x in parts(X,c)
    if D[c][x] && A[c][x] && !B[c][x]; unset!(c,x) end
  end
  Subobject(X, D)
end

""" Subtraction of sub-C-sets.

By (Reyes et al 2004, Sec 9.1, pp. 144), the subtraction ``A ∖ B`` for two
sub-``C``-sets ``A,B ↪ X`` is given by

``x ∈ (A ∖ B)(c) iff ∃f: c′ → c, ∃x′ ∈ f⁻¹⋅x, x′ ∈ A(c′) ∧ x′ ∉ B(c′)``

for all ``c ∈ C`` and ``x ∈ X(c)``. Compare with [`implies`](@ref).
"""
function subtract(A::SubACSet{S}, B::SubACSet{S}, ::SubOpBoolean) where S
  X = common_ob(A, B)
  A, B = map(predicate, components(A)), map(predicate, components(B))
  D = map(X₀ -> falses(length(X₀)), fin_sets(X))

  function set!(c, x)
    D[c][x] = true
    for (c′,x′) in all_subparts(X, Val{c}, x)
      if !D[c′][x′]; set!(c′,x′) end
    end
  end

  for c in ob(S), x in parts(X,c)
    if !D[c][x] && A[c][x] && !B[c][x]; set!(c,x) end
  end
  Subobject(X, D)
end

function common_ob(A::Subobject, B::Subobject)
  (X = ob(A)) == ob(B) ||
    error("Subobjects have different base objects: $(ob(A)) != $(ob(B))")
  return X
end

# FIXME: Should these two accessors go elsewhere?

@generated function all_subparts(X::StructACSet{S},
                                 ::Type{Val{c}}, x::Int) where {S,c}
  Expr(:tuple, map(out_hom(S, c)) do (f,c′)
    :($(quot(c′)), subpart(X,x,$(quot(f))))
  end...)
end

@generated function all_incident(X::StructACSet{S},
                                 ::Type{Val{c}}, x::Int) where {S,c}
  Expr(:call, GlobalRef(Iterators, :flatten),
    Expr(:tuple, map(in_hom(S, c)) do (c′,f)
      :(Tuple{Symbol,Int}[ ($(quot(c′)),x′) for x′ in incident(X,x,$(quot(f))) ])
    end...))
end

# Serialization
###############

""" Serialize an ACSet object to a JSON string
"""
function generate_json_acset(x::T) where T <: ACSet
  ts = tables(x)
  JSON.json(Dict(k => Tables.rowtable(v) for (k,v) in zip(keys(ts),ts)))
end

""" Deserialize a dictionary from a parsed JSON string to an object of the given ACSet type
"""
function parse_json_acset(type::Type{T}, input::Dict) where T <: ACSet
  out = type()
  for (k,v) ∈ input
    add_parts!(out, Symbol(k), length(v))
  end
  for l ∈ values(input)
    for (i, j) ∈ enumerate(l)
      for (k,v) ∈ j
        vtype = eltype(out[Symbol(k)])
        if !(typeof(v) <: vtype)
          v = vtype(v)
        end
        set_subpart!(out, i, Symbol(k), v)
      end
    end
  end
  out
end

""" Deserialize a JSON string to an object of the given ACSet type
"""
function parse_json_acset(type::Type{T}, input::String) where T <: ACSet
  parse_json_acset(type, JSON.parse(input))
end

""" Read a JSON file to an object of the given ACSet type
"""
function read_json_acset(type::Type{T}, input::String) where T <: ACSet
  parse_json_acset(type, open(f->read(f, String), input))
end

""" Serialize an ACSet object to a JSON file
"""
function write_json_acset(x::T, fname::AbstractString) where T <: ACSet
  open(string(fname), "w") do f
    write(f, generate_json_acset(x))
  end
end

end

ext: jl


text:
module TestPkg

using FilePathsBase

import Base: ==

__init__() = FilePathsBase.register(TestPath)

# Warning: We only expect this test to work on posix systems.

struct TestPath <: AbstractPath
    segments::Tuple{Vararg{String}}
    root::String
    drive::String
    separator::String
end

TestPath() = TestPath(tuple(), "", "test:", ";")

function TestPath(str::AbstractString)
    str = String(str)

    @assert startswith(str, "test:")
    drive = "test:"
    root = ""
    str = str[6:end]

    if isempty(str)
        return TestPath(tuple("."), "", drive)
    end

    tokenized = split(str, ";")
    if isempty(tokenized[1])
        root = ";"
    end

    return TestPath(tuple(map(String, filter!(!isempty, tokenized))...), root, drive, ";")
end

FilePathsBase.ispathtype(::Type{TestPath}, str::AbstractString) = startswith(str, "test:")
function test2posix(fp::TestPath)
    return PosixPath(fp.segments, isempty(fp.root) ? "" : "/")
end

function posix2test(fp::PosixPath)
    return TestPath(fp.segments, isempty(fp.root) ? "" : ";", "test:", ";")
end

# We're going to implement most of the posix API, but this won't make sense for many path types
FilePathsBase.exists(fp::TestPath) = exists(test2posix(fp))
Base.real(fp::TestPath) = posix2test(real(test2posix(fp)))
FilePathsBase.stat(fp::TestPath) = stat(test2posix(fp))
FilePathsBase.lstat(fp::TestPath) = lstat(test2posix(fp))
FilePathsBase.mode(fp::TestPath) = stat(fp).mode
Base.size(fp::TestPath) = stat(fp).size
FilePathsBase.created(fp::TestPath) = stat(fp).ctime
FilePathsBase.modified(fp::TestPath) = stat(fp).mtime
FilePathsBase.isdir(fp::TestPath) = isdir(mode(fp))
Base.isfile(fp::TestPath) = isfile(mode(fp))
Base.islink(fp::TestPath) = islink(lstat(fp).mode)
Base.issocket(fp::TestPath) = issocket(mode(fp))
Base.isfifo(fp::TestPath) = issocket(mode(fp))
Base.ischardev(fp::TestPath) = ischardev(mode(fp))
Base.isblockdev(fp::TestPath) = isblockdev(mode(fp))
Base.ismount(fp::TestPath) = ismount(test2posix(fp))
FilePathsBase.isexecutable(fp::TestPath) = isexecutable(test2posix(fp))
Base.iswritable(fp::TestPath) = iswritable(test2posix(fp))
Base.isreadable(fp::TestPath) = isreadable(test2posix(fp))
Base.cd(fp::TestPath) = cd(test2posix(fp))
Base.cd(f::Function, fp::TestPath) = cd(f, test2posix(fp))
Base.mkdir(fp::TestPath; kwargs...) = mkdir(test2posix(fp); kwargs...)
Base.symlink(src::TestPath, dest::TestPath; kwargs...) = symlink(test2posix(src), test2posix(dest); kwargs...)
Base.rm(fp::TestPath; kwargs...) = rm(test2posix(fp); kwargs...)
Base.readdir(fp::TestPath) = readdir(test2posix(fp))
Base.read(fp::TestPath) = read(test2posix(fp))
Base.write(fp::TestPath, x) = write(test2posix(fp), x)
Base.chown(fp::TestPath, args...; kwargs...) = chown(test2posix(fp), args...; kwargs...)
Base.chmod(fp::TestPath, args...; kwargs...) = chmod(test2posix(fp), args...; kwargs...)

end


text:
using AdversarialDriving
using AutomotiveSimulator, AutomotiveVisualization
using POMDPs, POMDPPolicies, POMDPSimulators

blindspot = Blindspot(π/12 + 0.07, π/6)
sut_agent = BlinkerVehicleAgent(get_ped_vehicle(id=1, s=5., v=15.), TIDM(ped_TIDM_template, noisy_observations = true, blindspot = blindspot))
adv_ped = NoisyPedestrianAgent(get_pedestrian(id=2, s=10.7, v=0.), AdversarialPedestrian(idm=IntelligentDriverModel(v_des = 0)))
mdp = AdversarialDrivingMDP(sut_agent, [adv_ped], ped_roadway, 0.1)

# Sample blindspot behavior
hist = POMDPSimulators.simulate(HistoryRecorder(max_steps = 25), mdp, FunctionPolicy((s) -> Disturbance[PedestrianControl()]))

# Make the renderable object to visualize the blindspot
rb = (s) -> RenderableBlindspot(posg(get_by_id(s, sutid(mdp))), sut(mdp).model.blindspot, 30, colorant"blue")
scenes_to_gif(state_hist(hist), mdp.roadway, "blindspot.gif", others = [crosswalk], others_fn = [rb])


text:
#!/usr/bin/env julia
"""
Script for generating default.prm from params.json
"""

using JSON

"""
   val2str(val::Any)

Convert a Julia value to a string containing a Matlab eval-able value.
"""
function val2str(val::Any)
   if isa(val, Array{Any})
      emptystrings = isa.(val, String) .& isempty.(val)
      val[emptystrings] .= "''"
      if any(isa.(val, String)) # swap out an array for a cell array
         val = replace(replace(string(val), "Any[" => "{"), "]" => "}")
      else
         val = replace(string(val), "Any" => "")
      end
      val = replace(val, "\"''\"" => "''") # swap out "''" for ''
   elseif isa(val, String)
      val = "'$val'"
   end

   val
end

sectionorder = ["usage", "execution", "probe", "recording file",
                "preprocessing", "spike detection",
                "feature extraction", "clustering", "curation",
                "display", "trial", "validation", "preview",
                "traces", "lfp", "aux channel"]

basedir = normpath(joinpath(dirname(@__FILE__), ".."))
paramfile = joinpath(basedir, "json", "params.json")
params = JSON.parse(read(paramfile, String))
commonbysection = Dict([section => [param for param in params["common parameters"]
                        if param[2]["section"][1] == section] for section in sectionorder])
new2old = Dict([p[2] => p[1] for p in params["old2new"]])

open(joinpath(basedir, "default.prm"), "w") do io
   println(io, "% JRCLUST parameters (default parameter set)")
   println(io, "% For a description of these parameters, including default and legal values, see https://jrclust.readthedocs.io/en/latest/parameters/index.html\n")

   for section in sectionorder
      sectionparams = Dict(commonbysection[section])
      if isempty(sectionparams)
         continue
      end

      println(io, uppercase("% $section parameters"))
      for paramname in sort(String.(keys(sectionparams)))
         paramdesc = sectionparams[paramname]

         # paramname = param[1]
         # paramdesc = param[2]
         defaultval = val2str(paramdesc["default_value"])
         description = paramdesc["description"]
         comment = paramdesc["comment"]

         print(io, "$paramname = $defaultval; % ")
         if haskey(new2old, paramname)
            print(io, "(formerly $(new2old[paramname])) ")
         end

         print(io, "$description")

         if !isempty(comment)
            println(io, " ($comment)")
         else
            println(io, "")
         end
      end

      println(io, "")
   end
end

max_stars_repo_name: eyalbrand/NET

max_issues_repo_name: eyalbrand/NET

max_forks_repo_name: eyalbrand/NET


text:
@testset "6.3.1 (c+d x)^m (a+b tanh)^n" begin
    (a, b, c, d, e, f, m, x, ) = @variables a b c d e f m x

    #= ::Package:: =#

    #= ::Title:: =#
    #=Integrands*of*the*form*(c+d*x)^m*(a+b*tanh(e+f*x))^n=#

    #= ::Section::Closed:: =#
    #=Integrands*of*the*form*(c+d*x)^m*(b*tanh(e+f*x))^n=#

    #= ::Subsection::Closed:: =#
    #=Integrands*of*the*form*(c+d*x)^m*tanh(e+f*x)^n=#

    @test_int [(c + d*x)^3*tanh(e + f*x), x, 6, -((c + d*x)^4/(4*d)) + ((c + d*x)^3*log(1 + ℯ^(2*(e + f*x))))/f + (3*d*(c + d*x)^2*PolyLog(2, -ℯ^(2*(e + f*x))))/(2*f^2) - (3*d^2*(c + d*x)*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^3) + (3*d^3*PolyLog(4, -ℯ^(2*(e + f*x))))/(4*f^4)]
    @test_int [(c + d*x)^2*tanh(e + f*x), x, 5, -((c + d*x)^3/(3*d)) + ((c + d*x)^2*log(1 + ℯ^(2*(e + f*x))))/f + (d*(c + d*x)*PolyLog(2, -ℯ^(2*(e + f*x))))/f^2 - (d^2*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^3)]
    @test_int [(c + d*x)^1*tanh(e + f*x), x, 4, -((c + d*x)^2/(2*d)) + ((c + d*x)*log(1 + ℯ^(2*(e + f*x))))/f + (d*PolyLog(2, -ℯ^(2*(e + f*x))))/(2*f^2)]
    @test_int [tanh(e + f*x)/(c + d*x)^1, x, 0, Unintegrable(tanh(e + f*x)/(c + d*x), x)]
    @test_int [tanh(e + f*x)/(c + d*x)^2, x, 0, Unintegrable(tanh(e + f*x)/(c + d*x)^2, x)]

    @test_int [(c + d*x)^3*tanh(e + f*x)^2, x, 7, -((c + d*x)^3/f) + (c + d*x)^4/(4*d) + (3*d*(c + d*x)^2*log(1 + ℯ^(2*(e + f*x))))/f^2 + (3*d^2*(c + d*x)*PolyLog(2, -ℯ^(2*(e + f*x))))/f^3 - (3*d^3*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^4) - ((c + d*x)^3*tanh(e + f*x))/f]
    @test_int [(c + d*x)^2*tanh(e + f*x)^2, x, 6, -((c + d*x)^2/f) + (c + d*x)^3/(3*d) + (2*d*(c + d*x)*log(1 + ℯ^(2*(e + f*x))))/f^2 + (d^2*PolyLog(2, -ℯ^(2*(e + f*x))))/f^3 - ((c + d*x)^2*tanh(e + f*x))/f]
    @test_int [(c + d*x)^1*tanh(e + f*x)^2, x, 3, c*x + (d*x^2)/2 + (d*log(cosh(e + f*x)))/f^2 - ((c + d*x)*tanh(e + f*x))/f]
    @test_int [tanh(e + f*x)^2/(c + d*x)^1, x, 0, Unintegrable(tanh(e + f*x)^2/(c + d*x), x)]
    @test_int [tanh(e + f*x)^2/(c + d*x)^2, x, 0, Unintegrable(tanh(e + f*x)^2/(c + d*x)^2, x)]

    @test_int [(c + d*x)^3*tanh(e + f*x)^3, x, 13, -((3*d*(c + d*x)^2)/(2*f^2)) + (c + d*x)^3/(2*f) - (c + d*x)^4/(4*d) + (3*d^2*(c + d*x)*log(1 + ℯ^(2*(e + f*x))))/f^3 + ((c + d*x)^3*log(1 + ℯ^(2*(e + f*x))))/f + (3*d^3*PolyLog(2, -ℯ^(2*(e + f*x))))/(2*f^4) + (3*d*(c + d*x)^2*PolyLog(2, -ℯ^(2*(e + f*x))))/(2*f^2) - (3*d^2*(c + d*x)*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^3) + (3*d^3*PolyLog(4, -ℯ^(2*(e + f*x))))/(4*f^4) - (3*d*(c + d*x)^2*tanh(e + f*x))/(2*f^2) - ((c + d*x)^3*tanh(e + f*x)^2)/(2*f)]
    @test_int [(c + d*x)^2*tanh(e + f*x)^3, x, 9, (c*d*x)/f + (d^2*x^2)/(2*f) - (c + d*x)^3/(3*d) + ((c + d*x)^2*log(1 + ℯ^(2*(e + f*x))))/f + (d^2*log(cosh(e + f*x)))/f^3 + (d*(c + d*x)*PolyLog(2, -ℯ^(2*(e + f*x))))/f^2 - (d^2*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^3) - (d*(c + d*x)*tanh(e + f*x))/f^2 - ((c + d*x)^2*tanh(e + f*x)^2)/(2*f)]
    @test_int [(c + d*x)^1*tanh(e + f*x)^3, x, 7, (d*x)/(2*f) - (c + d*x)^2/(2*d) + ((c + d*x)*log(1 + ℯ^(2*(e + f*x))))/f + (d*PolyLog(2, -ℯ^(2*(e + f*x))))/(2*f^2) - (d*tanh(e + f*x))/(2*f^2) - ((c + d*x)*tanh(e + f*x)^2)/(2*f)]
    @test_int [tanh(e + f*x)^3/(c + d*x)^1, x, 0, Unintegrable(tanh(e + f*x)^3/(c + d*x), x)]
    @test_int [tanh(e + f*x)^3/(c + d*x)^2, x, 0, Unintegrable(tanh(e + f*x)^3/(c + d*x)^2, x)]

    #= ::Subsection::Closed:: =#
    #=Integrands*of*the*form*(c+d*x)^m*(b*tanh(e+f*x))^(n/2)=#

    #= ::Subsubsection::Closed:: =#
    #=m>0=#

    @test_int [(c + d*x)*(b*tanh(e + f*x))^(5/2), x, 44, (2*b^(5/2)*d*atan(sqrt(b*tanh(e + f*x))/sqrt(b)))/(3*f^2) - ((-b)^(5/2)*(c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b)))/f - ((-b)^(5/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))^2)/(2*f^2) + (2*b^(5/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b)))/(3*f^2) + (b^(5/2)*(c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(b)))/f + (b^(5/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))^2)/(2*f^2) - (b^(5/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/f^2 + (b^(5/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/f^2 - (b^(5/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(2*f^2) - (b^(5/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(2*f^2) + ((-b)^(5/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/f^2 - ((-b)^(5/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log((2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(2*f^2) - ((-b)^(5/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(-((2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))))/(2*f^2) - ((-b)^(5/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/f^2 - (b^(5/2)*d*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/(2*f^2) - (b^(5/2)*d*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/(2*f^2) + (b^(5/2)*d*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(4*f^2) + (b^(5/2)*d*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(4*f^2) + ((-b)^(5/2)*d*PolyLog(2, 1 - 2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/(2*f^2) - ((-b)^(5/2)*d*PolyLog(2, 1 - (2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(4*f^2) - ((-b)^(5/2)*d*PolyLog(2, 1 + (2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(4*f^2) + ((-b)^(5/2)*d*PolyLog(2, 1 - 2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/(2*f^2) - (4*b^2*d*sqrt(b*tanh(e + f*x)))/(3*f^2) - (2*b*(c + d*x)*(b*tanh(e + f*x))^(3/2))/(3*f)]
    @test_int [(c + d*x)*(b*tanh(e + f*x))^(3/2), x, 43, -((2*b^(3/2)*d*atan(sqrt(b*tanh(e + f*x))/sqrt(b)))/f^2) - ((-b)^(3/2)*(c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b)))/f - ((-b)^(3/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))^2)/(2*f^2) + (2*b^(3/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b)))/f^2 + (b^(3/2)*(c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(b)))/f + (b^(3/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))^2)/(2*f^2) - (b^(3/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/f^2 + (b^(3/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/f^2 - (b^(3/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(2*f^2) - (b^(3/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(2*f^2) + ((-b)^(3/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/f^2 - ((-b)^(3/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log((2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(2*f^2) - ((-b)^(3/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(-((2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))))/(2*f^2) - ((-b)^(3/2)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/f^2 - (b^(3/2)*d*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/(2*f^2) - (b^(3/2)*d*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/(2*f^2) + (b^(3/2)*d*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(4*f^2) + (b^(3/2)*d*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(4*f^2) + ((-b)^(3/2)*d*PolyLog(2, 1 - 2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/(2*f^2) - ((-b)^(3/2)*d*PolyLog(2, 1 - (2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(4*f^2) - ((-b)^(3/2)*d*PolyLog(2, 1 + (2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(4*f^2) + ((-b)^(3/2)*d*PolyLog(2, 1 - 2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/(2*f^2) - (2*b*(c + d*x)*sqrt(b*tanh(e + f*x)))/f]
    @test_int [(c + d*x)*(b*tanh(e + f*x))^(1/2), x, 37, -((sqrt(-b)*(c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b)))/f) - (sqrt(-b)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))^2)/(2*f^2) + (sqrt(b)*(c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(b)))/f + (sqrt(b)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))^2)/(2*f^2) - (sqrt(b)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/f^2 + (sqrt(b)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/f^2 - (sqrt(b)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(2*f^2) - (sqrt(b)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(2*f^2) + (sqrt(-b)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/f^2 - (sqrt(-b)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log((2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(2*f^2) - (sqrt(-b)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(-((2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))))/(2*f^2) - (sqrt(-b)*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/f^2 - (sqrt(b)*d*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/(2*f^2) - (sqrt(b)*d*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/(2*f^2) + (sqrt(b)*d*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(4*f^2) + (sqrt(b)*d*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(4*f^2) + (sqrt(-b)*d*PolyLog(2, 1 - 2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/(2*f^2) - (sqrt(-b)*d*PolyLog(2, 1 - (2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(4*f^2) - (sqrt(-b)*d*PolyLog(2, 1 + (2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(4*f^2) + (sqrt(-b)*d*PolyLog(2, 1 - 2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/(2*f^2)]
    @test_int [(c + d*x)/(b*tanh(e + f*x))^(1/2), x, 37, -(((c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b)))/(sqrt(-b)*f)) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))^2)/(2*sqrt(-b)*f^2) + ((c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(b)))/(sqrt(b)*f) + (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))^2)/(2*sqrt(b)*f^2) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/(sqrt(b)*f^2) + (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/(sqrt(b)*f^2) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(2*sqrt(b)*f^2) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(2*sqrt(b)*f^2) + (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/(sqrt(-b)*f^2) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log((2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(2*sqrt(-b)*f^2) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(-((2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))))/(2*sqrt(-b)*f^2) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/(sqrt(-b)*f^2) - (d*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/(2*sqrt(b)*f^2) - (d*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/(2*sqrt(b)*f^2) + (d*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(4*sqrt(b)*f^2) + (d*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(4*sqrt(b)*f^2) + (d*PolyLog(2, 1 - 2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/(2*sqrt(-b)*f^2) - (d*PolyLog(2, 1 - (2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(4*sqrt(-b)*f^2) - (d*PolyLog(2, 1 + (2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(4*sqrt(-b)*f^2) + (d*PolyLog(2, 1 - 2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/(2*sqrt(-b)*f^2)]
    @test_int [(c + d*x)/(b*tanh(e + f*x))^(3/2), x, 43, (2*d*atan(sqrt(b*tanh(e + f*x))/sqrt(b)))/(b^(3/2)*f^2) - ((c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b)))/((-b)^(3/2)*f) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))^2)/(2*(-b)^(3/2)*f^2) + (2*d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b)))/(b^(3/2)*f^2) + ((c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(b)))/(b^(3/2)*f) + (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))^2)/(2*b^(3/2)*f^2) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/(b^(3/2)*f^2) + (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/(b^(3/2)*f^2) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(2*b^(3/2)*f^2) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(2*b^(3/2)*f^2) + (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/((-b)^(3/2)*f^2) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log((2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(2*(-b)^(3/2)*f^2) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(-((2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))))/(2*(-b)^(3/2)*f^2) - (d*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/((-b)^(3/2)*f^2) - (d*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/(2*b^(3/2)*f^2) - (d*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/(2*b^(3/2)*f^2) + (d*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(4*b^(3/2)*f^2) + (d*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(4*b^(3/2)*f^2) + (d*PolyLog(2, 1 - 2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/(2*(-b)^(3/2)*f^2) - (d*PolyLog(2, 1 - (2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(4*(-b)^(3/2)*f^2) - (d*PolyLog(2, 1 + (2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/(4*(-b)^(3/2)*f^2) + (d*PolyLog(2, 1 - 2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/(2*(-b)^(3/2)*f^2) - (2*(c + d*x))/(b*f*sqrt(b*tanh(e + f*x)))]

    @test_int [(c + d*x)^2*(b*tanh(e + f*x))^(3/2), x, 38, (4*(-b)^(3/2)*d*(c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b)))/f^2 + (2*(-b)^(3/2)*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))^2)/f^3 + (4*b^(3/2)*d*(c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(b)))/f^2 + (2*b^(3/2)*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))^2)/f^3 - (4*b^(3/2)*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/f^3 + (4*b^(3/2)*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/f^3 - (2*b^(3/2)*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/f^3 - (2*b^(3/2)*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/f^3 - (4*(-b)^(3/2)*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/f^3 + (2*(-b)^(3/2)*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log((2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/f^3 + (2*(-b)^(3/2)*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(-((2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))))/f^3 + (4*(-b)^(3/2)*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/f^3 - (2*b^(3/2)*d^2*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/f^3 - (2*b^(3/2)*d^2*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/f^3 + (b^(3/2)*d^2*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/f^3 + (b^(3/2)*d^2*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/f^3 - (2*(-b)^(3/2)*d^2*PolyLog(2, 1 - 2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/f^3 + ((-b)^(3/2)*d^2*PolyLog(2, 1 - (2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/f^3 + ((-b)^(3/2)*d^2*PolyLog(2, 1 + (2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/f^3 - (2*(-b)^(3/2)*d^2*PolyLog(2, 1 - 2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/f^3 - (2*b*(c + d*x)^2*sqrt(b*tanh(e + f*x)))/f + b^2*Unintegrable((c + d*x)^2/sqrt(b*tanh(e + f*x)), x)]
    @test_int [(c + d*x)^2*(b*tanh(e + f*x))^(1/2), x, 0, Unintegrable((c + d*x)^2*sqrt(b*tanh(e + f*x)), x)]
    @test_int [(c + d*x)^2/(b*tanh(e + f*x))^(1/2), x, 0, Unintegrable((c + d*x)^2/sqrt(b*tanh(e + f*x)), x)]
    @test_int [(c + d*x)^2/(b*tanh(e + f*x))^(3/2), x, 38, (4*d*(c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b)))/((-b)^(3/2)*f^2) + (2*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))^2)/((-b)^(3/2)*f^3) + (4*d*(c + d*x)*atanh(sqrt(b*tanh(e + f*x))/sqrt(b)))/(b^(3/2)*f^2) + (2*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))^2)/(b^(3/2)*f^3) - (4*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/(b^(3/2)*f^3) + (4*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/(b^(3/2)*f^3) - (2*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(b^(3/2)*f^3) - (2*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(b))*log((2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(b^(3/2)*f^3) - (4*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/((-b)^(3/2)*f^3) + (2*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log((2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/((-b)^(3/2)*f^3) + (2*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(-((2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))))/((-b)^(3/2)*f^3) + (4*d^2*atanh(sqrt(b*tanh(e + f*x))/sqrt(-b))*log(2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/((-b)^(3/2)*f^3) - (2*d^2*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) - sqrt(b*tanh(e + f*x)))))/(b^(3/2)*f^3) - (2*d^2*PolyLog(2, 1 - (2*sqrt(b))/(sqrt(b) + sqrt(b*tanh(e + f*x)))))/(b^(3/2)*f^3) + (d^2*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(b^(3/2)*f^3) + (d^2*PolyLog(2, 1 - (2*sqrt(b)*(sqrt(-b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(sqrt(b) + sqrt(b*tanh(e + f*x))))))/(b^(3/2)*f^3) - (2*d^2*PolyLog(2, 1 - 2/(1 - sqrt(b*tanh(e + f*x))/sqrt(-b))))/((-b)^(3/2)*f^3) + (d^2*PolyLog(2, 1 - (2*(sqrt(b) - sqrt(b*tanh(e + f*x))))/((sqrt(-b) + sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/((-b)^(3/2)*f^3) + (d^2*PolyLog(2, 1 + (2*(sqrt(b) + sqrt(b*tanh(e + f*x))))/((sqrt(-b) - sqrt(b))*(1 - sqrt(b*tanh(e + f*x))/sqrt(-b)))))/((-b)^(3/2)*f^3) - (2*d^2*PolyLog(2, 1 - 2/(1 + sqrt(b*tanh(e + f*x))/sqrt(-b))))/((-b)^(3/2)*f^3) - (2*(c + d*x)^2)/(b*f*sqrt(b*tanh(e + f*x))) + Unintegrable((c + d*x)^2*sqrt(b*tanh(e + f*x)), x)/b^2]

    #= ::Subsubsection::Closed:: =#
    #=m<0=#

    @test_int [(b*tanh(e + f*x))^(3/2)/(c + d*x), x, 0, Unintegrable((b*tanh(e + f*x))^(3/2)/(c + d*x), x)]
    @test_int [(b*tanh(e + f*x))^(1/2)/(c + d*x), x, 0, Unintegrable(sqrt(b*tanh(e + f*x))/(c + d*x), x)]
    @test_int [1/((c + d*x)*(b*tanh(e + f*x))^(1/2)), x, 0, Unintegrable(1/((c + d*x)*sqrt(b*tanh(e + f*x))), x)]
    @test_int [1/((c + d*x)*(b*tanh(e + f*x))^(3/2)), x, 0, Unintegrable(1/((c + d*x)*(b*tanh(e + f*x))^(3/2)), x)]

    #= ::Subsection::Closed:: =#
    #=Integrands*of*the*form*(c+d*x)^m*(b*tanh(e+f*x))^n*with*m*symbolic=#

    @test_int [x^m*tanh(a + b*x)^3, x, 0, Unintegrable(x^m*tanh(a + b*x)^3, x)]
    @test_int [x^m*tanh(a + b*x)^2, x, 0, Unintegrable(x^m*tanh(a + b*x)^2, x)]
    @test_int [x^m*tanh(a + b*x)^1, x, 0, Unintegrable(x^m*tanh(a + b*x), x)]

    #= ::Section::Closed:: =#
    #=Integrands*of*the*form*(c+d*x)^m*(a+b*tanh(e+f*x))^n*with*a^2-b^2=0=#

    #= ::Subsection::Closed:: =#
    #=Integrands*of*the*form*(c+d*x)^m*(a+a*tanh(e+f*x))^n=#

    #= ::Subsubsection:: =#
    #=n>0=#

    #= ::Subsubsection::Closed:: =#
    #=n<0=#

    @test_int [(c + d*x)^3/(a + a*tanh(e + f*x)), x, 5, (3*d^3*x)/(8*a*f^3) + (3*d*(c + d*x)^2)/(8*a*f^2) + (c + d*x)^3/(4*a*f) + (c + d*x)^4/(8*a*d) - (3*d^3)/(8*f^4*(a + a*tanh(e + f*x))) - (3*d^2*(c + d*x))/(4*f^3*(a + a*tanh(e + f*x))) - (3*d*(c + d*x)^2)/(4*f^2*(a + a*tanh(e + f*x))) - (c + d*x)^3/(2*f*(a + a*tanh(e + f*x)))]
    @test_int [(c + d*x)^2/(a + a*tanh(e + f*x)), x, 4, (d^2*x)/(4*a*f^2) + (c + d*x)^2/(4*a*f) + (c + d*x)^3/(6*a*d) - d^2/(4*f^3*(a + a*tanh(e + f*x))) - (d*(c + d*x))/(2*f^2*(a + a*tanh(e + f*x))) - (c + d*x)^2/(2*f*(a + a*tanh(e + f*x)))]
    @test_int [(c + d*x)^1/(a + a*tanh(e + f*x)), x, 3, (d*x)/(4*a*f) + (c + d*x)^2/(4*a*d) - d/(4*f^2*(a + a*tanh(e + f*x))) - (c + d*x)/(2*f*(a + a*tanh(e + f*x)))]
    @test_int [1/((c + d*x)^1*(a + a*tanh(e + f*x))), x, 7, (cosh(2*e - (2*c*f)/d)*CoshIntegral((2*c*f)/d + 2*f*x))/(2*a*d) + log(c + d*x)/(2*a*d) - (CoshIntegral((2*c*f)/d + 2*f*x)*sinh(2*e - (2*c*f)/d))/(2*a*d) - (cosh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(2*a*d) + (sinh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(2*a*d)]
    @test_int [1/((c + d*x)^2*(a + a*tanh(e + f*x))), x, 7, -((f*cosh(2*e - (2*c*f)/d)*CoshIntegral((2*c*f)/d + 2*f*x))/(a*d^2)) + (f*CoshIntegral((2*c*f)/d + 2*f*x)*sinh(2*e - (2*c*f)/d))/(a*d^2) + (f*cosh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(a*d^2) - (f*sinh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(a*d^2) - 1/(d*(c + d*x)*(a + a*tanh(e + f*x)))]
    @test_int [1/((c + d*x)^3*(a + a*tanh(e + f*x))), x, 8, -(f/(2*a*d^2*(c + d*x))) + (f^2*cosh(2*e - (2*c*f)/d)*CoshIntegral((2*c*f)/d + 2*f*x))/(a*d^3) - (f^2*CoshIntegral((2*c*f)/d + 2*f*x)*sinh(2*e - (2*c*f)/d))/(a*d^3) - (f^2*cosh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(a*d^3) + (f^2*sinh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(a*d^3) - 1/(2*d*(c + d*x)^2*(a + a*tanh(e + f*x))) + f/(d^2*(c + d*x)*(a + a*tanh(e + f*x)))]

    @test_int [(c + d*x)^3/(a + a*tanh(e + f*x))^2, x, 10, -((3*d^3*ℯ^(-4*e - 4*f*x))/(512*a^2*f^4)) - (3*d^3*ℯ^(-2*e - 2*f*x))/(16*a^2*f^4) - (3*d^2*ℯ^(-4*e - 4*f*x)*(c + d*x))/(128*a^2*f^3) - (3*d^2*ℯ^(-2*e - 2*f*x)*(c + d*x))/(8*a^2*f^3) - (3*d*ℯ^(-4*e - 4*f*x)*(c + d*x)^2)/(64*a^2*f^2) - (3*d*ℯ^(-2*e - 2*f*x)*(c + d*x)^2)/(8*a^2*f^2) - (ℯ^(-4*e - 4*f*x)*(c + d*x)^3)/(16*a^2*f) - (ℯ^(-2*e - 2*f*x)*(c + d*x)^3)/(4*a^2*f) + (c + d*x)^4/(16*a^2*d)]
    @test_int [(c + d*x)^2/(a + a*tanh(e + f*x))^2, x, 8, -((d^2*ℯ^(-4*e - 4*f*x))/(128*a^2*f^3)) - (d^2*ℯ^(-2*e - 2*f*x))/(8*a^2*f^3) - (d*ℯ^(-4*e - 4*f*x)*(c + d*x))/(32*a^2*f^2) - (d*ℯ^(-2*e - 2*f*x)*(c + d*x))/(4*a^2*f^2) - (ℯ^(-4*e - 4*f*x)*(c + d*x)^2)/(16*a^2*f) - (ℯ^(-2*e - 2*f*x)*(c + d*x)^2)/(4*a^2*f) + (c + d*x)^3/(12*a^2*d)]
    @test_int [(c + d*x)^1/(a + a*tanh(e + f*x))^2, x, 7, (3*d*x)/(16*a^2*f) - (d*x^2)/(8*a^2) + (x*(c + d*x))/(4*a^2) - d/(16*f^2*(a + a*tanh(e + f*x))^2) - (c + d*x)/(4*f*(a + a*tanh(e + f*x))^2) - (3*d)/(16*f^2*(a^2 + a^2*tanh(e + f*x))) - (c + d*x)/(4*f*(a^2 + a^2*tanh(e + f*x)))]
    @test_int [1/((c + d*x)^1*(a + a*tanh(e + f*x))^2), x, 21, (cosh(2*e - (2*c*f)/d)*CoshIntegral((2*c*f)/d + 2*f*x))/(2*a^2*d) + (cosh(4*e - (4*c*f)/d)*CoshIntegral((4*c*f)/d + 4*f*x))/(4*a^2*d) + log(c + d*x)/(4*a^2*d) - (CoshIntegral((4*c*f)/d + 4*f*x)*sinh(4*e - (4*c*f)/d))/(4*a^2*d) - (CoshIntegral((2*c*f)/d + 2*f*x)*sinh(2*e - (2*c*f)/d))/(2*a^2*d) - (cosh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(2*a^2*d) + (sinh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(2*a^2*d) - (cosh(4*e - (4*c*f)/d)*SinhIntegral((4*c*f)/d + 4*f*x))/(4*a^2*d) + (sinh(4*e - (4*c*f)/d)*SinhIntegral((4*c*f)/d + 4*f*x))/(4*a^2*d)]
    @test_int [1/((c + d*x)^2*(a + a*tanh(e + f*x))^2), x, 24, -(1/(4*a^2*d*(c + d*x))) - cosh(2*e + 2*f*x)/(2*a^2*d*(c + d*x)) - cosh(2*e + 2*f*x)^2/(4*a^2*d*(c + d*x)) - (f*cosh(2*e - (2*c*f)/d)*CoshIntegral((2*c*f)/d + 2*f*x))/(a^2*d^2) - (f*cosh(4*e - (4*c*f)/d)*CoshIntegral((4*c*f)/d + 4*f*x))/(a^2*d^2) + (f*CoshIntegral((4*c*f)/d + 4*f*x)*sinh(4*e - (4*c*f)/d))/(a^2*d^2) + (f*CoshIntegral((2*c*f)/d + 2*f*x)*sinh(2*e - (2*c*f)/d))/(a^2*d^2) + sinh(2*e + 2*f*x)/(2*a^2*d*(c + d*x)) - sinh(2*e + 2*f*x)^2/(4*a^2*d*(c + d*x)) + sinh(4*e + 4*f*x)/(4*a^2*d*(c + d*x)) + (f*cosh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(a^2*d^2) - (f*sinh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(a^2*d^2) + (f*cosh(4*e - (4*c*f)/d)*SinhIntegral((4*c*f)/d + 4*f*x))/(a^2*d^2) - (f*sinh(4*e - (4*c*f)/d)*SinhIntegral((4*c*f)/d + 4*f*x))/(a^2*d^2)]

    @test_int [(c + d*x)^3/(a + a*tanh(e + f*x))^3, x, 14, -((d^3*ℯ^(-6*e - 6*f*x))/(1728*a^3*f^4)) - (9*d^3*ℯ^(-4*e - 4*f*x))/(1024*a^3*f^4) - (9*d^3*ℯ^(-2*e - 2*f*x))/(64*a^3*f^4) - (d^2*ℯ^(-6*e - 6*f*x)*(c + d*x))/(288*a^3*f^3) - (9*d^2*ℯ^(-4*e - 4*f*x)*(c + d*x))/(256*a^3*f^3) - (9*d^2*ℯ^(-2*e - 2*f*x)*(c + d*x))/(32*a^3*f^3) - (d*ℯ^(-6*e - 6*f*x)*(c + d*x)^2)/(96*a^3*f^2) - (9*d*ℯ^(-4*e - 4*f*x)*(c + d*x)^2)/(128*a^3*f^2) - (9*d*ℯ^(-2*e - 2*f*x)*(c + d*x)^2)/(32*a^3*f^2) - (ℯ^(-6*e - 6*f*x)*(c + d*x)^3)/(48*a^3*f) - (3*ℯ^(-4*e - 4*f*x)*(c + d*x)^3)/(32*a^3*f) - (3*ℯ^(-2*e - 2*f*x)*(c + d*x)^3)/(16*a^3*f) + (c + d*x)^4/(32*a^3*d)]
    @test_int [(c + d*x)^2/(a + a*tanh(e + f*x))^3, x, 11, -((d^2*ℯ^(-6*e - 6*f*x))/(864*a^3*f^3)) - (3*d^2*ℯ^(-4*e - 4*f*x))/(256*a^3*f^3) - (3*d^2*ℯ^(-2*e - 2*f*x))/(32*a^3*f^3) - (d*ℯ^(-6*e - 6*f*x)*(c + d*x))/(144*a^3*f^2) - (3*d*ℯ^(-4*e - 4*f*x)*(c + d*x))/(64*a^3*f^2) - (3*d*ℯ^(-2*e - 2*f*x)*(c + d*x))/(16*a^3*f^2) - (ℯ^(-6*e - 6*f*x)*(c + d*x)^2)/(48*a^3*f) - (3*ℯ^(-4*e - 4*f*x)*(c + d*x)^2)/(32*a^3*f) - (3*ℯ^(-2*e - 2*f*x)*(c + d*x)^2)/(16*a^3*f) + (c + d*x)^3/(24*a^3*d)]
    @test_int [(c + d*x)^1/(a + a*tanh(e + f*x))^3, x, 11, (11*d*x)/(96*a^3*f) - (d*x^2)/(16*a^3) + (x*(c + d*x))/(8*a^3) - d/(36*f^2*(a + a*tanh(e + f*x))^3) - (c + d*x)/(6*f*(a + a*tanh(e + f*x))^3) - (5*d)/(96*a*f^2*(a + a*tanh(e + f*x))^2) - (c + d*x)/(8*a*f*(a + a*tanh(e + f*x))^2) - (11*d)/(96*f^2*(a^3 + a^3*tanh(e + f*x))) - (c + d*x)/(8*f*(a^3 + a^3*tanh(e + f*x)))]
    @test_int [1/((c + d*x)^1*(a + a*tanh(e + f*x))^3), x, 53, (3*cosh(2*e - (2*c*f)/d)*CoshIntegral((2*c*f)/d + 2*f*x))/(8*a^3*d) + (3*cosh(4*e - (4*c*f)/d)*CoshIntegral((4*c*f)/d + 4*f*x))/(8*a^3*d) + (cosh(6*e - (6*c*f)/d)*CoshIntegral((6*c*f)/d + 6*f*x))/(8*a^3*d) + log(c + d*x)/(8*a^3*d) - (CoshIntegral((6*c*f)/d + 6*f*x)*sinh(6*e - (6*c*f)/d))/(8*a^3*d) - (3*CoshIntegral((4*c*f)/d + 4*f*x)*sinh(4*e - (4*c*f)/d))/(8*a^3*d) - (3*CoshIntegral((2*c*f)/d + 2*f*x)*sinh(2*e - (2*c*f)/d))/(8*a^3*d) - (3*cosh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(8*a^3*d) + (3*sinh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(8*a^3*d) - (3*cosh(4*e - (4*c*f)/d)*SinhIntegral((4*c*f)/d + 4*f*x))/(8*a^3*d) + (3*sinh(4*e - (4*c*f)/d)*SinhIntegral((4*c*f)/d + 4*f*x))/(8*a^3*d) - (cosh(6*e - (6*c*f)/d)*SinhIntegral((6*c*f)/d + 6*f*x))/(8*a^3*d) + (sinh(6*e - (6*c*f)/d)*SinhIntegral((6*c*f)/d + 6*f*x))/(8*a^3*d)]
    @test_int [1/((c + d*x)^2*(a + a*tanh(e + f*x))^3), x, 60, -(1/(8*a^3*d*(c + d*x))) - (9*cosh(2*e + 2*f*x))/(32*a^3*d*(c + d*x)) - (3*cosh(2*e + 2*f*x)^2)/(8*a^3*d*(c + d*x)) - cosh(2*e + 2*f*x)^3/(8*a^3*d*(c + d*x)) - (3*cosh(6*e + 6*f*x))/(32*a^3*d*(c + d*x)) - (3*f*cosh(2*e - (2*c*f)/d)*CoshIntegral((2*c*f)/d + 2*f*x))/(4*a^3*d^2) - (3*f*cosh(4*e - (4*c*f)/d)*CoshIntegral((4*c*f)/d + 4*f*x))/(2*a^3*d^2) - (3*f*cosh(6*e - (6*c*f)/d)*CoshIntegral((6*c*f)/d + 6*f*x))/(4*a^3*d^2) + (3*f*CoshIntegral((6*c*f)/d + 6*f*x)*sinh(6*e - (6*c*f)/d))/(4*a^3*d^2) + (3*f*CoshIntegral((4*c*f)/d + 4*f*x)*sinh(4*e - (4*c*f)/d))/(2*a^3*d^2) + (3*f*CoshIntegral((2*c*f)/d + 2*f*x)*sinh(2*e - (2*c*f)/d))/(4*a^3*d^2) + (15*sinh(2*e + 2*f*x))/(32*a^3*d*(c + d*x)) - (3*sinh(2*e + 2*f*x)^2)/(8*a^3*d*(c + d*x)) + sinh(2*e + 2*f*x)^3/(8*a^3*d*(c + d*x)) + (3*sinh(4*e + 4*f*x))/(8*a^3*d*(c + d*x)) + (3*sinh(6*e + 6*f*x))/(32*a^3*d*(c + d*x)) + (3*f*cosh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(4*a^3*d^2) - (3*f*sinh(2*e - (2*c*f)/d)*SinhIntegral((2*c*f)/d + 2*f*x))/(4*a^3*d^2) + (3*f*cosh(4*e - (4*c*f)/d)*SinhIntegral((4*c*f)/d + 4*f*x))/(2*a^3*d^2) - (3*f*sinh(4*e - (4*c*f)/d)*SinhIntegral((4*c*f)/d + 4*f*x))/(2*a^3*d^2) + (3*f*cosh(6*e - (6*c*f)/d)*SinhIntegral((6*c*f)/d + 6*f*x))/(4*a^3*d^2) - (3*f*sinh(6*e - (6*c*f)/d)*SinhIntegral((6*c*f)/d + 6*f*x))/(4*a^3*d^2)]

    #= ::Subsection::Closed:: =#
    #=Integrands*of*the*form*(c+d*x)^m*(a+a*tanh(e+f*x))^n*with*m*symbolic=#

    @test_int [(c + d*x)^m*(a + a*tanh(e + f*x))^2, x, 0, Unintegrable((c + d*x)^m*(a + a*tanh(e + f*x))^2, x)]
    @test_int [(c + d*x)^m*(a + a*tanh(e + f*x))^1, x, 0, Unintegrable((c+d*x)^m*(a+a*tanh(e+f*x)),x)]
    @test_int [(c + d*x)^m/(a + a*tanh(e + f*x))^1, x, 2, (c + d*x)^(1 + m)/(2*a*d*(1 + m)) - (2^(-2 - m)*ℯ^(-2*e + (2*c*f)/d)*(c + d*x)^m*Gamma(1 + m, (2*f*(c + d*x))/d))/(((f*(c + d*x))/d)^m*(a*f))]
    @test_int [(c + d*x)^m/(a + a*tanh(e + f*x))^2, x, 4, (c + d*x)^(1 + m)/(4*a^2*d*(1 + m)) - (2^(-2 - m)*ℯ^(-2*e + (2*c*f)/d)*(c + d*x)^m*Gamma(1 + m, (2*f*(c + d*x))/d))/(((f*(c + d*x))/d)^m*(a^2*f)) - (4^(-2 - m)*ℯ^(-4*e + (4*c*f)/d)*(c + d*x)^m*Gamma(1 + m, (4*f*(c + d*x))/d))/(((f*(c + d*x))/d)^m*(a^2*f))]
    @test_int [(c + d*x)^m/(a + a*tanh(e + f*x))^3, x, 5, (c + d*x)^(1 + m)/(8*a^3*d*(1 + m)) - (3*2^(-4 - m)*ℯ^(-2*e + (2*c*f)/d)*(c + d*x)^m*Gamma(1 + m, (2*f*(c + d*x))/d))/(((f*(c + d*x))/d)^m*(a^3*f)) - (3*2^(-5 - 2*m)*ℯ^(-4*e + (4*c*f)/d)*(c + d*x)^m*Gamma(1 + m, (4*f*(c + d*x))/d))/(((f*(c + d*x))/d)^m*(a^3*f)) - (2^(-4 - m)*3^(-1 - m)*ℯ^(-6*e + (6*c*f)/d)*(c + d*x)^m*Gamma(1 + m, (6*f*(c + d*x))/d))/(((f*(c + d*x))/d)^m*(a^3*f))]

    #= ::Section::Closed:: =#
    #=Integrands*of*the*form*(c+d*x)^m*(a+b*tanh(e+f*x))^n=#

    #= ::Subsection::Closed:: =#
    #=Integrands*of*the*form*(c+d*x)^m*(a+b*tanh(e+f*x))^n=#

    #= ::Subsubsection::Closed:: =#
    #=n>0=#

    @test_int [(c + d*x)^3*(a + b*tanh(e + f*x)), x, 8, (a*(c + d*x)^4)/(4*d) - (b*(c + d*x)^4)/(4*d) + (b*(c + d*x)^3*log(1 + ℯ^(2*(e + f*x))))/f + (3*b*d*(c + d*x)^2*PolyLog(2, -ℯ^(2*(e + f*x))))/(2*f^2) - (3*b*d^2*(c + d*x)*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^3) + (3*b*d^3*PolyLog(4, -ℯ^(2*(e + f*x))))/(4*f^4)]
    @test_int [(c + d*x)^2*(a + b*tanh(e + f*x)), x, 7, (a*(c + d*x)^3)/(3*d) - (b*(c + d*x)^3)/(3*d) + (b*(c + d*x)^2*log(1 + ℯ^(2*(e + f*x))))/f + (b*d*(c + d*x)*PolyLog(2, -ℯ^(2*(e + f*x))))/f^2 - (b*d^2*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^3)]
    @test_int [(c + d*x)^1*(a + b*tanh(e + f*x)), x, 6, (a*(c + d*x)^2)/(2*d) - (b*(c + d*x)^2)/(2*d) + (b*(c + d*x)*log(1 + ℯ^(2*(e + f*x))))/f + (b*d*PolyLog(2, -ℯ^(2*(e + f*x))))/(2*f^2)]
    @test_int [(a + b*tanh(e + f*x))/(c + d*x)^1, x, 0, Unintegrable((a + b*tanh(e + f*x))/(c + d*x), x)]
    @test_int [(a + b*tanh(e + f*x))/(c + d*x)^2, x, 0, Unintegrable((a + b*tanh(e + f*x))/(c + d*x)^2, x)]

    @test_int [(c + d*x)^3*(a + b*tanh(e + f*x))^2, x, 15, -((b^2*(c + d*x)^3)/f) + (a^2*(c + d*x)^4)/(4*d) - (a*b*(c + d*x)^4)/(2*d) + (b^2*(c + d*x)^4)/(4*d) + (3*b^2*d*(c + d*x)^2*log(1 + ℯ^(2*(e + f*x))))/f^2 + (2*a*b*(c + d*x)^3*log(1 + ℯ^(2*(e + f*x))))/f + (3*b^2*d^2*(c + d*x)*PolyLog(2, -ℯ^(2*(e + f*x))))/f^3 + (3*a*b*d*(c + d*x)^2*PolyLog(2, -ℯ^(2*(e + f*x))))/f^2 - (3*b^2*d^3*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^4) - (3*a*b*d^2*(c + d*x)*PolyLog(3, -ℯ^(2*(e + f*x))))/f^3 + (3*a*b*d^3*PolyLog(4, -ℯ^(2*(e + f*x))))/(2*f^4) - (b^2*(c + d*x)^3*tanh(e + f*x))/f]
    @test_int [(c + d*x)^2*(a + b*tanh(e + f*x))^2, x, 13, -((b^2*(c + d*x)^2)/f) + (a^2*(c + d*x)^3)/(3*d) - (2*a*b*(c + d*x)^3)/(3*d) + (b^2*(c + d*x)^3)/(3*d) + (2*b^2*d*(c + d*x)*log(1 + ℯ^(2*(e + f*x))))/f^2 + (2*a*b*(c + d*x)^2*log(1 + ℯ^(2*(e + f*x))))/f + (b^2*d^2*PolyLog(2, -ℯ^(2*(e + f*x))))/f^3 + (2*a*b*d*(c + d*x)*PolyLog(2, -ℯ^(2*(e + f*x))))/f^2 - (a*b*d^2*PolyLog(3, -ℯ^(2*(e + f*x))))/f^3 - (b^2*(c + d*x)^2*tanh(e + f*x))/f]
    @test_int [(c + d*x)^1*(a + b*tanh(e + f*x))^2, x, 9, b^2*c*x + (1/2)*b^2*d*x^2 + (a^2*(c + d*x)^2)/(2*d) - (a*b*(c + d*x)^2)/d + (2*a*b*(c + d*x)*log(1 + ℯ^(2*(e + f*x))))/f + (b^2*d*log(cosh(e + f*x)))/f^2 + (a*b*d*PolyLog(2, -ℯ^(2*(e + f*x))))/f^2 - (b^2*(c + d*x)*tanh(e + f*x))/f]
    @test_int [(a + b*tanh(e + f*x))^2/(c + d*x)^1, x, 0, Unintegrable((a + b*tanh(e + f*x))^2/(c + d*x), x)]
    @test_int [(a + b*tanh(e + f*x))^2/(c + d*x)^2, x, 0, Unintegrable((a + b*tanh(e + f*x))^2/(c + d*x)^2, x)]

    @test_int [(c + d*x)^3*(a + b*tanh(e + f*x))^3, x, 28, -((3*b^3*d*(c + d*x)^2)/(2*f^2)) - (3*a*b^2*(c + d*x)^3)/f + (b^3*(c + d*x)^3)/(2*f) + (a^3*(c + d*x)^4)/(4*d) - (3*a^2*b*(c + d*x)^4)/(4*d) + (3*a*b^2*(c + d*x)^4)/(4*d) - (b^3*(c + d*x)^4)/(4*d) + (3*b^3*d^2*(c + d*x)*log(1 + ℯ^(2*(e + f*x))))/f^3 + (9*a*b^2*d*(c + d*x)^2*log(1 + ℯ^(2*(e + f*x))))/f^2 + (3*a^2*b*(c + d*x)^3*log(1 + ℯ^(2*(e + f*x))))/f + (b^3*(c + d*x)^3*log(1 + ℯ^(2*(e + f*x))))/f + (3*b^3*d^3*PolyLog(2, -ℯ^(2*(e + f*x))))/(2*f^4) + (9*a*b^2*d^2*(c + d*x)*PolyLog(2, -ℯ^(2*(e + f*x))))/f^3 + (9*a^2*b*d*(c + d*x)^2*PolyLog(2, -ℯ^(2*(e + f*x))))/(2*f^2) + (3*b^3*d*(c + d*x)^2*PolyLog(2, -ℯ^(2*(e + f*x))))/(2*f^2) - (9*a*b^2*d^3*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^4) - (9*a^2*b*d^2*(c + d*x)*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^3) - (3*b^3*d^2*(c + d*x)*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^3) + (9*a^2*b*d^3*PolyLog(4, -ℯ^(2*(e + f*x))))/(4*f^4) + (3*b^3*d^3*PolyLog(4, -ℯ^(2*(e + f*x))))/(4*f^4) - (3*b^3*d*(c + d*x)^2*tanh(e + f*x))/(2*f^2) - (3*a*b^2*(c + d*x)^3*tanh(e + f*x))/f - (b^3*(c + d*x)^3*tanh(e + f*x)^2)/(2*f)]
    @test_int [(c + d*x)^2*(a + b*tanh(e + f*x))^3, x, 22, (b^3*c*d*x)/f + (b^3*d^2*x^2)/(2*f) - (3*a*b^2*(c + d*x)^2)/f + (a^3*(c + d*x)^3)/(3*d) - (a^2*b*(c + d*x)^3)/d + (a*b^2*(c + d*x)^3)/d - (b^3*(c + d*x)^3)/(3*d) + (6*a*b^2*d*(c + d*x)*log(1 + ℯ^(2*(e + f*x))))/f^2 + (3*a^2*b*(c + d*x)^2*log(1 + ℯ^(2*(e + f*x))))/f + (b^3*(c + d*x)^2*log(1 + ℯ^(2*(e + f*x))))/f + (b^3*d^2*log(cosh(e + f*x)))/f^3 + (3*a*b^2*d^2*PolyLog(2, -ℯ^(2*(e + f*x))))/f^3 + (3*a^2*b*d*(c + d*x)*PolyLog(2, -ℯ^(2*(e + f*x))))/f^2 + (b^3*d*(c + d*x)*PolyLog(2, -ℯ^(2*(e + f*x))))/f^2 - (3*a^2*b*d^2*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^3) - (b^3*d^2*PolyLog(3, -ℯ^(2*(e + f*x))))/(2*f^3) - (b^3*d*(c + d*x)*tanh(e + f*x))/f^2 - (3*a*b^2*(c + d*x)^2*tanh(e + f*x))/f - (b^3*(c + d*x)^2*tanh(e + f*x)^2)/(2*f)]
    @test_int [(c + d*x)^1*(a + b*tanh(e + f*x))^3, x, 16, 3*a*b^2*c*x + (b^3*d*x)/(2*f) + (3/2)*a*b^2*d*x^2 + (a^3*(c + d*x)^2)/(2*d) - (3*a^2*b*(c + d*x)^2)/(2*d) - (b^3*(c + d*x)^2)/(2*d) + (3*a^2*b*(c + d*x)*log(1 + ℯ^(2*(e + f*x))))/f + (b^3*(c + d*x)*log(1 + ℯ^(2*(e + f*x))))/f + (3*a*b^2*d*log(cosh(e + f*x)))/f^2 + (3*a^2*b*d*PolyLog(2, -ℯ^(2*(e + f*x))))/(2*f^2) + (b^3*d*PolyLog(2, -ℯ^(2*(e + f*x))))/(2*f^2) - (b^3*d*tanh(e + f*x))/(2*f^2) - (3*a*b^2*(c + d*x)*tanh(e + f*x))/f - (b^3*(c + d*x)*tanh(e + f*x)^2)/(2*f)]
    @test_int [(a + b*tanh(e + f*x))^3/(c + d*x)^1, x, 0, Unintegrable((a + b*tanh(e + f*x))^3/(c + d*x), x)]
    @test_int [(a + b*tanh(e + f*x))^3/(c + d*x)^2, x, 0, Unintegrable((a + b*tanh(e + f*x))^3/(c + d*x)^2, x)]

    #= ::Subsubsection::Closed:: =#
    #=n<0=#

    @test_int [(c + d*x)^3/(a + b*tanh(e + f*x)), x, 6, (c + d*x)^4/(4*(a + b)*d) - (b*(c + d*x)^3*log(1 + (a - b)/(ℯ^(2*(e + f*x))*(a + b))))/((a^2 - b^2)*f) + (3*b*d*(c + d*x)^2*PolyLog(2, -((a - b)/(ℯ^(2*(e + f*x))*(a + b)))))/(2*(a^2 - b^2)*f^2) + (3*b*d^2*(c + d*x)*PolyLog(3, -((a - b)/(ℯ^(2*(e + f*x))*(a + b)))))/(2*(a^2 - b^2)*f^3) + (3*b*d^3*PolyLog(4, -((a - b)/(ℯ^(2*(e + f*x))*(a + b)))))/(4*(a^2 - b^2)*f^4)]
    @test_int [(c + d*x)^2/(a + b*tanh(e + f*x)), x, 5, (c + d*x)^3/(3*(a + b)*d) - (b*(c + d*x)^2*log(1 + (a - b)/(ℯ^(2*(e + f*x))*(a + b))))/((a^2 - b^2)*f) + (b*d*(c + d*x)*PolyLog(2, -((a - b)/(ℯ^(2*(e + f*x))*(a + b)))))/((a^2 - b^2)*f^2) + (b*d^2*PolyLog(3, -((a - b)/(ℯ^(2*(e + f*x))*(a + b)))))/(2*(a^2 - b^2)*f^3)]
    @test_int [(c + d*x)^1/(a + b*tanh(e + f*x)), x, 4, (c + d*x)^2/(2*(a + b)*d) - (b*(c + d*x)*log(1 + (a - b)/(ℯ^(2*(e + f*x))*(a + b))))/((a^2 - b^2)*f) + (b*d*PolyLog(2, -((a - b)/(ℯ^(2*(e + f*x))*(a + b)))))/(2*(a^2 - b^2)*f^2)]
    @test_int [1/((c + d*x)^1*(a + b*tanh(e + f*x))), x, 0, Unintegrable(1/((c + d*x)*(a + b*tanh(e + f*x))), x)]
    @test_int [1/((c + d*x)^2*(a + b*tanh(e + f*x))), x, 0, Unintegrable(1/((c + d*x)^2*(a + b*tanh(e + f*x))), x)]

    @test_int [(c + d*x)^3/(a + b*tanh(e + f*x))^2, x, 28, -((2*b^2*(c + d*x)^3)/((a^2 - b^2)^2*f)) + (2*b^2*(c + d*x)^3)/((a - b)*(a + b)^2*(a - b + (a + b)*ℯ^(2*e + 2*f*x))*f) + (c + d*x)^4/(4*(a - b)^2*d) + (3*b^2*d*(c + d*x)^2*log(1 + ((a + b)*ℯ^(2*e + 2*f*x))/(a - b)))/((a^2 - b^2)^2*f^2) - (2*b*(c + d*x)^3*log(1 + ((a + b)*ℯ^(2*e + 2*f*x))/(a - b)))/((a - b)^2*(a + b)*f) + (2*b^2*(c + d*x)^3*log(1 + ((a + b)*ℯ^(2*e + 2*f*x))/(a - b)))/((a^2 - b^2)^2*f) + (3*b^2*d^2*(c + d*x)*PolyLog(2, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/((a^2 - b^2)^2*f^3) - (3*b*d*(c + d*x)^2*PolyLog(2, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/((a - b)^2*(a + b)*f^2) + (3*b^2*d*(c + d*x)^2*PolyLog(2, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/((a^2 - b^2)^2*f^2) - (3*b^2*d^3*PolyLog(3, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/(2*(a^2 - b^2)^2*f^4) + (3*b*d^2*(c + d*x)*PolyLog(3, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/((a - b)^2*(a + b)*f^3) - (3*b^2*d^2*(c + d*x)*PolyLog(3, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/((a^2 - b^2)^2*f^3) - (3*b*d^3*PolyLog(4, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/(2*(a - b)^2*(a + b)*f^4) + (3*b^2*d^3*PolyLog(4, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/(2*(a^2 - b^2)^2*f^4)]
    @test_int [(c + d*x)^2/(a + b*tanh(e + f*x))^2, x, 24, -((2*b^2*(c + d*x)^2)/((a^2 - b^2)^2*f)) + (2*b^2*(c + d*x)^2)/((a - b)*(a + b)^2*(a - b + (a + b)*ℯ^(2*e + 2*f*x))*f) + (c + d*x)^3/(3*(a - b)^2*d) + (2*b^2*d*(c + d*x)*log(1 + ((a + b)*ℯ^(2*e + 2*f*x))/(a - b)))/((a^2 - b^2)^2*f^2) - (2*b*(c + d*x)^2*log(1 + ((a + b)*ℯ^(2*e + 2*f*x))/(a - b)))/((a - b)^2*(a + b)*f) + (2*b^2*(c + d*x)^2*log(1 + ((a + b)*ℯ^(2*e + 2*f*x))/(a - b)))/((a^2 - b^2)^2*f) + (b^2*d^2*PolyLog(2, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/((a^2 - b^2)^2*f^3) - (2*b*d*(c + d*x)*PolyLog(2, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/((a - b)^2*(a + b)*f^2) + (2*b^2*d*(c + d*x)*PolyLog(2, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/((a^2 - b^2)^2*f^2) + (b*d^2*PolyLog(3, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/((a - b)^2*(a + b)*f^3) - (b^2*d^2*PolyLog(3, -(((a + b)*ℯ^(2*e + 2*f*x))/(a - b))))/((a^2 - b^2)^2*f^3)]
    @test_int [(c + d*x)^1/(a + b*tanh(e + f*x))^2, x, 5, -((c + d*x)^2/(2*(a^2 - b^2)*d)) + (b*d - 2*a*c*f - 2*a*d*f*x)^2/(4*a*(a - b)*(a + b)^2*d*f^2) + (b*(b*d - 2*a*c*f - 2*a*d*f*x)*log(1 + (a - b)/(ℯ^(2*(e + f*x))*(a + b))))/((a^2 - b^2)^2*f^2) + (a*b*d*PolyLog(2, -((a - b)/(ℯ^(2*(e + f*x))*(a + b)))))/((a^2 - b^2)^2*f^2) + (b*(c + d*x))/((a^2 - b^2)*f*(a + b*tanh(e + f*x)))]
    @test_int [1/((c + d*x)^1*(a + b*tanh(e + f*x))^2), x, 0, Unintegrable(1/((c + d*x)*(a + b*tanh(e + f*x))^2), x)]
    @test_int [1/((c + d*x)^2*(a + b*tanh(e + f*x))^2), x, 0, Unintegrable(1/((c + d*x)^2*(a + b*tanh(e + f*x))^2), x)]
    end

max_stars_repo_path: test/6 Hyperbolic functions/6.3 Hyperbolic tangent/6.3.1 (c+d x)^m (a+b tanh)^n.jl


text:
# ------------------------------------------------
# -------------------- Legend --------------------
# ------------------------------------------------

MakieLayout.Legend(figpos, aog::Union{Layer,Layers}) =
    Legend(figpos, Entries(aog))

function MakieLayout.Legend(figpos, entries::Entries)
    legend = _Legend_(entries)
    isnothing(legend) && return

    if figpos isa FigureGrid
        figpos_new = figpos.figure[:,end + 1]
    else
        figpos_new = figpos
    end

    Legend(figpos_new, legend...)
end

function _Legend_(entries)
    named_scales = entries.scales.named
    named_labels = copy(entries.labels.named)

    # remove keywords that don't support legends
	for key in [:row, :col, :layout, :stack, :dodge, :group]
		pop!(named_labels, key, nothing)
	end
	for (key, val) in named_scales
		val isa ContinuousScale && pop!(named_labels, key, nothing)
	end

    # if no legend-worthy keyword remains return nothing
    isempty(named_labels) && return nothing

	attr_dict = mapreduce((a, b) -> mergewith!(union, a, b), entries.entries) do entry
		# FIXME: this should probably use the rescaled values
		defaultplottype = AbstractPlotting.plottype(entry.mappings.positional...)
		plottype = AbstractPlotting.plottype(entry.plottype, defaultplottype)
		attrs = keys(entry.mappings.named)
		return LittleDict{PlotFunc, Vector{Symbol}}(plottype => collect(attrs))
    end

	titles = unique!(collect(String, values(named_labels)))
	# empty strings create difficulties with the layout
	nonemptytitles = map(t -> isempty(t) ? " " : t, titles)

	labels_list = Vector{String}[]
	elements_list = Vector{Vector{LegendElement}}[]

	for title in titles
		label_attrs = [key for (key, val) in named_labels if val == title]
		first_scale = named_scales[first(label_attrs)]
		labels = map(string, first_scale.data)
		plottypes = [P => attrs ∩ label_attrs for (P, attrs) in pairs(attr_dict)]
		filter!(t -> !isempty(last(t)), plottypes)
		elements = map(eachindex(first_scale.data)) do idx
			local elements = LegendElement[]
			for (P, attrs) in plottypes
				options = [attr => named_scales[attr].plot[idx] for attr in attrs]
				append!(elements, legend_elements(P; options...))
			end
			return elements
		end
		push!(labels_list, labels)
		push!(elements_list, elements)
	end
	return elements_list, labels_list, nonemptytitles
end

# ------------------------------------------------
# ----- LegendElements with more defaults --------
# ------------------------------------------------

function from_default_theme(attr)
    theme = default_styles()
    return get(theme, attr) do
        AbstractPlotting.current_default_theme()[attr]
    end
end

line_element(;
             color=from_default_theme(:color),
             linestyle=from_default_theme(:linestyle),
             linewidth=from_default_theme(:linewidth),
             kwargs...) =
    LineElement(; color, linestyle, linewidth, kwargs...)

marker_element(;
               color=from_default_theme(:color),
               marker=from_default_theme(:marker),
               strokecolor=from_default_theme(:strokecolor),
               markerpoints=[Point2f0(0.5, 0.5)],
               kwargs...) =
    MarkerElement( ; color, marker, strokecolor, markerpoints, kwargs...)

poly_element(;
             color=from_default_theme(:color),
             strokecolor=:transparent,
             kwargs...) =
    PolyElement(; color, strokecolor, kwargs...)

legend_elements(::Type{Scatter}; kwargs...) = [marker_element(; kwargs...)]
legend_elements(::Type{Lines}; kwargs...) = [line_element(; kwargs...)]
legend_elements(::Type{Contour}; kwargs...) = [line_element(; kwargs...)]

function legend_elements(::Type{LinesFill}; color=from_default_theme(:color), fillalpha=0.15, kwargs...)
	meshcolor = to_color((color, fillalpha))
	return [poly_element(; color=meshcolor, kwargs...), line_element(; color, kwargs...)]
end

legend_elements(::Any; linewidth=0, strokecolor=:transparent, kwargs...) =
	[poly_element(; linewidth, kwargs...)]

#Notes

# TODO: correctly handle composite plot types (now fall back to poly)
# TODO: check that all scales for the same label agree on the data
# TODO: make legend updateable?
# TODO: allow custom attributes in legend elements?
# TODO: avoid recomputing `Entries`

# WIP colorbar implementation

# function _legend(P, attribute, scale::ContinuousScale, title)
#     extrema = scale.extrema
#     # @unpack f, extrema = scale
#     n_ticks = 4

#     ticks = MakieLayout.locateticks(extrema..., n_ticks)

#     label_kw = [(label = L(tick), kw = KW(attribute, tick)) for tick in ticks]

#     (; title, P, label_kw)
# end


text:
################
# Webhook type #
################

mutable struct Webhook <: GitHubType
    id::Union{Int, Nothing}
    url::Union{HTTP.URI, Nothing}
    test_url::Union{HTTP.URI, Nothing}
    ping_url::Union{HTTP.URI, Nothing}
    name::Union{String, Nothing}
    events::Union{Array{String}, Nothing}
    active::Union{Bool, Nothing}
    config::Union{Dict{String, String}, Nothing}
    updated_at::Union{Dates.DateTime, Nothing}
    created_at::Union{Dates.DateTime, Nothing}
end

Webhook(data::Dict) = json2github(Webhook, data)
Webhook(id::Real) = Webhook(Dict("id" => id))
namefield(hook::Webhook) = hook.id

###############
# API Methods #
###############

@api_default function create_webhook(api::GitHubAPI, owner, repo; options...)
    result = gh_post_json(api, "/repos/$(name(owner))/$(name(repo))/hooks"; options...)
    return Webhook(result)
end


text:
abstract type AbstractVisualizer end

# Interface methods
function setobject! end
function settransform! end
function delete! end
function setprop! end
function setcontrol! end

# Convenient shortcuts for creating new objects
setobject!(vis::AbstractVisualizer, geom::GeometryLike) = setobject!(vis, Object(geom))
setobject!(vis::AbstractVisualizer, geom::GeometryLike, material::AbstractMaterial) = setobject!(vis, Object(geom, material))


text:
import DataStructures

include("XTbMl.jl")

if Int === Int64
    struct MortalityTable
        select::Array{Union{Missing, Float64},2}
        ultimate::Array{Union{Missing, Float64},1}
    end
else
    struct MortalityTable
        select::Array{Union{Missing, Float32},2}
        ultimate::Array{Union{Missing, Float32},1}
    end
end

"""
    qx(table::XTbMLTable, issueAge, duration)

Given a mortality table, an issue age, and a duration, returns the appropriate select or ultimate qx.
"""
function qx(table::XTbMLTable, issueAge::Int, duration::Int)
    if length(table.select) > 0
        s = table.select[issueAge]
        if ismissing(s)
            q = missing
        else
            q = s[duration]
        end
    else
        q = missing
    end

    if ismissing(q)
        q = table.ultimate[issueAge + duration - 1]
    end
    return q
end

"""
    qx(table::XTbMLTable, age)
Given a mortality table and an age returns the appropriate ultimate qx.
"""
function qx(table::XTbMLTable, age)
    return table.ultimate[age]
end

export Tables,loadXTbMLTable, qx

# Load Available Tables
table_dir = joinpath(dirname(pathof(MortalityTables)), "tables", "SOA")

function Tables()
    tables = Dict()
    for (root, dirs, files) in walkdir(table_dir)
        for file in files

            if basename(file)[end-3:end] == ".xml"
                tbl, name = loadXTbMLTable(joinpath(root,file))
                tables[strip(name)] = XtbML_Table_To_Matrix(tbl) #strip removes leading/trailing whitespace from the name
            end
        end
    end
    return tables
end

function XtbML_Table_To_Matrix(tbl::XTbMLTable)
    return MortalityTable([qx(tbl,issue_age,dur) for issue_age=0:99,dur=1:121],[qx(tbl,age) for age=0:120])
end

### TABLE STRUCUTRE PARSING ###
# extract table varies-by characteristics using a regex rule

cso_vbt_2001 = DataStructures.OrderedDict(
    "set" => r"^(\d*\s\w{0,3})",
    "structure" => r"((Select|Ulitmate).*)(?=-)",
    "risk" => r"(.*)\s-" ,
    "sex" => r"(\w*)\s",
    "smoker" => r"(\w*)\,",
    "basis" => r"(\w*)")

#Given a Dict comprising the name and regex rule, will parse a string into the components
# e.g. "2001 CSO Preferred Select and Ultimate - Male Nonsmoker, ANB" and relevant ruleset
# becomes  ["2001 CSO", "Select and Ultimate", "Preferred", "Male", "Nonsmoker", "ANB"]
# and ["set", "structure", "risk", "sex", "smoker", "basis"]

function nameProcessor(name,rules)
    name = strip(name) # remove leading/trailing spaces
    ident_set = []
    parsed = []
    for (i,rule) in rules
        if occursin(rule,name)
            m = match(rule,name)
            name = strip(replace(name,rule => ""))

            append!(parsed,[strip(m[1])])
            append!(ident_set,[i])
        end
    end
    return ident_set, parsed
end


text:
"""
This uses a cube file to attach friction coefficients to existing models by fitting the data
provided by Gerrits et al. in PHYSICAL REVIEW B 102, 155130 (2020).
"""
module CubeLDFAModel

using DataInterpolations: CubicSpline
using DelimitedFiles: readdlm
using UnitfulAtomic: austrip
using Unitful: @u_str
using NQCBase: PeriodicCell, apply_cell_boundaries!
using NQCModels: NQCModels, FrictionModels, Model

include("cube.jl")

export LDFAModel

"""
    LDFAModel(model::Model, filename, atoms, cell;
              friction_atoms=collect(range(atoms)),
              )

Wrapper for existing models that adds LDFA friction.

This model uses a cube file to evaluate the electron density used to calculate the friction.
This model assumes that the cube file has units of bohr for the grid and cell distances,
but provides the density in ``Å^{-3}``, as is the default in `FHI-aims`.
"""
struct LDFAModel{T,M,S} <: FrictionModels.AdiabaticFrictionModel
    "Model that provides the energies and forces."
    model::M
    "Splines fitted to the numerical LDFA data."
    splines::S
    "Cube file reader."
    cube::Cube{T}
    "Temporary array for storing the electron density."
    ρ::Vector{T}
    "Temporary array for the Wigner-Seitz radii."
    radii::Vector{T}
    "Indices of atoms that should have friction applied."
    friction_atoms::Vector{Int}
    "Periodic cell containing the electron density."
    cell::PeriodicCell{T}
    function LDFAModel(model, splines, cube, ρ, radii, friction_atoms, cell)
        new{eltype(cell),typeof(model),typeof(splines)}(
            model, splines, cube, ρ, radii, friction_atoms, cell)
    end
end

function LDFAModel(model::Model, filename, atoms, cell;
                       friction_atoms=collect(range(atoms)), cell_matching_rtol=1e-3)

    cube = Cube(filename)

    if !isapprox(cell.vectors, cube.cell.vectors, rtol=cell_matching_rtol)
        error("the cube file cell vectors do not match the simulation cell vectors.\n",
              "  Simulation vectors: ", cell.vectors, "\n",
              "  Cube vectors: ", cube.cell.vectors
        )
    end

    ldfa_data, _ = readdlm(joinpath(@__DIR__, "ldfa.txt"), ',', header=true)
    r = ldfa_data[:,1]
    splines = []
    for i in range(atoms)
        η = ldfa_data[:,atoms.numbers[i].+1]
        indices = η .!= ""
        ri = convert(Vector{Float64}, r[indices])
        η = convert(Vector{Float64}, η[indices])
        push!(ri, 10.0) # Ensure it goes through 0.0 for large r.
        push!(η, 0.0)
        push!(splines, CubicSpline(η, ri))
    end

    ρ = zeros(length(atoms))
    radii = zero(ρ)

    LDFAModel(model, splines, cube, ρ, radii, friction_atoms, cell)
end

NQCModels.ndofs(model::LDFAModel) = NQCModels.ndofs(model.model)

function NQCModels.potential(model::LDFAModel, R::AbstractMatrix)
    NQCModels.potential(model.model, R)
end

function NQCModels.derivative!(model::LDFAModel, D::AbstractMatrix, R::AbstractMatrix)
    NQCModels.derivative!(model.model, D, R)
end

function density!(model::LDFAModel, ρ::AbstractVector, R::AbstractMatrix)
    for i in model.friction_atoms
        r = R[:,i]
        apply_cell_boundaries!(model.cube.cell, r)
        ρ[i] = austrip(model.cube(r + model.cube.origin) * u"Å^-3")
    end
end

function FrictionModels.friction!(model::LDFAModel, F::AbstractMatrix, R::AbstractMatrix)
    density!(model, model.ρ, R)
    clamp!(model.ρ, 0, Inf)
    @. model.radii = 1 / cbrt(4/3 * π * model.ρ)
    DoFs = size(R, 1)
    for i in model.friction_atoms
        η = model.radii[i] < 10 ? model.splines[i](model.radii[i]) : 0.0
        for j in axes(R, 1)
            F[(i-1)*DoFs+j, (i-1)*DoFs+j] = η
        end
    end
    return F
end

end