text:
using HTTP: HTTP

const guideline_registry_consistency_tests_pass = Guideline(;
    info="Registy consistency tests",
    docs=nothing,
    check=data ->
        meets_registry_consistency_tests_pass(data.registry_head, data.registry_deps),
)

function meets_registry_consistency_tests_pass(
    registry_head::String, registry_deps::Vector{String}
)
    try
        RegistryCI.test(registry_head; registry_deps=registry_deps)
        return true, ""
    catch ex
        @error "" exception = (ex, catch_backtrace())
    end
    return false, "The registry consistency tests failed"
end

const guideline_compat_for_julia = Guideline(;
    info="Compat with upper bound for julia",
    docs=string(
        "There is an upper-bounded `[compat]` entry for `julia` that ",
        "only includes a finite number of breaking releases of Julia.",
    ),
    check=data -> meets_compat_for_julia(data.registry_head, data.pkg, data.version),
)

function meets_compat_for_julia(working_directory::AbstractString, pkg, version)
    package_relpath = get_package_relpath_in_registry(;
        package_name=pkg, registry_path=working_directory
    )
    compat = Pkg.TOML.parsefile(joinpath(working_directory, package_relpath, "Compat.toml"))
    # Go through all the compat entries looking for the julia compat
    # of the new version. When found, test
    # 1. that it is a bounded range,
    # 2. that the upper bound is not 2 or higher,
    # 3. that the range includes at least one 1.x version.
    for version_range in keys(compat)
        if version in Pkg.Types.VersionRange(version_range)
            if haskey(compat[version_range], "julia")
                julia_compat = Pkg.Types.VersionSpec(compat[version_range]["julia"])
                if !isempty(
                    intersect(
                        julia_compat, Pkg.Types.VersionSpec("$(typemax(Base.VInt))-*")
                    ),
                )
                    return false, "The compat entry for `julia` is unbounded."
                elseif !isempty(intersect(julia_compat, Pkg.Types.VersionSpec("2-*")))
                    return false,
                    "The compat entry for `julia` has an upper bound of 2 or higher."
                elseif isempty(intersect(julia_compat, Pkg.Types.VersionSpec("1")))
                    # For completeness, although this seems rather
                    # unlikely to occur.
                    return false,
                    "The compat entry for `julia` doesn't include any 1.x version."
                else
                    return true, ""
                end
            end
        end
    end

    return false, "There is no compat entry for `julia`."
end

const guideline_compat_for_all_deps = Guideline(;
    info="Compat (with upper bound) for all dependencies",
    docs=string(
        "Dependencies: All dependencies should have `[compat]` entries that ",
        "are upper-bounded and only include a finite number of breaking releases. ",
        "For more information, please see the \"Upper bounded `[compat]` entries\" subsection under \"Additional information\" below.",
    ),
    check=data -> meets_compat_for_all_deps(data.registry_head, data.pkg, data.version),
)

function meets_compat_for_all_deps(working_directory::AbstractString, pkg, version)
    package_relpath = get_package_relpath_in_registry(;
        package_name=pkg, registry_path=working_directory
    )
    deps = Pkg.TOML.parsefile(joinpath(working_directory, package_relpath, "Deps.toml"))
    compat = Pkg.TOML.parsefile(joinpath(working_directory, package_relpath, "Compat.toml"))
    # First, we construct a Dict in which the keys are the package's
    # dependencies, and the value is always false.
    dep_has_compat_with_upper_bound = Dict{String,Bool}()
    for version_range in keys(deps)
        if version in Pkg.Types.VersionRange(version_range)
            for name in keys(deps[version_range])
                if !is_jll_name(name) && !is_julia_stdlib(name)
                    @debug("Found a new (non-stdlib non-JLL) dependency: $(name)")
                    dep_has_compat_with_upper_bound[name] = false
                end
            end
        end
    end
    # Now, we go through all the compat entries. If a dependency has a compat
    # entry with an upper bound, we change the corresponding value in the Dict
    # to true.
    for version_range in keys(compat)
        if version in Pkg.Types.VersionRange(version_range)
            for (name, value) in compat[version_range]
                if value isa Vector
                    if !isempty(value)
                        value_ranges = Pkg.Types.VersionRange.(value)
                        each_range_has_upper_bound = _has_upper_bound.(value_ranges)
                        if all(each_range_has_upper_bound)
                            @debug(
                                "Dependency \"$(name)\" has compat entries that all have upper bounds"
                            )
                            dep_has_compat_with_upper_bound[name] = true
                        end
                    end
                else
                    value_range = Pkg.Types.VersionRange(value)
                    if _has_upper_bound(value_range)
                        @debug(
                            "Dependency \"$(name)\" has a compat entry with an upper bound"
                        )
                        dep_has_compat_with_upper_bound[name] = true
                    end
                end
            end
        end
    end
    meets_this_guideline = all(values(dep_has_compat_with_upper_bound))
    if meets_this_guideline
        return true, ""
    else
        bad_dependencies = Vector{String}()
        for name in keys(dep_has_compat_with_upper_bound)
            if !(dep_has_compat_with_upper_bound[name])
                @error(
                    "Dependency \"$(name)\" does not have a compat entry that has an upper bound"
                )
                push!(bad_dependencies, name)
            end
        end
        sort!(bad_dependencies)
        message = string(
            "The following dependencies do not have a `[compat]` entry ",
            "that is upper-bounded and only includes a finite number ",
            "of breaking releases: ",
            join(bad_dependencies, ", "),
        )
        return false, message
    end
end

const guideline_patch_release_does_not_narrow_julia_compat = Guideline(;
    info="If it is a patch release, then it does not narrow the `[compat]` range for `julia`.",
    check=data -> meets_patch_release_does_not_narrow_julia_compat(
        data.pkg,
        data.version;
        registry_head=data.registry_head,
        registry_master=data.registry_master,
    ),
)

function meets_patch_release_does_not_narrow_julia_compat(
    pkg::String, new_version::VersionNumber; registry_head::String, registry_master::String
)
    old_version = latest_version(pkg, registry_master)
    if old_version.major != new_version.major || old_version.minor != new_version.minor
        # Not a patch release.
        return true, ""
    end
    julia_compats_for_old_version = julia_compat(pkg, old_version, registry_master)
    julia_compats_for_new_version = julia_compat(pkg, new_version, registry_head)
    if Set(julia_compats_for_old_version) == Set(julia_compats_for_new_version)
        return true, ""
    end
    meets_this_guideline = range_did_not_narrow(
        julia_compats_for_old_version, julia_compats_for_new_version
    )
    if meets_this_guideline
        return true, ""
    else
        if (old_version >= v"1") || (new_version >= v"1")
            msg = string(
                "A patch release is not allowed to narrow the ",
                "supported ranges of Julia versions. ",
                "The ranges have changed from ",
                "$(julia_compats_for_old_version) ",
                "(in $(old_version)) ",
                "to $(julia_compats_for_new_version) ",
                "(in $(new_version)).",
            )
            return false, msg
        else
            @info("Narrows Julia compat, but it's OK since package is pre-1.0")
            return true, ""
        end
    end
end

const _AUTOMERGE_NEW_PACKAGE_MINIMUM_NAME_LENGTH = 5

const guideline_name_length = Guideline(;
    info="Name not too short",
    docs="The name is at least $(_AUTOMERGE_NEW_PACKAGE_MINIMUM_NAME_LENGTH) characters long.",
    check=data -> meets_name_length(data.pkg),
)

function meets_name_length(pkg)
    meets_this_guideline = length(pkg) >= _AUTOMERGE_NEW_PACKAGE_MINIMUM_NAME_LENGTH
    if meets_this_guideline
        return true, ""
    else
        return false,
        "Name is not at least $(_AUTOMERGE_NEW_PACKAGE_MINIMUM_NAME_LENGTH) characters long"
    end
end

const guideline_name_ascii = Guideline(;
    info="Name is composed of ASCII characters only.",
    check=data -> meets_name_ascii(data.pkg),
)

function meets_name_ascii(pkg)
    if isascii(pkg)
        return true, ""
    else
        return false, "Name is not ASCII"
    end
end

const guideline_julia_name_check = Guideline(;
    info="Name does not include \"julia\" or start with \"Ju\".",
    check=data -> meets_julia_name_check(data.pkg),
)

function meets_julia_name_check(pkg)
    if occursin("julia", lowercase(pkg))
        return false,
        "Lowercase package name $(lowercase(pkg)) contains the string \"julia\"."
    elseif startswith(pkg, "Ju")
        return false, "Package name starts with \"Ju\"."
    else
        return true, ""
    end
end

damerau_levenshtein(name1, name2) = StringDistances.DamerauLevenshtein()(name1, name2)
function sqrt_normalized_vd(name1, name2)
    return VisualStringDistances.visual_distance(name1, name2; normalize=x -> 5 + sqrt(x))
end

const guideline_distance_check = Guideline(;
    info="Name is not too similar to existing package names",
    docs="""
To prevent confusion between similarly named packages, the names of new packages must also satisfy the following three checks: (for more information, please see the \"Name similarity distance check\" subsection under \"Additional information\" below)
    - the [Damerau–Levenshtein
      distance](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance)
      between the package name and the name of any existing package must be at
      least 3.
    - the Damerau–Levenshtein distance between the lowercased version of a
      package name and the lowercased version of the name of any existing
      package must be at least 2.
    - and a visual distance from
      [VisualStringDistances.jl](https://github.com/ericphanson/VisualStringDistances.jl)
      between the package name and any existing package must exceeds a certain
      a hand-chosen threshold (currently 2.5).
  """,
    check=data -> meets_distance_check(data.pkg, data.registry_master),
)

