diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 090ca12..637dbd7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -51,12 +51,15 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v3 - # The script resolves KernelSmoothing (master) before FDA (main), also on 1.10. - - name: Resolve ecosystem URL dependencies including FDA - shell: julia --color=yes {0} - run: include(joinpath(ENV["GITHUB_WORKSPACE"], "scripts", "instantiate.jl")) + - name: Check naming conventions + run: ${{ runner.os == 'Windows' && 'python' || 'python3' }} scripts/check_naming.py + - name: Resolve dependencies from master checkouts + run: julia --color=yes scripts/instantiate.jl - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 + - name: Test against dependency master checkouts + run: julia --color=yes scripts/test_master.jl + env: + JULIA_TEST_COVERAGE: "true" - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v7 with: diff --git a/.gitignore b/.gitignore index d8d57ec..15cb289 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,9 @@ # Local development and generated experiment outputs .dev/ results/ + +# Generated master dependency test environments +.master-test-env/ + +# Local Julia-minor environments are regenerated from master checkouts +/Manifest-v*.toml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3bc4365 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Development conventions + +Types and modules use UpperCamelCase. Descriptive functions, fields and keyword +arguments use snake_case; mutating functions end in `!`. Retain standard numerical +abbreviations, mathematical identifiers and names required by upstream interfaces. +Run `python3 scripts/check_naming.py` to check new declarations. + +Run `julia scripts/instantiate.jl` before tests. Workspace dependencies use sibling +master checkouts, including transitive dependencies. Missing checkouts are cloned +from master. Existing checkouts are never reset or silently updated. Package +version numbering and registry changes are deferred. + +Run the complete tests with `julia scripts/test_master.jl`. This uses an isolated +master-based environment on Julia 1.10 and newer without changing package deps. diff --git a/Project.toml b/Project.toml index dcad72e..1a1dcc5 100644 --- a/Project.toml +++ b/Project.toml @@ -13,10 +13,10 @@ ProximalBase = "0a755509-c5b1-5d6f-9b24-59bcda3c37a3" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [sources] -CovarianceSelection = {rev = "master", url = "https://github.com/mlakolar/CovarianceSelection.jl"} -FunctionalDataAnalysis = {rev = "main", url = "https://github.com/mlakolar/FunctionalDataAnalysis.jl"} -KernelSmoothing = {rev = "master", url = "https://github.com/mlakolar/KernelSmoothing.jl"} -ProximalBase = {rev = "master", url = "https://github.com/mlakolar/ProximalBase.jl"} +CovarianceSelection = {path = "../CovarianceSelection.jl"} +FunctionalDataAnalysis = {path = "../FunctionalDataAnalysis.jl"} +KernelSmoothing = {path = "../KernelSmoothing.jl"} +ProximalBase = {path = "../ProximalBase.jl"} [compat] Aqua = "0.8" diff --git a/README.md b/README.md index 421139a..bb74305 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Estimation of Gaussian graphical models for multivariate functional data: The package uses current `master` versions of **ProximalBase** and **CovarianceSelection**, with **ProximalSolvers** as a transitive dependency, and the registered **BSplineKit** for B-spline bases. **FunctionalDataAnalysis** -(FDA, `main`) supplies data types, trapezoidal quadrature, dense FPCA and PACE. +(FDA, `master`) supplies data types, trapezoidal quadrature, dense FPCA and PACE. **KernelSmoothing** (`master`) supplies PACE kernels. FDA and KernelSmoothing must be registered before FGM can be registered. @@ -23,9 +23,10 @@ Julia 1.10 or newer is required. Resolve ecosystem dependencies explicitly: ```sh julia scripts/instantiate.jl +julia scripts/test_master.jl ``` -The generated Manifest records exact dependency commits. The setup script +The generated Manifest uses the sibling master checkouts. The setup script preserves the package Project file. For an existing resolved environment, `julia --project -e 'using Pkg; Pkg.instantiate()'` reuses its Manifest. @@ -70,7 +71,7 @@ curves = FDA.MultivariateFunctionalData([ FDA.GridFunctionalData(values[:, j, :], grid) for j in axes(values, 2) ]) values_copy = FDA.to_array(curves) -fpca = FDA.fit(FDA.FPCA, curves; basis=:marginal, ncomponents=2, maxK=2) +fpca = FDA.fit(FDA.FPCA, curves; basis=:marginal, ncomponents=2, max_k=2) scores = FDA.score_array(fpca) # subjects × nodes × components heldout_scores = FDA.predict(fpca, curves) # one matrix per node reconstructed = FDA.reconstruct(fpca, heldout_scores) diff --git a/docs/M4_MIGRATION.md b/docs/M4_MIGRATION.md index 153730f..cf9a717 100644 --- a/docs/M4_MIGRATION.md +++ b/docs/M4_MIGRATION.md @@ -21,7 +21,7 @@ The audit follow-up continued from **d7840ffa8095040868edc1eabc4dca0e5693408a**. response columns. A validation self-test checks independent component sign flips, input preservation, and detection of a coefficient change. - **F2:** The qiao2019 experiment requests `basis=:marginal` and - `maxK=opts["components"]`, matching the package's fixed truncation. + `max_k=opts["components"]`, matching the package's fixed truncation. - **F3:** CSV provenance appends `+dirty` to the recorded HEAD when tracked or untracked working-tree changes exist. Source and worker SHA-256 digests still identify the estimator source and exact comparison worker. The CSV @@ -37,9 +37,9 @@ does not change package estimator code or expected-value tolerances. 1. FDA PR #16 fixed pooled rank upstream. The refreshed FDA `main` is **1b75c014778ad374b30fad70fa9083591e1bb546**, source tree - `f3e6290f513a5c4350ffa7b184eee62a423518f2`. Its pooled `maxK` default is + `f3e6290f513a5c4350ffa7b184eee62a423518f2`. Its pooled `max_k` default is `max(1, min(g-2, p*(n-1)-1))`, with explicit cap `min(g, p*(n-1))`. - FGM calls FDA's pooled route directly. The existing `maxK=ncomponents` + FGM calls FDA's pooled route directly. The existing `max_k=ncomponents` requests the caller's fixed truncation, including the structural rank cap; there is no local rank workaround or replacement eigenstep. A regression fixture now retains four pooled components from two nodes with only three @@ -101,7 +101,7 @@ Such a case remains an error; no ridge or pseudoinverse was introduced. `SparseFunctionalData`, `FPCAResult`, `fit_fpca`, `transform_fpca`, `reconstruct`, `SparseFPCAFit`, `fit_sparse_fpca`. The README lists each. The exported `quadrature_weights` is now FDA's function. -- Project sources specify FDA `main` and KernelSmoothing `master`. CI invokes +- Project sources use sibling FDA and KernelSmoothing `master` checkouts. CI invokes `scripts/instantiate.jl`, which resolves KernelSmoothing before FDA in one URL-resolution transaction, including on Julia 1.10. ProximalSolvers stays transitive. The script restores Project.toml after **all** Pkg operations, @@ -112,10 +112,10 @@ Such a case remains an error; no ridge or pseudoinverse was introduced. | Route | Resolved defaults / required choices | Source and reason | | --- | --- | --- | -| Dense FPCA | marginal for glasso/neighborhood/doubly; pooled for separable; explicit integer `ncomponents`, `maxK=ncomponents`; no presmoothing | FGM estimator definitions and caller truncation; explicit maxK avoids FDA's automatic smaller cap | +| Dense FPCA | marginal for glasso/neighborhood/doubly; pooled for separable; explicit integer `ncomponents`, `max_k=ncomponents`; no presmoothing | FGM estimator definitions and caller truncation; explicit max_k avoids FDA's automatic smaller cap | | Dense eigenstep | trapezoidal quadrature; covariance/eigenvalues `n−1`; `eigen_tol=1e-10`, `sign_tol=1e-12`; mean alignment then largest absolute entry | FDA FPCA and multivariate FPCA docstrings, following fdapace 0.6.0 except documented FDA numerical sign/cutoff rules | | Sparse PACE | required work grid and bandwidth; marginal PACE; Gaussian kernel, optional Epanechnikov; `weighting=:subj`; same bandwidth for mean, covariance, diagonal; `binning=nothing` | Preserve FGM settings, rather than fdapace automatic bandwidth/binning defaults; kernels map to KernelSmoothing objects | -| Sparse spectrum | `maxK=length(grid)`, `eigen_tol=100eps(Float64)`, FDA `sign_tol=1e-12`; all retained positive modes in conditional observation covariance | Preserve FGM full-mode conditional-score algorithm and former relative positive-mode cutoff, with FDA signs | +| Sparse spectrum | `max_k=length(grid)`, `eigen_tol=100eps(Float64)`, FDA `sign_tol=1e-12`; all retained positive modes in conditional observation covariance | Preserve FGM full-mode conditional-score algorithm and former relative positive-mode cutoff, with FDA signs | | Sparse noise | `nothing` estimates; supplied nonnegative scalar or vector passes through per node | Existing FGM interface; adopted FDA middle-half integral using the raw smoothed covariance diagonal, as approved above | | Score covariance | `center=true`, `corrected=false` (`1/n`) | Existing FGM likelihood scaling; `corrected=true` selects `1/(n−1)` | | P-spline | required `nbasis`; degree 3, penalty 0, second coefficient differences, trapezoidal loss | Existing FGM cubic unpenalized defaults; FDA derivative penalty does not reproduce the objective | @@ -128,7 +128,7 @@ Such a case remains an error; no ridge or pseudoinverse was introduced. | Simulation | n 100, p 50, ncomponents 5, ntime 100, model `:ar2`, noise_sd 0.5, random_diagonal_margin 0.1 | Existing FGM generating-design implementation; settings and caller RNG remain explicit | The migration adds no new automatic tuning. The additional implementation -choices are explicit maxK overrides, no sparse binning, retention of FGM's +choices are explicit max_k overrides, no sparse binning, retention of FGM's sparse eigenvalue cutoff, per-candidate tuning fits to preserve valid lower ranks, and metadata storage. There are no outstanding author decisions. The linked external plan was inaccessible through the browser; @@ -277,7 +277,7 @@ julia +lts --startup-file=no --project=. validation/m4_compare.jl results/m4-aud The generated table was copied to `validation/m4_comparison.csv` as the final tracked artifact update. Its neighborhood settings now show the estimator's -own `maxK=2`. All previously reported metric cells remain unchanged; the new +own `max_k=2`. All previously reported metric cells remain unchanged; the new coefficient column measures the neighborhood output that was missing before. The full-suite, Aqua, Julia-release, and documentation checks remain the controller's responsibility. diff --git a/experiments/qiao2019.jl b/experiments/qiao2019.jl index b1c09aa..8671c72 100644 --- a/experiments/qiao2019.jl +++ b/experiments/qiao2019.jl @@ -206,7 +206,7 @@ function run_experiment(opts, arguments) ncomponents=5, ntime=opts["ntime"], model, noise_sd=0.5) smoothed = smooth_bspline(sim.data; nbasis=opts["nbasis"]) fpca = FDA.fit(FDA.FPCA,smoothed; method=:dense,basis=:marginal, - ncomponents=opts["components"],maxK=opts["components"]) + ncomponents=opts["components"],max_k=opts["components"]) bounds = penalty_bounds(smoothed, fpca) empty_graph = falses(opts["p"], opts["p"]) complete_graph = trues(opts["p"], opts["p"]) diff --git a/scripts/check_naming.py b/scripts/check_naming.py new file mode 100644 index 0000000..e4cc472 --- /dev/null +++ b/scripts/check_naming.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Check new Julia declarations; compatibility files and mathematical names are explicit exceptions.""" +import argparse +from pathlib import Path +import re + +# Matrix caches and scientific notation retain their conventional identifiers. +MATHEMATICAL = {'A','B','C','D','G','H','I','J','K','L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z', + 'Ax','At','Bt','Xt','Yt','XtY','XtX','sUtY','Ws','SX','SY','Xn','Yn','SXn','SYn', + 'Xa','Ya','SXa','SYa','Xr','Yr','posA','posR','T0','F','Sx','Sy','Sxy','Sxx','Syy','AD','Vt','AU','D_calls','Dt_calls','Dx','Dxp','Dty','Dtyp','Bpath'} +FUNCTION = re.compile(r'^\s*(?:function\s+)([\w.!]+)\s*\(|^\s*([\w.!]+)\([^\n]*\)\s*(?:where\s+[^=]+)?=(?!=)') +FIELD = re.compile(r'^\s*(\w+)\s*::') +LEGACY_KEYWORDS = {'printEvery','normD','maxK','requested_maxK','epsTol','maxIter','checkEvery','bootSamples','estimSupport'} +FACTORIES = {'IndBallLinf','Constrained','_AtomIterate','CDLeastSquaresLoss','CDWeightedLSLoss','CDSqrtLassoLoss','CDSqrtQuadraticLoss','CDQuadraticLoss'} +TYPE = re.compile(r'^\s*(?:mutable\s+)?(?:struct|abstract\s+type|primitive\s+type)\s+(\w+)') + +def check(root): + errors=[] + types={m.group(1) for path in (root/'src').rglob('*.jl') + for line in path.read_text(encoding="utf-8").splitlines() if (m := TYPE.match(line))} + types |= {m.group(1) for path in (root/'src').rglob('*.jl') for m in re.finditer(r'^\s*const\s+(\w+)\s*=',path.read_text(encoding="utf-8"),re.M)} + for path in sorted((root/'src').rglob('*.jl')): + if 'deprecated' in path.stem: continue + in_doc=False; in_struct=False + lines=path.read_text(encoding="utf-8").splitlines() + for number,line in enumerate(lines,1): + if line.count('"""') % 2: in_doc=not in_doc; continue + if in_doc: continue + line=line.split('#',1)[0] + typ=TYPE.match(line) + if typ: + name=typ.group(1) + if not name.lstrip('_')[0].isupper():errors.append((path,number,'type',name)) + in_struct='struct' in line + continue + if re.match(r'^\s*end\b',line):in_struct=False + function=FUNCTION.match(line) + if function: + name=function.group(1) or function.group(2) + # Qualified names implement external interfaces; constructors must name an actual type. + if '.' not in name and name not in types and name not in FACTORIES and name not in MATHEMATICAL and re.search('[A-Z]',name): + errors.append((path,number,'function',name)) + if ';' in line or line.lstrip().startswith('function '): + signature=line[line.find('(')+1:] + depth=1 + signature.count('(')-signature.count(')') + offset=number + while depth>0 and offset ("ProximalBase.jl", "ProximalBase.jl"), + "ProximalSolvers" => ("ProximalOPT.jl", "ProximalSolvers.jl"), + "HighDimensionalModels" => ("HighDimensionalModels.jl", "HighDimensionalModels.jl"), + "CovarianceSelection" => ("CovarianceSelection.jl", "CovarianceSelection.jl"), + "DiffPrecTest" => ("DiffPrecTest.jl", "DiffPrecTest.jl"), + "KernelSmoothing" => ("KernelSmoothing.jl", "KernelSmoothing.jl"), + "FunctionalDataAnalysis" => ("FunctionalDataAnalysis.jl", "FunctionalDataAnalysis.jl"), + "FunctionalGraphicalModels" => ("FunctionalGraphicalModels.jl", "FunctionalGraphicalModels.jl"), +) + +function instantiate_master(environment::AbstractString; + workspace::Union{Nothing,AbstractString}=nothing, + owner::AbstractString=environment) + environment = realpath(environment) + owner = realpath(owner) + workspace = workspace === nothing ? dirname(owner) : realpath(workspace) + owner_project = TOML.parsefile(joinpath(owner, "Project.toml")) + owner_name = get(owner_project, "name", "") + specs = Dict{String,Pkg.PackageSpec}() + visiting = Set{String}() + function visit(project_path) + project = TOML.parsefile(project_path) + deps = merge(get(project, "deps", Dict()), get(project, "extras", Dict())) + for name in sort!(collect(keys(deps))) + haskey(WORKSPACE_PACKAGES, name) || continue + name in visiting && continue + push!(visiting, name) + folder, repository = WORKSPACE_PACKAGES[name] + is_owner = name == owner_name + checkout = is_owner ? owner : joinpath(workspace, folder) + if !isdir(checkout) + url = "https://github.com/mlakolar/$repository" + run(`git clone --branch master --single-branch $url $checkout`) + end + branch = strip(read(`git -C $checkout branch --show-current`, String)) + (is_owner || branch == "master") || error("$name must use its master checkout: $checkout is on $branch") + dependency = TOML.parsefile(joinpath(checkout, "Project.toml")) + dependency["name"] == name || error("wrong package name at $checkout") + dependency["uuid"] == deps[name] || error("wrong UUID for $name at $checkout") + specs[name] = Pkg.PackageSpec(; path=checkout) + visit(joinpath(checkout, "Project.toml")) + end + end + project_path = joinpath(environment, "Project.toml") + original = read(project_path, String) + visit(project_path) + Pkg.activate(environment) + try + if isempty(specs) + Pkg.resolve() + else + Pkg.develop([specs[name] for name in sort!(collect(keys(specs)))]) + end + Pkg.instantiate() + finally + # Transitive master dependencies must not become stale direct dependencies. + write(project_path, original) + end + # Keep extra-only master paths in the manifest for Julia 1.10 test sandboxes. + # Resolving again after restoring Project.toml would prune those entries. + return nothing +end + +if abspath(PROGRAM_FILE) == @__FILE__ + isempty(ARGS) && error("usage: julia instantiate_master.jl PACKAGE_DIRECTORY [WORKSPACE_DIRECTORY]") + instantiate_master(ARGS[1]; workspace=length(ARGS) > 1 ? ARGS[2] : dirname(realpath(ARGS[1]))) +end diff --git a/scripts/test_master.jl b/scripts/test_master.jl new file mode 100644 index 0000000..464cd38 --- /dev/null +++ b/scripts/test_master.jl @@ -0,0 +1,53 @@ +# A dedicated test environment keeps extra-only master dependencies reachable on +# Julia 1.10 and leaves the package Project.toml unchanged for Aqua checks. +using Pkg, TOML +include(joinpath(@__DIR__, "master_dependencies.jl")) +package_root = dirname(@__DIR__) +project_path = joinpath(package_root, "Project.toml") +original_project = read(project_path, String) +project = TOML.parse(original_project) +deps = copy(get(project, "deps", Dict{String,Any}())) +extras = merge(get(project, "weakdeps", Dict()), get(project, "extras", Dict())) +for name in get(get(project, "targets", Dict()), "test", String[]) + deps[name] = extras[name] +end +deps[project["name"]] = project["uuid"] +compat = Dict(name => value for (name, value) in get(project, "compat", Dict()) + if name == "julia" || haskey(deps, name)) +sources = Dict{String,Any}() +for name in keys(deps) + if name == project["name"] + sources[name] = Dict("path" => package_root) + elseif haskey(WORKSPACE_PACKAGES, name) + folder, _ = WORKSPACE_PACKAGES[name] + sources[name] = Dict("path" => joinpath(dirname(package_root), folder)) + elseif haskey(get(project, "sources", Dict()), name) + source = copy(project["sources"][name]) + haskey(source, "path") && (source["path"] = abspath(joinpath(package_root, source["path"]))) + sources[name] = source + end +end +environment = joinpath(package_root, ".master-test-env", "v$(VERSION.major).$(VERSION.minor)") +mkpath(environment) +open(joinpath(environment, "Project.toml"), "w") do io + TOML.print(io, Dict("deps" => deps, "compat" => compat, "sources" => sources)) +end +instantiate_master(environment; workspace=dirname(package_root), owner=package_root) +read(project_path, String) == original_project || error("test setup changed package Project.toml") +installed = Pkg.dependencies() +for (name, uuid) in deps + if name == project["name"] || haskey(WORKSPACE_PACKAGES, name) + expected = name == project["name"] ? package_root : + joinpath(dirname(package_root), WORKSPACE_PACKAGES[name][1]) + realpath(installed[Base.UUID(uuid)].source) == realpath(expected) || + error("test environment did not retain the master checkout of $name") + end +end +println("Master test environment: ", environment) +if !("--setup-only" in ARGS) + depwarn = get(ENV, "JULIA_TEST_DEPWARN", "yes") + depwarn in ("yes", "no", "error") || error("invalid JULIA_TEST_DEPWARN") + coverage = get(ENV, "JULIA_TEST_COVERAGE", "false") == "true" ? "user" : "none" + test_file = joinpath(package_root, "test", "runtests.jl") + run(`$(Base.julia_cmd()) --startup-file=no --check-bounds=yes --depwarn=$depwarn --code-coverage=$coverage --project=$environment $test_file`) +end diff --git a/src/doubly.jl b/src/doubly.jl index c924227..1cdf1bf 100644 --- a/src/doubly.jl +++ b/src/doubly.jl @@ -104,7 +104,7 @@ function fit_doubly(data::FDA.MultivariateFunctionalData; lambda, "grid, bandwidth, noise_variance and nondefault kernel require irregular nodes for fit_doubly")) _dense_parts(data) fpca = FDA.fit(FDA.FPCA, data; basis=:marginal, - ncomponents=ncomponents, maxK=ncomponents, method=:dense) + ncomponents=ncomponents, max_k=ncomponents, method=:dense) return _fit_doubly(fpca; lambda, threshold, method=:dense_reconstruction) end all(c -> c isa FDA.IrregularFunctionalData, data.components) || @@ -149,7 +149,7 @@ function fit_doubly(data::FDA.MultivariateFunctionalData; lambda, smoothing_kernel = kernel === :gaussian ? KernelSmoothing.GaussianKernel() : KernelSmoothing.EpanechnikovKernel() fpca = FDA.fit(FDA.FPCA, data; basis=:marginal, method=:pace, - grid=g, ncomponents=ncomponents, maxK=length(g), + grid=g, ncomponents=ncomponents, max_k=length(g), mean_bandwidth=bandwidth, covariance_bandwidth=bandwidth, diagonal_bandwidth=bandwidth, kernel=smoothing_kernel, weighting=:subj, binning=nothing, noise_variance=noise, diff --git a/src/glasso.jl b/src/glasso.jl index a6f0f51..7b6cb00 100644 --- a/src/glasso.jl +++ b/src/glasso.jl @@ -82,7 +82,7 @@ Solver defaults retain FGM's ADMM accuracy and iteration choices; they are numerical controls, rather than defaults from the paper. Dense inputs are complete FDA multivariate grid data. FDA's marginal FPCA uses trapezoidal quadrature, `n-1` eigenvalues, and its mean-alignment sign rule. Explicit -`maxK=ncomponents` preserves the requested FGM truncation. All resolved FPCA +`max_k=ncomponents` preserves the requested FGM truncation. All resolved FPCA and graph tuning choices are stored on the result and shown by `show`. """ function functional_glasso(S::AbstractMatrix{<:Real}, block_sizes, lambda::Real; @@ -168,7 +168,7 @@ end function functional_glasso(data::FDA.MultivariateFunctionalData, lambda::Real; ncomponents::Integer, kwargs...) _dense_parts(data) - fpca = FDA.fit(FDA.FPCA, data; ncomponents, maxK=ncomponents, basis=:marginal) + fpca = FDA.fit(FDA.FPCA, data; ncomponents, max_k=ncomponents, basis=:marginal) fit = functional_glasso(FDA.score_array(fpca), lambda; kwargs...) FGLassoResult(fit.precision,fit.sparse_precision,fit.covariance,fit.block_sizes, fit.adjacency,fit.diagnostics,fpca,fit.settings) diff --git a/src/neighborhood.jl b/src/neighborhood.jl index 7282eda..7fc5592 100644 --- a/src/neighborhood.jl +++ b/src/neighborhood.jl @@ -235,7 +235,7 @@ This differs from regressing the separate marginal FPCA scores of each node. Tuning is user-supplied; see [`select_neighborhood_scv`](@ref) for Algorithm 2. The default OR rule and zero threshold retain FGM's graph convention; `center=true` centers scores. Solver defaults are FGM numerical controls. -The dense route stores the FDA fit, including explicit `maxK=ncomponents`, +The dense route stores the FDA fit, including explicit `max_k=ncomponents`, the `n-1` eigenvalue convention and FDA's sign rule. `settings` stores all resolved graph and solver choices. @@ -251,7 +251,7 @@ end function _neighborhood_target_scores(data::FDA.MultivariateFunctionalData, ncomponents) values, grid = _dense_parts(data) - fpca = FDA.fit(FDA.FPCA, data; ncomponents, maxK=ncomponents, basis=:marginal) + fpca = FDA.fit(FDA.FPCA, data; ncomponents, max_k=ncomponents, basis=:marginal) n,p,t = size(values) m = ncomponents bases = cat((b.values for b in fpca.bases)...; dims=3) diff --git a/src/separable.jl b/src/separable.jl index 43a2aee..86b18f0 100644 --- a/src/separable.jl +++ b/src/separable.jl @@ -207,14 +207,14 @@ are graphs of the individual projected score vectors; no equivalence to the full-process conditional independence graph is asserted. The defaults retain FGM's two-component truncation, equal penalty mixture, and graph threshold. FDA's pooled FPCA uses `n-1` eigenvalues and explicitly -sets `maxK=ncomponents`; graph covariance scaling remains `1/n`. +sets `max_k=ncomponents`; graph covariance scaling remains `1/n`. """ function fit_separable(data::FDA.MultivariateFunctionalData; lambda::Real, alpha::Real=0.5, ncomponents::Integer=2, threshold::Real=1e-6, kwargs...) isfinite(threshold) && threshold >= 0 || throw(ArgumentError("threshold must be finite and nonnegative")) _dense_parts(data) - fpca = FDA.fit(FDA.FPCA, data; ncomponents, maxK=ncomponents, basis=:pooled) + fpca = FDA.fit(FDA.FPCA, data; ncomponents, max_k=ncomponents, basis=:pooled) scores = FDA.score_array(fpca) n, p, m = size(scores) correlations = zeros(p, p, m) diff --git a/src/tuning.jl b/src/tuning.jl index 92afc9e..ce786cb 100644 --- a/src/tuning.jl +++ b/src/tuning.jl @@ -120,7 +120,7 @@ function _fgm_fold_prediction(data::FDA.MultivariateFunctionalData, training_tim degree::Integer=3, penalty::Real=0) ncomponents <= nbasis || throw(ArgumentError("ncomponents must not exceed nbasis")) smoothed = _fgm_training_smoother(data, training_times; nbasis, degree, penalty) - fpca = FDA.fit(FDA.FPCA, smoothed; basis=:marginal, ncomponents, maxK=ncomponents) + fpca = FDA.fit(FDA.FPCA, smoothed; basis=:marginal, ncomponents, max_k=ncomponents) reason = _fgm_retained_rank_reason(fpca, ncomponents) isempty(reason) || throw(ArgumentError(reason)) (; prediction=FDA.reconstruct(fpca), smoothed, fpca) @@ -152,7 +152,7 @@ This chooses `L` and `M`; it does not select the graph regularization penalty. Default grids `5:10` and `1:6`, five randomized folds, the current default RNG, cubic splines (`degree=3`) and no penalty (`penalty=0`) retain FGM's existing choices. The default common grid partition is an FGM implementation choice. -FDA marginal dense FPCA is fitted at each candidate count with `maxK=M`, +FDA marginal dense FPCA is fitted at each candidate count with `max_k=M`, so the requested fixed count is available up to FGM's `n-1` limit. All other FPCA settings use FDA's documented dense defaults and are stored on `fpca`. @@ -209,7 +209,7 @@ function select_fgm_components(data::FDA.MultivariateFunctionalData; nbasis_grid # Fit candidates separately so a rank-deficient larger count # cannot invalidate a smaller, valid candidate. fpca = FDA.fit(FDA.FPCA, smoothed; basis=:marginal, - ncomponents=M, maxK=M) + ncomponents=M, max_k=M) reason = _fgm_retained_rank_reason(fpca, M) isempty(reason) || throw(ArgumentError(reason)) catch err diff --git a/test/doubly.jl b/test/doubly.jl index 6d63af0..8c4fdb3 100644 --- a/test/doubly.jl +++ b/test/doubly.jl @@ -69,7 +69,7 @@ end @test node.settings.diagonal_bandwidth == 0.3 @test node.settings.weighting == :subj @test node.settings.binning === nothing - @test node.settings.maxK == length(grid) + @test node.settings.max_k == length(grid) @test node.settings.eigen_tol == 100eps(Float64) @test node.eigenvalues[1] ≈ 1 atol=1e-8 @test node.basis.values[:,1] ≈ ones(9) atol=1e-8 diff --git a/test/fpca.jl b/test/fpca.jl index 6523951..5c51302 100644 --- a/test/fpca.jl +++ b/test/fpca.jl @@ -14,7 +14,7 @@ end d = FunctionalGraphicalModels._dense_data(values, grid) for basis in (:marginal, :pooled) - fit = FDA.fit(FDA.FPCA, d; ncomponents=2, maxK=2, basis) + fit = FDA.fit(FDA.FPCA, d; ncomponents=2, max_k=2, basis) scores = FDA.score_array(fit) @test size(scores) == (4, 2, 2) @test all(b -> size(b.values) == (6, 2), fit.bases) diff --git a/test/glasso.jl b/test/glasso.jl index ae58e2d..fb9933a 100644 --- a/test/glasso.jl +++ b/test/glasso.jl @@ -56,6 +56,6 @@ end @test fit.adjacency == direct.adjacency @test fit.settings.lambda == 0.1 @test fit.settings.threshold == 1e-6 - @test all(s -> s.maxK == 2, fit.fpca.settings.node_settings) + @test all(s -> s.max_k == 2, fit.fpca.settings.node_settings) @test occursin("settings=", sprint(show, fit)) end diff --git a/test/neighborhood.jl b/test/neighborhood.jl index 48b0f31..0ba3091 100644 --- a/test/neighborhood.jl +++ b/test/neighborhood.jl @@ -71,7 +71,7 @@ end values[i,k,s] = 2a[i,k]*sinpi(k*grid[s]) + 0.3b[i,k]*cospi(grid[s]) end data = FunctionalGraphicalModels._dense_data(values,grid) - fpca = FDA.fit(FDA.FPCA, data; ncomponents=2, maxK=2, basis=:marginal) + fpca = FDA.fit(FDA.FPCA, data; ncomponents=2, max_k=2, basis=:marginal) fit = functional_neighborhood(data,0.08; ncomponents=2) for j in 1:p projected = zeros(n,p,2) @@ -85,7 +85,7 @@ end @test size(fit.target_bases) == (t,2,p) @test fit.fpca isa FDA.MultivariateFPCA @test fit.settings.rule === :or - @test all(s -> s.maxK == 2, fit.fpca.settings.node_settings) + @test all(s -> s.max_k == 2, fit.fpca.settings.node_settings) @test occursin("settings=", sprint(show, fit)) end diff --git a/test/separable.jl b/test/separable.jl index 3ecdf6c..9b2bf37 100644 --- a/test/separable.jl +++ b/test/separable.jl @@ -51,7 +51,7 @@ end @test fit.diagnostics.converged @test fit.fpca isa FDA.MultivariateFPCA @test fit.fpca.basis == :pooled - @test fit.fpca.settings.maxK == m + @test fit.fpca.settings.max_k == m @test size(FDA.score_array(fit.fpca)) == (n, p, m) @test length(fit.fpca.pooled.eigenvalues) == m @test all(>(0), fit.fpca.pooled.eigenvalues) @@ -110,7 +110,7 @@ end @test fit.diagnostics.converged @test fit.settings.threshold == 1e-6 @test fit.diagnostics.settings.alpha == 0.5 - @test fit.fpca.settings.maxK == 2 + @test fit.fpca.settings.max_k == 2 @test occursin("settings=", sprint(show, fit)) @test size(fit.precisions) == (p,p,2) @test all(fit.adjacency .== dropdims(any(fit.component_adjacency; dims=3); dims=3))