Standard GMRES is well-implemented in Julia. LGMRES, GMRES-E, and PD-GMRES are not in
any Julia package — all three must be ported. However, the fallback dispatch cases (m == n
→ unrestarted GMRES, d == 0 / l == 0 → standard restarted GMRES) can delegate to
Krylov.jl instead of reimplementing them.
| Dependency | Replaces | Notes |
|---|---|---|
Krylov.jl |
MATLAB built-in gmres |
Fallback dispatch in all three solvers; the LGMRES first-cycle call |
LinearAlgebra (stdlib) |
eigs, norm, \, etc. |
eigen(F, G) replaces eigs(F, G, k, 'LM') — valid because F, G are small dense matrices (size m+d) |
SparseArrays (stdlib) |
MATLAB sparse | Transparent to algorithm logic |
MAT.jl |
load('data/X.mat') |
Test data only — .mat files stay as-is |
Test (stdlib) |
MOxUnit | @test, @test_throws, @testset |
No Arpack.jl needed. The generalized eigenvalue problem in harmonic_ritz_vectors
operates on matrices of size s×s where s = m+d (typically < 30). LinearAlgebra.eigen(F, G) computes all eigenvalues of a small dense matrix, which is cleaner and faster than calling
Arpack for a handful of eigenpairs.
plane_rotations can be ported directly. Julia's LinearAlgebra.givens exists but the
naive loop (building rotation matrix P and multiplying) is fine at this subspace size, and a
direct port minimises translation risk.
KrySBAS.jl/
├── Project.toml
├── src/
│ ├── KrySBAS.jl # module, includes, exports
│ ├── solvers/
│ │ ├── gmres_e.jl
│ │ ├── lgmres.jl
│ │ └── pd_gmres.jl
│ └── utils/
│ ├── plane_rotations.jl
│ ├── modified_gram_schmidt_arnoldi.jl
│ ├── augmented_gram_schmidt_arnoldi.jl
│ ├── harmonic_ritz_vectors.jl
│ └── pd_rule.jl
└── test/
├── runtests.jl
├── test_plane_rotations.jl
├── test_modified_gram_schmidt_arnoldi.jl
├── test_augmented_gram_schmidt_arnoldi.jl
├── test_harmonic_ritz_vectors.jl
├── test_pd_rule.jl
├── test_gmres_e.jl
├── test_lgmres.jl
├── test_pd_gmres.jl
└── test_poisson.jl
These differences appear in every file:
nargin→ default arguments. Everyif (nargin < k) || isempty(x); x = default; endbecomes a Julia keyword argument with a default:function foo(A, b; m=min(size(A,1), 10), ...). Usenothingwhen a default must be computed from other args (e.g.maxitdepends on bothnandm).error()/warning()→ same names in Julia, different string syntax. Useerror("...")or@warn "...".- MATLAB
eps→ Juliaeps(Float64). tic/toc→time()captured ast0 = time(); ...; elapsed = time() - t0, or use the@elapsedmacro.size(A)returns a tuple in Julia —n, _ = size(A)works;[n, ~] = size(A)does not exist.isemptyexists in Julia and behaves the same.zeros(n, 1)→zeros(n). Julia vectors are 1D (Vector{Float64}), not column matrices. Preferzeros(n)andVectorthroughout.\for backslash solve works the same.- Broadcasting: MATLAB's
a .* b→ Julia'sa .* b(same syntax, but Julia's dot-broadcasting is more general). fliplr(v)→reverse(v, dims=2)for matrices, orreverse(v)for vectors.
Each step ends with a green test run before moving on.
- Create the package with
] generate KrySBASin Julia. - Write
Project.tomlwith[deps]forKrylovandMAT, and[compat]bounds. - Write
src/KrySBAS.jl: empty module withincludestubs and placeholder exports. - Write
test/runtests.jlthatincludes each test file. - Write a minimal GitHub Actions workflow (
.github/workflows/julia_tests.yml) usingjulia-actions/setup-julia+julia -e 'using Pkg; Pkg.test("KrySBAS")'. - Write a
julia_style.ymlusingJuliaFormatter.jlto enforce formatting (replaces MISS_HIT).
Test checkpoint: Pkg.test() runs with zero test files — no errors, no output. CI passes
on an empty module.
Port plane_rotations.m directly. The signature becomes:
function plane_rotations(H::Matrix, beta::Real)::Tuple{Matrix, Vector}Key differences: [~, m] = size(H) → m = size(H, 2). The loop body is otherwise identical.
Tests: Port plane_rotations tests verifying:
- A known 2×1 Hessenberg example gives expected upper-triangular output.
g[1] == betabefore rotations.- Output is type-stable (both outputs are
Float64).
Direct port. Signature:
function modified_gram_schmidt_arnoldi(A, v::Vector, m::Int)
# returns (H, V, m_updated)The early-exit on H[j+1, j] == 0 (happy breakdown) stays identical.
Tests:
- Identity matrix + unit vector → H and V have expected structure.
- Happy breakdown triggers early return with truncated
m. - Output V has orthonormal columns (
V' * V ≈ I).
Direct port. Signature:
function augmented_gram_schmidt_arnoldi(A, v::Vector, m::Int, appendV::Matrix)
# returns (H, V, s)The logic around the augmented block (j <= m vs. j > m) is identical.
Tests:
- With
appendVof zero columns (k=0) should matchmodified_gram_schmidt_arnoldioutput. - Augmented columns increase
scorrectly. Vcolumns are orthonormal.
This is the only utility that needs a substantive change. Replace:
[E2, D2] = eigs(F, G, k, 'LM', opts);with:
vals, vecs = eigen(F, G) # all eigenvalues of small dense problemThen sort abs.(vals) ascending and take the first k eigenvectors. This replaces the
'LM' + sort-ascending pattern in the MATLAB code with a cleaner all-eigenvalues-then-select
approach (valid because s is always small).
The complex-conjugate splitting logic ports directly — isreal(v) → !isreal(v), and
real(v), imag(v) all exist in Julia.
Tests:
- Known small (3×3, 4×4) generalized eigenvalue problem with precomputed harmonic Ritz vectors.
- Complex eigenvector case: verify split produces two real vectors with correct norms.
- Output has
kcolumns.
Pure arithmetic, direct port. Signature:
function pd_rule(m, n, mInitial, mMin, mMax, mStep,
res::Vector, iter::Int, alphaP, alphaD)::Tuple{Int, Int}Return a (mj, mInitial) tuple instead of the [mj mInitial] row vector.
Tests: Port existing numerical tests with known res vectors — verify output m matches
expected values for the iter > 3, iter > 2, and iter <= 2 branches.
Signature:
function gmres_e(A, b::Vector;
m::Int = min(size(A, 1), 10),
d::Int = -1, # -1 signals "use default min(m, 3)"
tol::Float64 = 1e-6,
maxit::Int = min(size(A, 1), 10),
x0::Union{Vector, Nothing} = nothing,
eigstol::Float64 = 1e-6)Fallback dispatch (replacing MATLAB's built-in gmres):
# m == n: unrestarted
x, stats = Krylov.gmres(A, b; atol=0.0, rtol=tol, itmax=n)
# m < n, d == 0: standard restarted GMRES
x, stats = Krylov.gmres(A, b; restart=m, atol=0.0, rtol=tol, itmax=maxit * m)The rest of the algorithm (Arnoldi loop, plane rotations, harmonic Ritz update) calls the utility functions from Steps 1–4.
Tests: Port all tests from test_gmres_e.m:
- Input validation (
@test_throws). - Fallback cases (identity matrix,
m == n,d == 0). - Embree 3×3 toy example.
- Sherman1, Sherman4 sparse matrices loaded via
MAT.load("data/sherman1.mat").
The LGMRES first-cycle call is currently gmres(A, b, m+l, tol, 1, ...) (one cycle of
GMRES(m+l)). In Julia, replace with:
x, stats = Krylov.gmres(A, b; restart=m + l, atol=0.0, rtol=tol,
itmax=m + l, x0=x0)The rest (augmented Arnoldi loop, zMat history, error vector accumulation) is a direct port
of the while loop.
Tests: Port all tests from test_lgmres.m — same matrix suite as GMRES-E.
The unrestarted path:
x, stats = Krylov.gmres(A, b; atol=0.0, rtol=tol, itmax=maxit, x0=x0)The restarted path uses modified_gram_schmidt_arnoldi + plane_rotations + pd_rule already
ported above — direct translation of the while loop.
Tests: Port all tests from test_pd_gmres.m.
Port test_poisson.m. This exercises all three solvers on a real PDE-derived system. Verify:
- All three solvers converge on the Poisson problem.
relresvec[end] < tolfor each solver.
- Update
julia_tests.ymlto run the full test suite including the integration test. - Run
JuliaFormatter.format(".")and commit formatted code. - Add
codecovupload step (Julia coverage viaPkg.testwithcoverage=true). - Remove the MATLAB workflows, or keep them temporarily if a parallel maintenance period is needed.
# 1. Install juliaup (version manager)
brew install juliaup
juliaup add release # installs latest stable Julia
# 2. VS Code extension: "Julia" (julialang.language-julia)
# 3. In the Julia REPL — add development dependencies
] # enters package mode
add Krylov MAT JuliaFormatterRunning tests locally at any step:
] test KrySBAS # from the package root in pkg mode
# or equivalently
using Pkg; Pkg.test()Running a single test file during development:
include("test/test_gmres_e.jl")