function meets_distance_check(
    pkg_name::AbstractString, registry_master::AbstractString; kwargs...
)
    other_packages = get_all_non_jll_package_names(registry_master)
    return meets_distance_check(pkg_name, other_packages; kwargs...)
end

function meets_distance_check(
    pkg_name::AbstractString,
    other_packages::Vector;
    DL_lowercase_cutoff=1,
    DL_cutoff=2,
    sqrt_normalized_vd_cutoff=2.5,
    comment_collapse_cutoff=10,
)
    problem_messages = Tuple{String,Tuple{Float64,Float64,Float64}}[]
    for other_pkg in other_packages
        if pkg_name == other_pkg
            # We short-circuit in this case; more information doesn't help.
            return (false, "Package name already exists in the registry.")
        elseif lowercase(pkg_name) == lowercase(other_pkg)
            # We'll sort this first
            push!(
                problem_messages,
                (
                    "Package name matches existing package name $(other_pkg) up to case.",
                    (0, 0, 0),
                ),
            )
        else
            msg = ""

            # Distance check 1: DL distance
            dl = damerau_levenshtein(pkg_name, other_pkg)
            if dl <= DL_cutoff
                msg = string(
                    msg,
                    " Damerau-Levenshtein distance $dl is at or below cutoff of $(DL_cutoff).",
                )
            end

            # Distance check 2: lowercase DL distance
            dl_lowercase = damerau_levenshtein(lowercase(pkg_name), lowercase(other_pkg))
            if dl_lowercase <= DL_lowercase_cutoff
                msg = string(
                    msg,
                    " Damerau-Levenshtein distance $(dl_lowercase) between lowercased names is at or below cutoff of $(DL_lowercase_cutoff).",
                )
            end

            # Distance check 3: normalized visual distance,
            # gated by a `dl` check for speed.
            if (sqrt_normalized_vd_cutoff > 0 && dl <= 4)
                nrm_vd = sqrt_normalized_vd(pkg_name, other_pkg)
                if nrm_vd <= sqrt_normalized_vd_cutoff
                    msg = string(
                        msg,
                        " Normalized visual distance ",
                        Printf.@sprintf("%.2f", nrm_vd),
                        " is at or below cutoff of ",
                        Printf.@sprintf("%.2f", sqrt_normalized_vd_cutoff),
                        ".",
                    )
                end
            else
                # need to choose something for sorting purposes
                nrm_vd = 10.0
            end

            if msg != ""
                # We must have found a clash.
                push!(
                    problem_messages,
                    (string("Similar to $(other_pkg).", msg), (dl, dl_lowercase, nrm_vd)),
                )
            end
        end
    end

    isempty(problem_messages) && return (true, "")
    sort!(problem_messages; by=Base.tail)
    message = string(
        "Package name similar to $(length(problem_messages)) existing package",
        length(problem_messages) > 1 ? "s" : "",
        ".\n",
    )
    use_spoiler = length(problem_messages) > comment_collapse_cutoff
    # we indent each line by two spaces in all the following
    # so that it nests properly in the outer list.
    if use_spoiler
        message *= """
                     <details>
                     <summary>Similar package names</summary>

                   """
    end
    numbers = string.("  ", 1:length(problem_messages))
    message *= join(join.(zip(numbers, first.(problem_messages)), Ref(". ")), '\n')
    if use_spoiler
        message *= "\n\n  </details>\n"
    end
    return (false, message)
end

const guideline_normal_capitalization = Guideline(;
    info="Normal capitalization",
    docs=string(
        "The package name should start with an upper-case letter, ",
        "contain only ASCII alphanumeric characters, ",
        "and contain at least one lowercase letter.",
    ),
    check=data -> meets_normal_capitalization(data.pkg),
)

function meets_normal_capitalization(pkg)
    meets_this_guideline = occursin(r"^[A-Z]\w*[a-z]\w*[0-9]?$", pkg)
    if meets_this_guideline
        return true, ""
    else
        return false,
        "Name does not meet all of the following: starts with an upper-case letter, ASCII alphanumerics only, not all letters are upper-case."
    end
end

const guideline_repo_url_requirement = Guideline(;
    info="Repo URL ends with `/PackageName.jl.git`.",
    check=data -> meets_repo_url_requirement(data.pkg; registry_head=data.registry_head),
)

function meets_repo_url_requirement(pkg::String; registry_head::String)
    package_relpath = get_package_relpath_in_registry(;
        package_name=pkg, registry_path=registry_head
    )
    package_toml_parsed = Pkg.TOML.parsefile(
        joinpath(registry_head, package_relpath, "Package.toml")
    )

    url = package_toml_parsed["repo"]
    subdir = get(package_toml_parsed, "subdir", "")
    is_subdirectory_package = occursin(r"[A-Za-z0-9]", subdir)
    meets_this_guideline = url_has_correct_ending(url, pkg)

    if is_subdirectory_package
        return true, "" # we do not apply this check if the package is a subdirectory package
    end
    if meets_this_guideline
        return true, ""
    end
    return false, "Repo URL does not end with /name.jl.git, where name is the package name"
end

function _invalid_sequential_version(reason::AbstractString)
    return false, "Does not meet sequential version number guideline: $reason", :invalid
end

function _valid_change(old_version::VersionNumber, new_version::VersionNumber)
    diff = difference(old_version, new_version)
    @debug("Difference between versions: ", old_version, new_version, diff)
    if diff == v"0.0.1"
        return true, "", :patch
    elseif diff == v"0.1.0"
        return true, "", :minor
    elseif diff == v"1.0.0"
        return true, "", :major
    else
        return _invalid_sequential_version("increment is not one of: 0.0.1, 0.1.0, 1.0.0")
    end
end

const guideline_sequential_version_number = Guideline(;
    info="Sequential version number",
    docs=string(
        "Version number: Should be a standard increment and not skip versions. ",
        "This means incrementing the patch/minor/major version with +1 compared to ",
        "previous (if any) releases. ",
        "If, for example, `1.0.0` and `1.1.0` are existing versions, ",
        "valid new versions are `1.0.1`, `1.1.1`, `1.2.0` and `2.0.0`. ",
        "Invalid new versions include `1.0.2` (skips `1.0.1`), ",
        "`1.3.0` (skips `1.2.0`), `3.0.0` (skips `2.0.0`) etc.",
    ),
    check=data -> meets_sequential_version_number(
        data.pkg,
        data.version;
        registry_head=data.registry_head,
        registry_master=data.registry_master,
    ),
)

function meets_sequential_version_number(
    existing::Vector{VersionNumber}, ver::VersionNumber
)
    always_assert(!isempty(existing))
    if ver in existing
        return _invalid_sequential_version("version $ver already exists")
    end
    issorted(existing) || (existing = sort(existing))
    idx = searchsortedlast(existing, ver)
    idx > 0 || return _invalid_sequential_version(
        "version $ver less than least existing version $(existing[1])"
    )
    prv = existing[idx]
    always_assert(ver != prv)
    nxt = if thismajor(ver) != thismajor(prv)
        nextmajor(prv)
    elseif thisminor(ver) != thisminor(prv)
        nextminor(prv)
    else
        nextpatch(prv)
    end
    ver <= nxt || return _invalid_sequential_version("version $ver skips over $nxt")
    return _valid_change(prv, ver)
end

function meets_sequential_version_number(
    pkg::String, new_version::VersionNumber; registry_head::String, registry_master::String
)
    _all_versions = all_versions(pkg, registry_master)
    return meets_sequential_version_number(_all_versions, new_version)
end

const guideline_standard_initial_version_number = Guideline(;
    info="Standard initial version number. Must be one of: `0.0.1`, `0.1.0`, `1.0.0`, or `X.0.0`.",
    check=data -> meets_standard_initial_version_number(data.version),
)

function meets_standard_initial_version_number(version)
    meets_this_guideline =
        version == v"0.0.1" ||
        version == v"0.1.0" ||
        version == v"1.0.0" ||
        _is_x_0_0(version)
    if meets_this_guideline
        return true, ""
    else
        return false, "Version number is not 0.0.1, 0.1.0, 1.0.0, or X.0.0"
    end
end

function _is_x_0_0(version::VersionNumber)
    result = (version.major >= 1) && (version.minor == 0) && (version.patch == 0)
    return result
end

const guideline_version_number_no_prerelease = Guideline(;
    info="No prerelease data in the version number",
    docs = "Version number is not allowed to contain prerelease data",
    check = data -> meets_version_number_no_prerelease(
        data.version,
    ),
)
const guideline_version_number_no_build = Guideline(;
    info="No build data in the version number",
    docs = "Version number is not allowed to contain build data",
    check = data -> meets_version_number_no_build(
        data.version,
    ),
)
function meets_version_number_no_prerelease(version::VersionNumber)
    if isempty(version.prerelease)
        return true, ""
    else
        return false, "Version number is not allowed to contain prerelease data"
    end
end
function meets_version_number_no_build(version::VersionNumber)
    if isempty(version.build)
        return true, ""
    else
        return false, "Version number is not allowed to contain build data"
    end
end

const guideline_code_can_be_downloaded = Guideline(;
    info="Code can be downloaded.",
    check=data -> meets_code_can_be_downloaded(
        data.registry_head,
        data.pkg,
        data.version,
        data.pr;
        pkg_code_path=data.pkg_code_path,
    ),
)

function meets_code_can_be_downloaded(registry_head, pkg, version, pr; pkg_code_path)
    uuid, package_repo, subdir, tree_hash_from_toml = parse_registry_pkg_info(
        registry_head, pkg, version
    )

    # We get the `tree_hash` two ways and check they agree, which helps ensures the `subdir` parameter is correct. Two ways:
    # 1. By the commit hash in the PR body and the subdir parameter
    # 2. By the tree hash in the Versions.toml

    commit_hash = commit_from_pull_request_body(pr)

    local tree_hash_from_commit, tree_hash_from_commit_success
    clone_success = load_files_from_url_and_tree_hash(
        pkg_code_path, package_repo, tree_hash_from_toml
    ) do dir
        tree_hash_from_commit, tree_hash_from_commit_success = try
            readchomp(Cmd(`git rev-parse $(commit_hash):$(subdir)`; dir=dir)), true
        catch e
            @error e
            "", false
        end
    end

    if !clone_success
        return false, "Cloning repository failed."
    end

    if !tree_hash_from_commit_success
        return false,
        "Could not obtain tree hash from commit hash and subdir parameter. Possibly this indicates that an incorrect `subdir` parameter was passed during registration."
    end

    if tree_hash_from_commit != tree_hash_from_toml
        @error "`tree_hash_from_commit != tree_hash_from_toml`" tree_hash_from_commit tree_hash_from_toml
        return false,
        "Tree hash obtained from the commit message and subdirectory does not match the tree hash in the Versions.toml file. Possibly this indicates that an incorrect `subdir` parameter was passed during registration."
    else
        return true, ""
    end
end

function _generate_pkg_add_command(pkg::String, version::VersionNumber)::String
    return "Pkg.add(Pkg.PackageSpec(name=\"$(pkg)\", version=v\"$(string(version))\"));"
end

is_valid_url(str::AbstractString) = !isempty(HTTP.URI(str).scheme) && isvalid(HTTP.URI(str))

const guideline_version_can_be_pkg_added = Guideline(;
    info="Version can be `Pkg.add`ed",
    docs="Package installation: The package should be installable (`Pkg.add(\"PackageName\")`).",
    check=data -> meets_version_can_be_pkg_added(
        data.registry_head, data.pkg, data.version; registry_deps=data.registry_deps
    ),
)

function meets_version_can_be_pkg_added(
    working_directory::String,
    pkg::String,
    version::VersionNumber;
    registry_deps::Vector{<:AbstractString}=String[],
)
    pkg_add_command = _generate_pkg_add_command(pkg, version)
    _registry_deps = convert(Vector{String}, registry_deps)
    _registry_deps_is_valid_url = is_valid_url.(_registry_deps)
    code = """
        import Pkg;
        Pkg.Registry.add(Pkg.RegistrySpec(path=\"$(working_directory)\"));
        _registry_deps = $(_registry_deps);
        _registry_deps_is_valid_url = $(_registry_deps_is_valid_url);
        for i = 1:length(_registry_deps)
            regdep = _registry_deps[i]
            if _registry_deps_is_valid_url[i]
                Pkg.Registry.add(Pkg.RegistrySpec(url = regdep))
            else
                Pkg.Registry.add(regdep)
            end
        end
        @info("Attempting to `Pkg.add` package...");
        $(pkg_add_command)
        @info("Successfully `Pkg.add`ed package");
        """

    cmd_ran_successfully = _run_pkg_commands(
        working_directory,
        pkg,
        version;
        code=code,
        before_message="Attempting to `Pkg.add` the package",
    )
    if cmd_ran_successfully
        @info "Successfully `Pkg.add`ed the package"
        return true, ""
    else
        @error "Was not able to successfully `Pkg.add` the package"
        return false,
        string(
            "I was not able to install the package ",
            "(i.e. `Pkg.add(\"$(pkg)\")` failed). ",
            "See the CI logs for details.",
        )
    end
end

const guideline_version_has_osi_license = Guideline(;
    info="Version has OSI-approved license",
    docs=string(
        "License: The package should have an ",
        "[OSI-approved software license](https://opensource.org/licenses/alphabetical) ",
        "located in the top-level directory of the package code, ",
        "e.g. in a file named `LICENSE` or `LICENSE.md`. ",
        "This check is required for the General registry. ",
        "For other registries, registry maintainers have the option to disable this check.",
    ),
    check=data -> meets_version_has_osi_license(data.pkg; pkg_code_path=data.pkg_code_path),
)

function meets_version_has_osi_license(pkg::String; pkg_code_path)
    pkgdir = pkg_code_path
    if !isdir(pkgdir) || isempty(readdir(pkgdir))
        return false,
        "Could not check license because could not access package code. Perhaps the `can_download_code` check failed earlier."
    end

    license_results = LicenseCheck.find_licenses(pkgdir)

    # Failure mode 1: no licenses
    if isempty(license_results)
        @error "Could not find any licenses"
        return false,
        string(
            "No licenses detected in the package's top-level folder. An OSI-approved license is required.",
        )
    end

    flat_results = [
        (
            filename=lic.license_filename,
            identifier=identifier,
            approved=LicenseCheck.is_osi_approved(identifier),
        ) for lic in license_results for identifier in lic.licenses_found
    ]

    osi_results = [
        string(r.identifier, " license in ", r.filename) for r in flat_results if r.approved
    ]
    non_osi_results = [
        string(r.identifier, " license in ", r.filename) for
        r in flat_results if !r.approved
    ]

    osi_string = string(
        "Found OSI-approved license(s): ", join(osi_results, ", ", ", and "), "."
    )
    non_osi_string = string(
        "Found non-OSI license(s): ", join(non_osi_results, ", ", ", and "), "."
    )

    # Failure mode 2: no OSI-approved licenses, but has some kind of license detected
    if isempty(osi_results)
        @error "Found no OSI-approved licenses" non_osi_string
        return false, string("Found no OSI-approved licenses. ", non_osi_string)
    end

    # Pass: at least one OSI-approved license, possibly other licenses.
    @info "License check passed; results" osi_results non_osi_results
    if !isempty(non_osi_results)
        return true, string(osi_string, " Also ", non_osi_string)
    else
        return true, string(osi_string, " Found no other licenses.")
    end
end

const guideline_version_can_be_imported = Guideline(;
    info="Version can be `import`ed",
    docs="Package loading: The package should be loadable (`import PackageName`).",
    check=data -> meets_version_can_be_imported(
        data.registry_head, data.pkg, data.version; registry_deps=data.registry_deps
    ),
)

function meets_version_can_be_imported(
    working_directory::String,
    pkg::String,
    version::VersionNumber;
    registry_deps::Vector{<:AbstractString}=String[],
)
    pkg_add_command = _generate_pkg_add_command(pkg, version)
    _registry_deps = convert(Vector{String}, registry_deps)
    _registry_deps_is_valid_url = is_valid_url.(_registry_deps)
    code = """
        import Pkg;
        Pkg.Registry.add(Pkg.RegistrySpec(path=\"$(working_directory)\"));
        _registry_deps = $(_registry_deps);
        _registry_deps_is_valid_url = $(_registry_deps_is_valid_url);
        for i = 1:length(_registry_deps)
            regdep = _registry_deps[i]
            if _registry_deps_is_valid_url[i]
                Pkg.Registry.add(Pkg.RegistrySpec(url = regdep))
            else
                Pkg.Registry.add(regdep)
            end
        end
        @info("Attempting to `Pkg.add` package...");
        $(pkg_add_command)
        @info("Successfully `Pkg.add`ed package");
        @info("Attempting to `import` package");
        Pkg.precompile()
        import $(pkg);
        @info("Successfully `import`ed package");
        """

    cmd_ran_successfully = _run_pkg_commands(
        working_directory,
        pkg,
        version;
        code=code,
        before_message="Attempting to `import` the package",
    )

    if cmd_ran_successfully
        @info "Successfully `import`ed the package"
        return true, ""
    else
        @error "Was not able to successfully `import` the package"
        return false,
        string(
            "I was not able to load the package ",
            "(i.e. `import $(pkg)` failed). ",
            "See the CI logs for details.",
        )
    end
end

function _run_pkg_commands(
    working_directory::String, pkg::String, version::VersionNumber; code, before_message
)
    original_directory = pwd()
    tmp_dir_1 = mktempdir()
    atexit(() -> rm(tmp_dir_1; force=true, recursive=true))
    cd(tmp_dir_1)
    # We need to be careful with what environment variables we pass to the child
    # process. For example, we don't want to pass an environment variable containing
    # our GitHub token to the child process. Because if the Julia package that we are
    # testing has malicious code in its __init__() function, it could try to steal
    # our token. So we only pass these environment variables:
    # 1. HTTP_PROXY. If it's set, it is delegated to the child process.
    # 2. HTTPS_PROXY. If it's set, it is delegated to the child process.
    # 3. JULIA_DEPOT_PATH. We set JULIA_DEPOT_PATH to the temporary directory that
    #    we created. This is because we don't want the child process using our
    #    real Julia depot. So we set up a fake depot for the child process to use.
    # 4. JULIA_PKG_SERVER. If it's set, it is delegated to the child process.
    # 5. JULIA_REGISTRYCI_AUTOMERGE. We set JULIA_REGISTRYCI_AUTOMERGE to "true".
    # 6. PATH. If we don't pass PATH, things break. And PATH should not contain any
    #    sensitive information.
    # 7. PYTHON. We set PYTHON to the empty string. This forces any packages that use
    #    PyCall to install their own version of Python instead of using the system
    #    Python.
    # 8. R_HOME. We set R_HOME to "*".
    # 9. HOME. Lots of things need HOME.

    env = Dict(
        "JULIA_DEPOT_PATH" => mktempdir(),
        "JULIA_REGISTRYCI_AUTOMERGE" => "true",
        "PYTHON" => "",
        "R_HOME" => "*",
        "JULIA_PKG_PRECOMPILE_AUTO" => "0",
    )
    for k in ("HOME", "PATH", "HTTP_PROXY", "HTTPS_PROXY", "JULIA_PKG_SERVER")
        if haskey(ENV, k)
            env[k] = ENV[k]
        end
    end

    cmd = Cmd(`$(Base.julia_cmd()) -e $(code)`; env=env)

    # GUI toolkits may need a display just to load the package
    xvfb = Sys.which("xvfb-run")
    @info("xvfb: ", xvfb)
    if xvfb !== nothing
        pushfirst!(cmd.exec, "-a")
        pushfirst!(cmd.exec, xvfb)
    end
    @info(before_message)
    cmd_ran_successfully = success(pipeline(cmd; stdout=stdout, stderr=stderr))
    cd(original_directory)

    rmdir(tmp_dir_1)

    return cmd_ran_successfully
end

function rmdir(dir)
    try
        chmod(dir, 0o700; recursive=true)
    catch
    end
    return rm(dir; force=true, recursive=true)
end

url_has_correct_ending(url, pkg) = endswith(url, "/$(pkg).jl.git")

function get_automerge_guidelines(
    ::NewPackage;
    check_license::Bool,
    this_is_jll_package::Bool,
    this_pr_can_use_special_jll_exceptions::Bool,
)
    guidelines = [
        (guideline_registry_consistency_tests_pass, true),
        (guideline_pr_only_changes_allowed_files, true),
        # (guideline_only_changes_specified_package, true), # not yet implemented
        (guideline_normal_capitalization, !this_pr_can_use_special_jll_exceptions),
        (guideline_name_length, !this_pr_can_use_special_jll_exceptions),
        (guideline_julia_name_check, true),
        (guideline_repo_url_requirement, true),
        (guideline_version_number_no_prerelease, true),
        (guideline_version_number_no_build, !this_pr_can_use_special_jll_exceptions),
        (guideline_compat_for_julia, true),
        (guideline_compat_for_all_deps, true),
        (guideline_allowed_jll_nonrecursive_dependencies, this_is_jll_package),
        (guideline_name_ascii, true),
        (:update_status, true),
        (guideline_version_can_be_pkg_added, true),
        (guideline_code_can_be_downloaded, true),
        # `guideline_version_has_osi_license` must be run
        # after `guideline_code_can_be_downloaded` so
        # that it can use the downloaded code!
        (guideline_version_has_osi_license, check_license),
        (guideline_version_can_be_imported, true),
        (:update_status, true),
        (guideline_dependency_confusion, true),
        # We always run the `guideline_distance_check`
        # check last, because if the check fails, it
        # prints the list of similar package names in
        # the automerge comment. To make the comment easy
        # to read, we want this list to be at the end.
        (guideline_distance_check, true),
    ]
    return guidelines
end

function get_automerge_guidelines(
    ::NewVersion;
    check_license::Bool,
    this_is_jll_package::Bool,
    this_pr_can_use_special_jll_exceptions::Bool,
)
    guidelines = [
        (guideline_registry_consistency_tests_pass, true),
        (guideline_pr_only_changes_allowed_files, true),
        (guideline_sequential_version_number, !this_pr_can_use_special_jll_exceptions),
        (guideline_version_number_no_prerelease, true),
        (guideline_version_number_no_build, !this_pr_can_use_special_jll_exceptions),
        (guideline_compat_for_julia, true),
        (guideline_compat_for_all_deps, true),
        (
            guideline_patch_release_does_not_narrow_julia_compat,
            !this_pr_can_use_special_jll_exceptions,
        ),
        (guideline_allowed_jll_nonrecursive_dependencies, this_is_jll_package),
        (:update_status, true),
        (guideline_version_can_be_pkg_added, true),
        (guideline_code_can_be_downloaded, true),
        # `guideline_version_has_osi_license` must be run
        # after `guideline_code_can_be_downloaded` so
        # that it can use the downloaded code!
        (guideline_version_has_osi_license, check_license),
        (guideline_version_can_be_imported, true),
    ]
    return guidelines
end


text:
using Sundials

## f2 routine. Compute function f(x,y).

function f(x, y, ydot)
    ydot[1] = y[2]
    ydot[2] = -2.0*y[2]^2 / y[1]
end
x = linspace(0.0, 1.0, 11)
@show Sundials.cvode(f, [1.0, 1.0], x)

ext: jl


text:
#

module EPSphasor

include("ModelController.jl")
include("ModelMechanic.jl")
include("ModelElectric.jl")

using Modia
using Modia.StaticArrays

#**************************************************************************
#------------------

function MultiJ(A::SVector{2})::SVector{2}
    # return = A * J
    return [-A[2], A[1]]
end

#------------------

function Betrag(A::SVector{2}):: Float64
    return sqrt(A[1]*A[1]+A[2]*A[2])
end

function Betrag(A::Vector{Float64}):: Float64
    return sqrt(A[1]*A[1]+A[2]*A[2])
end

#------------------TODO ???

function MultiAconjB(A::SVector{2}, B::Vector{Float64})::SVector{2}
    # ret = A * conj(B)
    return [A[1]*B[1]+A[2]*B[2], -A[1]*B[2]+A[2]*B[1]]
end

function MultiAconjB(A::Vector{Float64}, B::SVector{2})::SVector{2}
    # ret = A * conj(B)
    return [A[1]*B[1]+A[2]*B[2], -A[1]*B[2]+A[2]*B[1]]
end

function MultiAconjB(A::SVector{2}, B::SVector{2})::SVector{2}
    # ret = A * conj(B)
    return [A[1]*B[1]+A[2]*B[2], -A[1]*B[2]+A[2]*B[1]]
end

#------------------TODO ???

function MultiAB(A::SVector{2}, B::Vector{Float64})::SVector{2}
    # ret = A * B
    return [A[1]*B[1]-A[2]*B[2], A[1]*B[2]+A[2]*B[1]]
end

function MultiAB(A::SVector{2}, B::SVector{2})::SVector{2}
    # ret = A * B
    return [A[1]*B[1]-A[2]*B[2], A[1]*B[2]+A[2]*B[1]]
end

function MultiAB(A::Vector{Float64}, B::Vector{Float64})
    # ret = A * B
    return [A[1]*B[1]-A[2]*B[2], A[1]*B[2]+A[2]*B[1]]
end

#------------------

PinVector = Model(
    v = Var(potential=true, start= SVector{2}(0.0, 0.0)),
    i = Var(flow=true, start= SVector{2}(0.0, 0.0)),
)

OnePin = Model(
    p = PinVector,
    Omegarated=outer,
    wReference=outer,
    amps = Var(start=SVector{2}(0.0, 0.02)),
    volts = Var(start=SVector{2}(1.0, 0.01)),

    equations = :[
        volts = p.v
        amps = p.i
    ]
)

TwoPin = Model(
    p = PinVector,
    n = PinVector,
    Omegarated=outer,
    wReference=outer,
    volts = Var(start=SVector{2}(0.0, 0.0)),
    amps = Var(start=SVector{2}(0.0, 0.0)),
    Zero2 = parameter | SVector{2}(0.0, 0.0),

    equations = :[
        volts = p.v - n.v
        p.i + n.i = Zero2
        amps = p.i
    ],
)

Resistor = TwoPin | Model(
    r = 1.11,
    equations = :[
        volts = r*amps
    ]
)

GroundVec = OnePin | Model(
    equations = :[
        volts =  [0.0, 0.0]
    ]
)

VoltageSource = TwoPin | Model(
    V = input | Map(start=1.0),
    w=Var(),
    rad=Var(start=0.0),
    equations = :[
        w=Omegarated*(1.0-wReference)
        w=der(rad)
        volts = V*[cos(rad), sin(rad)]
    ]
)

#=  MODELICA
  volts = x * (Complex(der(ix.re), der(ix.im)) / Omegarated + J * wReference * ix);
  volts = iparallel * rparallel;
  amps = iparallel + ix;

initial equation
  if SteadyState then
      der(ix.re)/Omegarated  + (wStart - wReference) * ix.im  = 0.0;
      der(ix.im)/Omegarated  - (wStart - wReference) * ix.re  = 0.0;
   end if;
=#

Reactor = TwoPin | Model(
    x = 0.1,
    rparallel = parameter | Map(value=:(1000.0*x)), #TODO  Angenommen - für Übertragunsfunktion stabiler !!
    ix = Var(start=SVector{2}(0.0, 0.0)),
    equations = :[
        volts = x* (derIX + wReference * EPSphasor.MultiJ(ix) )
        volts = rparallel * ir
        amps = ix + ir
        derIX = der(ix)/Omegarated
        derINI1 = derIX[1] + (1.0 - wReference) * ix[2]
        derINI2 = derIX[2] - (1.0 - wReference) * ix[1]
    ]
)

#=  MODELICA
equation
  amps * b = Complex(der(volts.re), der(volts.im)) / localPARA.Omegarated + J * wReference * volts;

initial equation
  if SteadyState then
    der(volts.re)  + (localPARA.wStart - wReference) * volts.im * localPARA.Omegarated = 0.0;
    der(volts.im)  - (localPARA.wStart - wReference) * volts.re * localPARA.Omegarated = 0.0;
  end if;
=#

Capacitor = TwoPin | Model(
    b = 3.0,
    equations = :[
        amps*b = derV + wReference*EPSphasor.MultiJ(volts)
        derV = der(volts)/Omegarated
        derINI1 = derV[1] + (1.0-wReference) * volts[2]
        derINI2 = derV[2] - (1.0-wReference) * volts[1]
    ]
)

Meter = TwoPin | Model(
    Srated = 100.0e6,
    Vrated = 15.0e3,
    Irated = parameter | Map(value=:(Srated/Vrated/sqrt(3.0))),
    power = Var(start=SVector{2}(0.0, 0.0)),
    equations = :[
        volts =  Zero2
        current = EPSphasor.Betrag(amps)
        voltage = EPSphasor.Betrag(p.v)
        power = EPSphasor.MultiAconjB(p.v, amps)
        powerRe = power[1]
        powerIm = power[2]
        Voltage = voltage * Vrated
        Current = current * Irated
        PQ = power * Srated
        S = EPSphasor.Betrag(PQ)
    ]
)

##################################

TransformerIdeal = Model(
    ratio=1.0,
    pin1 = PinVector,
    pin2 = PinVector,
    equations = :[
        pin1.v = ratio * pin2.v
        pin1.i * ratio = -pin2.i
    ]
)

Transformer = Model(
    Srated = 100.0e6,
    Strafo = 100.0e6,
    xt = 0.1,
    rt = 0.1 / 40,
    ratio = 1.0,
    rFe = 1000.0,   #TODO besser sind core losses
    Omegarated=Var(_outer=true, _inner=true),
    wReference=Var(_outer=true, _inner=true),
    pin1 = PinVector,
    pin2 = PinVector,
    X = Reactor | Map(x= :(xt*Srated/Strafo)),
    R = Resistor| Map(r = :(rt*Srated/Strafo)),
    trafoIdeal = TransformerIdeal | Map(ratio = :(ratio)),
    RFe = Resistor | Map(r = :(rFe *Srated / Strafo)),
    Gnd = GroundVec,
    connect = :[
        (pin1, trafoIdeal.pin1)
        (trafoIdeal.pin2, R.p)
        (R.n, X.p, RFe.p)
        (X.n, pin2)
        (Gnd.p, RFe.n)
    ]
)

#########################################

MachinePart = OnePin | Model(
    Flange_a = Mechanic.Flange,
    Flange_b = Mechanic.Flange,
    power = Var(start=SVector{2}(0.0, 0.0)),

    # mech interface
    equations = :[
        phi = Flange_a.phi - Flange_b.phi
        tau = Flange_a.tau
        tau = -Flange_b.tau
        der(phi) = w
        powerMech = w * tau
        current = EPSphasor.Betrag(amps)
        voltage = EPSphasor.Betrag(volts)
        power = EPSphasor.MultiAconjB(volts, amps)
    ]
)

SynchrPart = MachinePart | Model(
    Omegarated=Var(_outer=true, _inner=true),
    wReference=Var(_outer=true, _inner=true),
    ra = 0.0024,
    xd = 1.186,
    xq = 0.699,
    xl = 0.1165,
    r1d = 0.0171,
    r1q = 0.011,
    x1d = 0.2981,
    x1q = 0.1165,
    xad = parameter | Map(value=:(xd - xl)),
    xaq = parameter | Map(value=:(xq - xl)),
    xadq = parameter | Map(value=:([xad  0.0; 0.0  xaq])),
    r1dq = parameter | Map(value=:([r1d  0.0; 0.0  r1q])),
    x1dq = parameter | Map(value=:([x1d  0.0; 0.0  x1q])),

    Psifd = Var(init=1.234),
    imagn = Var(start=SVector{2}(0.0, 0.0)),
    Psi  = Var(init=SVector{2}(1.0, 0.0)),
    Psi1 = Var(init=SVector{2}(1.0, 0.0)),
    Psim = Var(start=SVector{2}(1.0, 0.0)),
    i = Var(start=SVector{2}(0.0, 0.0)),
    v = Var(start=SVector{2}(0.0, 0.0)),
    i1 = Var(start=SVector{2}(0.0, 0.0)),
    TR = Var(start=SVector{2}(0.0, 0.0)),

    phiKR = Var(start=0.0),
    tau = Var(start = 0.0),

    equations = :[
        # reference frame transformation
        der(phiKR) = (w - wReference)*Omegarated
#         TR = [cos(phiKR - PIhalf), sin(phiKR - PIhalf)]
        TR = [sin(phiKR), -cos(phiKR)]  # vereinfacht
        amps = EPSphasor.MultiAB(i, TR)
        volts = EPSphasor.MultiAB(v, TR)

        # flux
        Psi = Psim+i*xl
        Psim = xadq*imagn
        Psi1 = Psim + [Psif1d, 0.0] + x1dq*i1
        #voltage
        v = ra*i + der(Psi)/Omegarated + EPSphasor.MultiJ(Psi)

        [0.0, 0.0] = r1dq*i1 + der(Psi1)/Omegarated
        # torque
        tau = -i[2]*Psi[1] + i[1]*Psi[2]
    ],

)

SynchronousMachineNONsat = SynchrPart | Model(

    Plus    = Electric.Pin,
    Minus   = Electric.Pin,

    rfd	= 0.00037,
    xfd	= 0.2181,
    xf1d = 0.0011,

    equations = :[
        # excitation
        imagn = i + i1 + [ifd, 0.0]
        ifd = Plus.i
        Plus.i + Minus.i = 0
        uF = Plus.v - Minus.v

        Psifd = Psif1d + ifd*xfd + xad*imagn[1]
        Psif1d = xf1d * (i1[1]+ifd)
        uF*rfd/xad = rfd*ifd + der(Psifd)/Omegarated
    ]
)

InductionPart = MachinePart | Model(
    Omegarated=Var(_outer=true, _inner=true),
    wReference=Var(_outer=true, _inner=true),
    x1s = 0.1708,
    x2s = 0.1216,
    xh = 3.2,
    r1 = 0.0139,
    r2 = 0.0117,
    Psi1 = Var(start=SVector{2}(0.0, -1.0)),
    Psi2 = Var(start=SVector{2}(0.3, -0.9)),
    Psim = Var(start=SVector{2}(0.2, -0.9)),
    imagn = Var(start=SVector{2}(0.0,  0.3)),

    equations = :[
        i1 = amps
        u1 = volts
        imagn = i1 + i2
        # flux
        Psim  = xh * imagn
        Psi1 = x1s * i1 + Psim
        Psi2 = Psim + x2s * i2
        # voltages
        u1 = r1*i1 + der(Psi1)/Omegarated  + wReference*EPSphasor.MultiJ(Psi1)
        u2 = r2*i2 + der(Psi2)/Omegarated  + (wReference-w)*EPSphasor.MultiJ(Psi2)
        # torque
        tau = -i1[2]*Psi1[1] + i1[1]*Psi1[2]
    ]
)

InductionMachine = InductionPart | Model(

    equations = :[
        u2 = [0.0, 0.0]
    ]
)

#########################################

SYNunit = Model(

    VoltageREF  = Var(input=true),

    flange = Mechanic.Flange,
    pin = PinVector,

    kRAD = 180.0/pi,

    Omegarated=Var(_outer=true, _inner=true),
    wReference=Var(_outer=true, _inner=true),

    SM = SynchronousMachineNONsat,
    EXC = Electric.VarVoltage,
    Gnd = Electric.Ground,
    exciter = Controller.ExcCONST,
#     exciter = Controller.ExcST8C,
    FIXED= Mechanic.Fixed,

    equations = :[
        exciter.vREF = VoltageREF
        exciter.vGEN = SM.voltage
        exciter.vS = 0.0
        exciter.iFD = SM.ifd
        phiDEG = SM.phiKR*kRAD
        exciter.vT.v = SM.volts
    ],

    connect = :[
        (pin, SM.p)
        (EXC.V, exciter.y)
        (EXC.p, SM.Plus)
        (EXC.n, SM.Minus, Gnd.p)
        (SM.Flange_b, FIXED.flange)
        (SM.Flange_a, flange)
    ]
)

end   #module

max_stars_repo_name: johhell/EPSphasor

max_issues_repo_name: johhell/EPSphasor

max_forks_repo_name: johhell/EPSphasor


text:
include("defns.jl")

"e.g. 1 2 3"
cons(l, ::Nothing) = l

"e.g. +/"
cons(l::L, ::Op1Sym{c}) where {L<:Fn, c} = Op1{c, L}(l)

"e.g. .×"
cons(l::Op2Sym{c}, r::R) where {c, R<:Fn} = Op2Partial{c, R}(r)

"e.g. +⋅×"
cons(l::L, r::Op2Partial{c,R}) where {c, L<:Fn, R} = Op2{c, L, R}(l, r.r)

"e.g. - 1 2 3"
cons(l::Union{Fn, Op}, r::Arr) = Apply(l, r)

"e.g. .- 1 2 3"
cons(l::Union{Fn, Op}, r::Apply) = Apply(cons(l, r.f), r.r)

"e.g 1 2 3 ω"
cons(a::Arr, b::Arr) = ConcArr(a,b) # is this even correct?

"e.g 1 2 3 × 1 2 3"
cons(l::Arr, r::Apply) = Apply2(r.f, l, r.r)

"e.g. /ι10"
cons(l::Op1Sym, r::Apply{T}) where {T<:Fn} = Apply(l, r)

"e.g. -/ι10"
cons(l::Fn, r::Apply{T}) where {T<:Fn} = Apply(l, r)

"e.g. ↔/⍬"
cons(l::Op1Sym{c}, r::Union{Op1Sym,OpSymPair}) where {c} = OpSymPair{c}(r)

"e.g. ×↔/⍬"
cons(l::L, ops::OpSymPair{c}) where {L<:Fn, c} = cons(Op1{c, L}(l), ops.r)

# "e.g ι3 × ι3"
# cons(l::Fn, r::Apply2) = Apply2(r.f, cons(l, r.l), r.r)

cons(x, y) = error("Invalid syntax: $x next to $y")

parse_apl(str) = parse_apl(reverse(replace(str, r"\s+" => " ")), firstindex(str), 0, 0)[1]
function parse_apl(s, i, paren_level, curly_level)
    # top-level parsing
    exp = nothing

    arity = 0

    while iterate(s, i) !== nothing
        c, nxt = iterate(s, i)
        if c == ' '
        elseif c == ')'
            subexp,nxt,plvl,clvl,arity = parse_apl(s, nxt, paren_level+1, curly_level)
            plvl != paren_level && error("Parenthesis error")
            exp = cons(subexp, exp)
        elseif c == '('
            return exp, nxt, paren_level-1, curly_level, arity
        elseif c == '}'
            fn_exp,nxt,plvl,clvl,arity = parse_apl(s, nxt, paren_level, curly_level+1)
            clvl != curly_level && error("Parenthesis error")
            exp = cons(UDefFn{arity}(fn_exp), exp)
        elseif c == '{'
            return exp, nxt, paren_level, curly_level-1,arity
        elseif c in dyadic_operators
            exp = cons(Op2Sym{c}(), exp)
        elseif c in numeric
            nums, nxt = parse_strand(s, i)
            exp = cons(JlVal(nums), exp)
        elseif c in function_names
            exp = cons(PrimFn{c}(), exp)
        elseif c in monadic_operators
            exp = cons(Op1Sym{c}(), exp)
        elseif c == 'α'
            arity = 2
            exp = cons(Α(), exp)
        elseif c == 'ω'
            arity = max(arity, 1)
            exp = cons(Ω(), exp)
        elseif c == '⍬'
            exp = JlVal(Float64[])
        else
            error("$c: undefined APL symbol")
        end
        i=nxt
    end
    exp,i,paren_level, curly_level, arity
end

function parse_strand(str, i)
    buf = IOBuffer()
    p = i
    broke = false
    scalar = true
    while iterate(str, i) !== nothing
        p = i
        c, i = iterate(str, i)
        if c == ' '
            write(buf, ',')
            scalar=false
        elseif c == '⁻'
            write(buf, '-')
        elseif c == '.'
            write(buf, '.')
        elseif c in '0':'9'
            write(buf, c)
        else
            broke = true
            break
        end
    end
    s = reverse(strip(String(take!(buf)), [',']))
    Meta.parse(scalar ? s : "[$s]") |> eval, broke ? p : i
end


text:
for n in 4:10
    # setup square matrices
    S = [BlockMatrix(rand(2n, 2n), [n, n], [n, n]) for _ in 1:3]
    id = BlockMatrix(Matrix(1.0I, 2n, 2n), [n, n], [n, n])
    @testset "verify identity" for M in S
        @test M == rred_star(M, id) == rred_star(id, M)
    end
    @testset "verify associativity" for A in S, B in S, C in S
        @test (rred_star(A, rred_star(B, C)) ≈ rred_star(rred_star(A, B), C))
    end
    @testset "verify inverse" for M in S
        M⁻¹ = inv(M)
        @test rred_star(M, M⁻¹) ≈ rred_star(M⁻¹, M) ≈ id
    end
end


text:
function d_up(p,j1,j2,n,m)
    dj = zeros(m)
    for i in 1:n
        if j1[i]>0 && j2[i]>0
            q = p[j1[i]:j2[i]]
            q ./= sum(q)
            dj[j1[i]:j2[i]] += q
        end
    end
    return dj
end

function setbreaks(trow, o=0)
    tj = sort(unique(max.(o, trow)))
    return tj
end

function p_up!(p,n,m,j1,j2,dj)
    bj = zeros(m)
    for i in 1:n
        if j1[i]>0 && j2[i]>0
        q = sum(p[j1[i]:j2[i]])
        if q > 0.0
        b = (1-q)/q
        ind = setdiff(1:m,j1[i]:j2[i])
        r = vec(p[ind])
        r ./= sum(r)
        bj[ind] += b*r
    end
    end
    end
    num = dj + bj
    copy!(p, num ./ sum(num))
end

function acount(L,R,breaks,n,m)
    afirst = zeros(Int,n)
    alast = zeros(Int,n)
    for i in 1:n
        alpha = L[i] .<= breaks[1:(m-1)] .&& breaks[2:m] .<= R[i]
        if any(alpha)
            afirst[i] = findfirst(alpha)
            alast[i] =  findlast(alpha)
        end
    end
    return afirst, alast
end

function bcount(U,breaks,n,m)
    bfirst = zeros(Int,n)
    blast = zeros(Int,n)
    for i in 1:n
        beta = breaks .<= U[i]
        if any(beta)
            bfirst[i] = findfirst(beta)
            blast[i] =  findlast(beta)
        end
    end
    return bfirst, blast
end

function SurvIC(L, R, S, iter=100, tol=1e-8)
    tj = setbreaks([S-L;S-R])
    n = length(L)
    m = length(tj)
    aind = acount(S-R, S-L,tj,n,m)
    p = inv(m)*ones(m)
    dj = d_up(p, aind[1], aind[2], n, m)
    copy!(p, dj ./ sum(dj))
    con = false
    count = 0
    p2 = p #こういうとこcopy!()とか使ったほうがいいですか？
    for it in 1:iter
        count += 1
        dj = d_up(p,aind[1],aind[2],n,m)
        copy!(p2, dj ./ sum(dj))
        con = all(abs.(p2-p) .< tol)
        p = p2
    if con || any(isnan.(p))
        break
    end
    end
    return tj, 1.0 .- cumsum(p), con, count
end

function SurvICRT(L, R, S, Tmax, iter=100, tol=1e-10)
    tj = setbreaks([S-L;S-R])
    n = length(L)
    m = length(tj)
    aind = acount(S-R, S-L,tj,n,m)
    p = inv(m)*ones(m)
    bind = bcount(Tmax.-S,tj,n,m)
    dj = d_up(p,aind[1],aind[2],n,m)
    p_up!(p, n, m, bind[1], bind[2], dj)
    con = false
    count = 0
    p2 = p #こういうとこcopy!()とか使ったほうがいいですか？
    for it in 1:iter
        count += 1
        dj = d_up(p,aind[1],aind[2],n,m)
        p_up!(p, n, m, bind[1], bind[2], dj)
        con = all(abs.(p2-p) .< tol)
        p = p2
    if con || any(isnan.(p))
        break
    end
    end
    return tj, 1.0 .- cumsum(p), con, count
end

function SurvDIC(EL, ER, SL, SR, iter=100, tol=1e-8)
    S = 0.5*(SL+SR)
    tj = setbreaks([S-EL;S-ER])
    n = length(EL)
    m = length(tj)
    aind = acount(S-ER, S-EL,tj,n,m)
    p = inv(m)*ones(m)
    dj = d_up(p,aind[1],aind[2],n,m)
    copy!(p, dj ./ sum(dj))
    con = false
    count = 0
    p2 = p #こういうとこcopy!()とか使ったほうがいいですか？
    for it in 1:iter
        count += 1
        dj = d_up(p,aind[1],aind[2],n,m)
        copy!(p2, dj ./ sum(dj))
        con = all(abs.(p2-p) .< tol)
        p = p2
    if con || any(isnan.(p))
        break
    end
    end
    return tj, 1.0 .- cumsum(p), con, count
end

function SurvDICRT(EL, ER, SL, SR, Tmax, iter=100, tol=1e-8)
    S = 0.5*(SL+SR)
    tj = setbreaks([S-EL;S-ER])
    n = length(EL)
    m = length(tj)
    aind = acount(S-ER, S-EL,tj,n,m)
    p = inv(m)*ones(m)
    bind = bcount(Tmax.-SR,tj,n,m)
    dj = d_up(p,aind[1],aind[2],n,m)
    p_up!(p, n, m, bind[1], bind[2],dj)
    con = false
    count = 0
    p2 = p #こういうとこcopy!()とか使ったほうがいいですか？
    for it in 1:iter
        count += 1
        dj = d_up(p,aind[1],aind[2],n,m)
        p_up!(p, n, m, bind[1], bind[2], dj)
        con = all(abs.(p2-p) .< tol)
        p = p2
    if con || any(isnan.(p))
        break
    end
    end
    return tj, 1.0 .- cumsum(p), con, count
end

max_stars_repo_name: abikoushi/IntervalCensored

max_issues_repo_name: abikoushi/IntervalCensored

max_forks_repo_name: abikoushi/IntervalCensored


text:
using ProceduralNoise
using Base.Test

# TODO


text:
using Distributions

# Set Confidence Intervals
η_v = .05 # 1 - Confidence for voltage limit cc
η_g = .05 # 1 - Confidence for generation limit cc
ξ = .005# 1 - Confidence for variance estimator

# SND Quantiles for global use
snd = Normal(0,1)
z_v = quantile(snd, 1-η_v)
z_g = quantile(snd, 1-η_g)
z_g2 = quantile(snd, 1-(η_g/2))

# Set True Load Standard Deviation
ld = 0.2

# Save Historic Data Globally
global ERROR_HIST = 0

# Calculate variance estimation confidence
function chisq_interval(s, n, alpha)
    # println("s = $s, n = $n, alpha = $alpha")
    # s... sample variance
    # n... degrees of freedom
    # alpha... confidence
    # Returns lower and upper bound of the 1-alpha-confidence interval
    # of the estmated variance
    ch = Chisq(n)
    lower = (n * s)/quantile(ch, 1- alpha)
    upper = (n * s)/quantile(ch, alpha)
    return [lower, upper]
end

function create_data(var_true, N)
    obs = zeros(length(var_true), N)
    for (i,v) in enumerate(var_true)
        if v > 0
            dist = Normal(0,sqrt(v))
            obs[i,:] = rand(dist, N)
        end
    end
    return obs
end

function init_stochasticity(;create_new_data=true, sample_N=100, variance_opt="implicit")

# Set the ture error distribution
# Same Variance for each bus
n_buses = length(BUSES)
if variance_opt == "implicit"
# Create individual node variance from load
    loads = [BUSES[b].d_P for b in 1:n_buses]
    var_true_vector = (loads.*ld).^2
    # Variance = (ld% of load)^2
elseif variance_opt == "explicit"
# Same variance for all nodes
    if DATA_DIR == "basecase"
        f = 0.01
    elseif DATA_DIR == "simplecase"
        f = 0.1
    elseif DATA_DIR == "33buscase_pu"
        f = 4
    else
        f = 1
        warn("Data Dir. $(DATA_DIR) unkown for uncertainty setting")
    end
    var_true_vector = ones(n_buses)*f
elseif variance_opt == "manual"
# Manually create an array of variances
    var_true_vector = []
    if length(var_true_vector) < n_buses
        warn("Variance Vector to short")
        var_true_vector = ones(n_buses).*1e-8
    end
else
    warn("Unknown Variance Setting")
    var_true_vector = ones(n_buses).*1e-8
end

# Create or load data for samples
if create_new_data || ERROR_HIST == 0
    info("Creating new Historical Observations for DR Implementation")
    N = sample_N
    error_hist = create_data(var_true_vector, N)
    global ERROR_HIST = error_hist
else
    N = length(ERROR_HIST[1,:])
    error_hist = ERROR_HIST
end

# Work with the data
# Calculate sample variances
var_sample_vector =[]
for i in 1:n_buses
    sn = 1/N * sum(error_hist[i,:].^2)
    push!(var_sample_vector, sn)
end

# var_sample_covmat = 1/N * error_hist * error_hist'
# var_sample_vector =  diag(var_sample_covmat) # Drop covariance entries

intervals = [chisq_interval(s_n, N, ξ) for s_n in var_sample_vector]
var_sample_lower = [intervals[i][1] for i in 1:n_buses]
var_sample_upper = [intervals[i][2] for i in 1:n_buses]

# return var_true_vector, error_dist_true, var_sample_upper, var_sample_vector
return var_true_vector, var_sample_upper, var_sample_vector
end


text:
# This file was generated, do not modify it. # hide
fit!(clf)
ŷ = predict(clf, X2)
ŷ[1:3]


text:
####bandwidth selector for kernel density

#rule of thumb
function bwnormal(xdata::RealVector)
    0.9 * min((quantile(xdata, .75) - quantile(xdata, .25)) / 1.34, std(xdata)) * length(xdata) ^ (-0.2)
end
function midrange(x::RealVector)
    lq, uq = quantile(x, [.25, .75])
    uq - lq
end

# J(h)=∑ᵢⱼK'((xᵢ - xⱼ)/h) /(n²h) + 2K(0)/(nh) =
# where K'(u) = invsqrt2π(exp(-0.25u*u)/sqrt(2) - 2exp(-0.5u*u))
#J(h) = invsqrt2π/(n²h) ∑ᵢⱼ (exp(-0.25u*u)/sqrt(2) - 2exp(-0.5u*u)) + 2 * invsqrt2π/nh
#J(h) = 2*invsqrt2π/(n²h) ∑{i<j} (exp(-0.25u*u)/sqrt(2) - 2exp(-0.5u*u)) + invsqrt2π/sqrt(2)nh
#For normal kernel
# function Jh(xdata::RealVector, h::Real, n::Int)
#     tmp = 0.0
#     @inbounds for i in 1:(n-1)
#         for j in (i+1):n
#             u = (xdata[i] - xdata[j])/h
#             # u = exp(-0.25*u*u)
#             u = -0.25*u*u
#             u = exp(u)
#             tmp += u*invsqrt2 - 2*u*u
#         end
#     end
#     2*tmp / (n*n*h) + 1/(sqrt2*n*h)
# end
#For gaussiankernel, equivalent to the above one. The above version requires
#less computing but is inefficient in practice due to the exp function.
function Jh(xdata::RealVector, h::Real, w::Vector, n::Int)
    ll = 0.0
    @inbounds for ind in 1:n
        gaussiankernel(xdata[ind], xdata, sqrt2*h, w, n)
        ll += mean(w)
        # gaussiankernel(xdata[ind], xdata, h, w, n)
        # w[ind] = 0.0
        # ll -= 2*mean(w)
    end
    ll / n - leaveoneout(xdata, gaussiankernel, h, w, n)
end

#for general kernel
function Jh(xdata::RealVector, kernel::Function, h::Real, w::Vector, n::Int, xlb::Real, xub::Real)
    hquadrature(x->begin kernel(x, xdata,h,w,n); mean(w)^2; end, xlb, xub, maxevals=200)[1] - leaveoneout(xdata, kernel, h, w, n)
end
function leaveoneout(xdata::RealVector, kernel::Function, h::Real, w::Vector, n::Int)

    ll = 0.0
    @inbounds for ind in 1:n
        kernel(xdata[ind], xdata, h, w, n)
        w[ind] = 0.0
        ll += mean(w)
    end
    ll * 2 / (n-1)
end
#For betakernel
function Jh(xdata::RealVector, logxdata::RealVector,log1_xdata::RealVector, kernel::Function, h::Real, w::Vector, n::Int, xlb::Real, xub::Real)
    hquadrature(x->begin kernel(x, logxdata, log1_xdata, h,w,n); mean(w)^2; end, xlb, xub, maxevals=200)[1] - leaveoneout(xdata, logxdata, log1_xdata, kernel, h, w, n)
end
function leaveoneout(xdata::RealVector, logxdata::RealVector, log1_xdata::RealVector, kernel::Function, h::Real, w::Vector, n::Int)

    ll = 0.0
    @inbounds for ind in 1:n
        kernel(xdata[ind], logxdata, log1_xdata, h, w, n)
        w[ind] = 0.0
        ll += mean(w)
    end
    ll * 2 / (n-1)
end
#For gammakernel
function Jh(xdata::RealVector, logxdata::RealVector, kernel::Function, h::Real, w::Vector, n::Int, xlb::Real, xub::Real)
    hquadrature(x->begin kernel(x, xdata, logxdata, h,w,n); mean(w)^2; end, xlb, xub, maxevals=200)[1] - leaveoneout(xdata, logxdata, kernel, h, w, n)
end
function leaveoneout(xdata::RealVector, logxdata::RealVector, kernel::Function, h::Real, w::Vector, n::Int)

    ll = 0.0
    @inbounds for ind in 1:n
        kernel(xdata[ind], xdata, logxdata, h, w, n)
        w[ind] = 0.0
        ll += mean(w)
    end
    ll * 2 / (n-1)
end
#Least Squares cross validation for Gaussian Kernel
#May fail to work if there are multiple equial x_i
# Silverman suggest search interval be (0.25, 1.5)n^(-0.2)σ
function bwlscv(xdata::RealVector, kernel::Function)
    n=length(xdata)
    w  = zeros(n)
    h0=bwnormal(xdata)
    #when n is large, leaveoneout is more expensive than numeric integration
    if kernel == gaussiankernel && n<200
        return Optim.minimizer(Optim.optimize(h -> Jh(xdata, h, w, n), 0.01*h0, 10*h0, iterations=200, abs_tol=h0/n))
    end

    xlb, xub = extrema(xdata)
    h0=midrange(xdata)
    hlb = h0/n
    hub = h0
    if kernel == betakernel
        xlb = 0.0
        xub = 1.0
        hub = 0.25
        logxdata = log.(xdata)
        log1_xdata = log.(1.0 .- xdata)
        return Optim.minimizer(Optim.optimize(h -> Jh(xdata, logxdata, log1_xdata, kernel, h, w, n, xlb,xub), hlb, hub, iterations=200,abs_tol=h0/n^2))
    elseif kernel == gammakernel
        xlb = 0.0
        logxdata = log.(xdata)
        return Optim.minimizer(Optim.optimize(h -> Jh(xdata, logxdata, kernel, h, w, n, xlb,xub), hlb, hub, iterations=200,abs_tol=h0/n^2))
    end
    return Optim.minimizer(Optim.optimize(h -> Jh(xdata, kernel, h, w, n, xlb,xub), hlb, hub, iterations=200,abs_tol=h0/n^2))
end

# likelihood cross validation for beta and gamma kernel
#there seems no other easy way; least square cross validation can be formidable because their convolution have no close form
#may also work for other kernels, but likelihood cv has some known disadvantages.
#Not recommended, but easy to implement for arbitrary kernel
function lcv(xdata::RealVector, kernel::Function, h::Real, w::Vector, n::Int)
#     -mean(kerneldensity(xdata,xdata,kernel,h)) + mean(map(kernel, xdata, xdata, h))
    ind = 1
    ind_end = 1+n
    ll = 0.0
    @inbounds while ind < ind_end
        kernel(xdata[ind], xdata, h, w, n)
        w[ind] = 0.0
        ll += log(mean(w))
        ind += 1
    end
    -ll
end
function bwlcv(xdata::RealVector, kernel::Function)
    n = length(xdata)
    w = zeros(n)
    h0=midrange(xdata)
    hlb = h0/n^2
    hub = h0
    if kernel==betakernel
        hub = 0.25
    end
    return Optim.minimizer(Optim.optimize(h->lcv(xdata,kernel,h,w,n), hlb, hub, iterations=200,abs_tol=h0/n^2))
end

function lcv(xdata::RealMatrix, kernel::Vector, h::RealVector, w::Vector, n::Int)
#     -mean(kerneldensity(xdata,xdata,kernel,h)) + mean(map(kernel, xdata, xdata, h))
    if any(h .<= 0.0)
        return Inf
    end
    ind = 1
    ind_end = 1+n
    ll = 0.0
    p = size(xdata)[2]
    @inbounds while ind < ind_end
        fill!(w, 1.0)
        wtmp = ones(n)
        for j in 1:p
            kernel[j](xdata[ind, j], xdata[:, j], h[j], wtmp, n)
            for k in 1:n
                w[k] = w[k] * wtmp[k]
            end
        end
        divide!(w, sum(w))

        w[ind] = 0.0
        ll += log(mean(w))
        ind += 1
    end
    -ll
end
function bwlcv(xdata::RealMatrix, kernel::Vector)
    n, p = size(xdata)
    w = ones(n)
    h0 = zeros(p)
    hlb = zeros(p)
    hub = zeros(p)
    for j in 1:p
        if kernel[j] == gaussiankernel
            h0[j] = bwnormal(xdata[:, j])
            hlb[j] = 0.1*h0[j]
            hub[j] = 10*h0[j]
        elseif kernel[j] == betakernel
            h0[j] = midrange(xdata[:, j])
            hlb[j] = h0[j]/n
            hub[j] = 0.25
        elseif kernel[j] == gammakernel
            h0[j] = midrange(xdata[:, j])
            hlb[j] = h0[j]/n
            hub[j] = h0[j]
        end
    end
    h = Optim.minimizer(Optim.optimize(h->lcv(xdata, kernel, h, w, n), h0))
    if all(hlb .<= h .<= hub)
        return h
    else
        return h0
    end
end

#leave-one-out LSCV. 1/n \sum((yi - yihat)/(1-wi))
function lscvlocalconstant(xdata::RealVector, ydata::RealVector, kernel::Function, h::Real, w::Vector, n::Int)
    tmp = 0.0
    ind = 1
    ind_end = 1 + n
    @inbounds while ind < ind_end
#         map!(kernel, w, xdata, xdata[i], h)
        kernel(xdata[ind], xdata, h, w, n)
        divide!(w, sum(w))
        tmp += abs2((wsum(w, ydata) - ydata[ind])/(1-w[ind]))
        ind += 1
    end
    tmp/n
end

function bwlocalconstant(xdata::RealVector, ydata::RealVector, kernel::Function=gaussiankernel)
    n=length(xdata)
    length(ydata)==n || error("length(ydata) != length(xdata)")
    w = ones(n)
    if kernel == gaussiankernel
        h0= bwnormal(xdata)
        hlb = 0.1*h0
        hub = 10*h0
    elseif kernel == betakernel
        h0 = midrange(xdata)
        hlb = h0/n
        hub = 0.25
    elseif kernel == gammakernel
        h0 = midrange(xdata)
        hlb = h0/n
        hub = h0
    end
    Optim.minimizer(Optim.optimize(h->lscvlocalconstant(xdata, ydata, kernel, h, w, n), hlb, hub))
end

#see reference:Smoothing Parameter Selection in Nonparametric Regression Using an Improved Akaike Information Criterion
# Clifford M. Hurvich, Jeffrey S. Simonoff and Chih-Ling Tsai
# Journal of the Royal Statistical Society. Series B (Statistical Methodology), Vol. 60, No. 2 (1998), pp. 271-293
#http://www.jstor.org/stable/2985940
function AIClocallinear(xdata::RealVector, ydata::RealVector, kernel::Function, h::Real, w::Vector, n::Int)
    tmp = 0.0
    traceH = 0.0
    ind = 1
    ind_end = 1+n
    @inbounds while ind < ind_end
#           map!(kernel, w, xdata, xdata[i], h)
          kernel(xdata[ind], xdata, h, w, n)
          s0 = sum(w)
          s1 = s0*xdata[ind] - wsum(w, xdata)
          s2 = wsumsqdiff(w, xdata, xdata[ind], n)
          sy0 = wsum(w, ydata)
          sy1 = wsumyxdiff(w, xdata, xdata[ind], ydata, n)
          tmp+=abs2((s2 * sy0 - s1 * sy1) /(s2 * s0 - s1 * s1) - ydata[ind])
          traceH += s0*w[ind]/(s2 * s0 - s1 * s1)
          ind += 1
    end
    tmp/n  + 2*(traceH+1)/(n-traceH-2)
end

function bwlocallinear(xdata::RealVector, ydata::RealVector, kernel::Function=gaussiankernel)
    n=length(xdata)
    length(ydata)==n || error("length(ydata) != length(xdata)")
    w = ones(n)
    if kernel == gaussiankernel
        h0= bwnormal(xdata)
        hlb = 0.1*h0
        hub = 10*h0
    elseif kernel == betakernel
        h0 = midrange(xdata)
        hlb = h0/n
        hub = 0.25
    elseif kernel == gammakernel
        h0 = midrange(xdata)
        hlb = h0/n
        hub = h0
    end
    Optim.minimizer(Optim.optimize(h->AIClocallinear(xdata, ydata, kernel,h, w, n), hlb, hub))
end

function bwreg(xdata::RealVector, ydata::RealVector, reg::Function, kernel::Function=gaussiankernel)

    if (reg == locallinear) || (reg == lp1)
        return bwlocallinear(xdata, ydata, kernel)
    elseif (reg == localconstant) || (reg == lp0)
        return bwlocalconstant(xdata, ydata, kernel)
    else
        error("I don't know what is $reg")
    end
end

# #leave-one-out LSCV for multivariate NW
function lscvlocalconstant(xdata::RealMatrix, ydata::RealVector, kernel::Array{Function, 1}, h::RealVector, w::Vector, n::Int)
    if any(h .<= 0.0)
        return Inf
    end
    tmp = 0.0
    ind = 1
    ind_end = 1+n
    p=size(xdata)[2]
    @inbounds while ind < ind_end
        fill!(w, 1.0)
        wtmp = ones(n)
        for j in 1:p
            kernel[j](xdata[ind, j], xdata[:, j], h[j], wtmp, n)
            for k in 1:n
                w[k] = w[k] * wtmp[k]
            end
        end
        divide!(w, sum(w))
        tmp += abs2((wsum(w, ydata) - ydata[ind])/(1-w[ind]))
        ind += 1
    end
    tmp/n
end

function bwlocalconstant(xdata::RealMatrix, ydata::RealVector, kernel::Array{Function, 1} = Function[gaussiankernel for i in 1:size(xdata)[2]])
    n, p = size(xdata)
    w = ones(n)
    h0 = zeros(p)
    hlb = zeros(p)
    hub = zeros(p)
    for j in 1:p
        if kernel[j] == gaussiankernel
            h0[j] = bwnormal(xdata[:, j])
            hlb[j] = 0.1*h0[j]
            hub[j] = 10*h0[j]
        elseif kernel[j] == betakernel
            h0[j] = midrange(xdata[:, j])
            hlb[j] = h0[j]/n
            hub[j] = 0.25
        elseif kernel[j] == gammakernel
            h0[j] = midrange(xdata[:, j])
            hlb[j] = h0[j]/n
            hub[j] = h0[j]
        end
    end
    h_output=Optim.minimizer(Optim.optimize(h->lscvlocalconstant(xdata, ydata, kernel, h, w, n), h0))
    if any(h_output .<= 0.0)
        for j in 1:p
            if h_output[j] .<= 0.0
                h_output[j] = 2 .* h0[j]
            end
        end
        h_output = Optim.minimizer(Optim.optimize(h->lscvlocalconstant(xdata, ydata, kernel, h, w, n), h_output))
    end
    h_output
end


text:
module StaticKernels

export Kernel
export extend
export @kernel

include("util.jl")
include("types.jl")

include("extension.jl")
include("extensionarray.jl")
include("window.jl")
include("kernel.jl")

include("loop.jl")
include("operations.jl")

end


text:
#
# This file is a part of MolecularGraph.jl
# Licensed under the MIT License http://opensource.org/licenses/MIT
#

@testset "graph.product" begin

    @testset "modularproduct" begin
        g = pathgraph(3)
        prod = modularproduct(g, g)
        @test nodecount(prod) == 9
        @test edgecount(prod) == 10
    end

    @testset "cartesian.product" begin
        g = pathgraph(5)
        prod = cartesianproduct(g, g)
        grid = squaregrid(5, 5)
        @test isgraphmatch(prod, grid)
    end
end # graph.product


text:
const EXAMPLES = filter(ex -> endswith(ex, ".jl") && ex != "run_examples.jl" && ex != "all_examples.jl" && ex != "goldsteinprice.jl" && ex != "sparse_polynomials.jl" && ex != "scaled_perm.jl" && ex != "symmetry.jl",
                        readdir(@__DIR__))