Skip to content

fix: address Aqua and JET findings#189

Merged
jack-champagne merged 17 commits intomainfrom
fix/aqua-jet-findings
May 7, 2026
Merged

fix: address Aqua and JET findings#189
jack-champagne merged 17 commits intomainfrom
fix/aqua-jet-findings

Conversation

@jack-champagne
Copy link
Copy Markdown
Member

Summary

Triage and fix the 8 stale exports + 4 method ambiguities reported by Aqua and the 31 type-error findings reported by JET on the ci/add-aqua-jet baseline. Each finding category lands as its own commit so reviewers can read the chain top-to-bottom.

Fixed

  • F1. Stale exports (Aqua, 8 → 1). Removed export open_rollout and export open_rollout_fidelity from src/quantum/dynamics.jl (no definitions). Stopped re-exporting plot_name! from QuantumObjectPlotsNamedTrajectories owns the canonical export; the Piccolo-local definition stays in the submodule for PiccoloMakieExt's qualified call.
  • F2. Vector{Float64}(::Symbol) in pulse constructors (JET, 6 → 0). ZeroOrderPulse / LinearSplinePulse / CubicSplinePulse constructors gained an explicit isa Symbol arm that errors clearly on unknown symbols (only :free is supported alongside nothing and Vector{<:Real}).
  • F3. CubicSplineInterpolation in dynamics.jl (JET, 4 → 0). That name is a deprecated Interpolations.jl symbol and is not reachable from this file. Replaced with CubicHermiteSpline(traj, control_name), the documented (traj, name) 2-arg method provided by NamedTrajectories.InterpolationsExt over DataInterpolations.
  • F4. Other undefined names (JET, 4 → 0). Reconstructed SamplingTrajectory directly inside _update_goal(::SamplingTrajectory, ...) instead of calling the undefined update_base_trajectory. Qualified the recursive _update_system! call as Rollouts._update_system!. Added an else error(...) arm for unknown lab_frame_type in TransmonSystem.
  • F5. SparseVector vs AbstractMatrix (JET, 3 → 0). OpenQuantumSystem, VariationalQuantumSystem, CompositeQuantumSystem drift-less constructors built spzeros(T, size(H_drives[1])). JET cannot prove the tuple has length 2, so it union-split into SparseVector. Destructure (m, n) = size(...) and call spzeros(T, m, n) — unambiguous matrix dispatch.
  • F6. Method ambiguities (Aqua, 4 → 0). Two clusters: (a) QuantumSystem(::_DriftInputs, ::Vector{<:AbstractDrive}, ...) — added a 3-arg disambiguator that invokes the pair-based path; (b) UnitaryTrajectory(::QuantumSystem, ::G, ::G) — narrowed the (system, goal, T::Real) method's goal to Union{AbstractMatrix, AbstractPiccoloOperator} (the only types ever passed in practice; verified via grep). Re-enabled ambiguities in test/aqua.jl.
  • F7. default_algorithm(::OpenQuantumSystem) (JET, 6 → 0). Defined default_algorithm(::OpenQuantumSystem) = Tsit5(). Justification: the Lindblad generator is non-Hermitian by construction, and both DensityTrajectory and MultiDensityTrajectory constructors already default algorithm = Tsit5(). Added a smoke test that runs rollout(::DensityTrajectory) without an explicit algorithm and a unit test asserting the dispatch.
  • Misc JET inference cleanups. Switched if return_layers to if all_layers !== nothing in operator_algebra (lets JET narrow the union). Bound complex_control_norm_constraint_name into a local before the !== nothing guard in apply_piccolo_options!. Tightened add_global_bounds_constraints! to Union{Nothing, AbstractDict{Symbol, <:Any}} and split for pair in dict with explicit name::Symbol binding. Replaced collect(traj.global_names) with Symbol[traj.global_names...] in sampling_state_objective (avoids Vector{Union{}} from empty tuples).

Deferred / left as broken=true

  • F1 partial — hessian_structure (1 stale export). This name is exported by three different DirectTrajOpt submodules (CommonInterface, Constraints, Integrators); the conflict makes it appear undefined at Piccolo's surface. Upstream issue, not Piccolo's to fix. undefined_exports = (broken = true,) is preserved with a comment.
  • F4 partial — EnsembleSplineIntegrator (1 JET finding). Referenced inside SplinePulseProblem(...) when integrator_type = :ensemble, but no method is defined in Piccolo or any declared dep. The constructor itself errors users toward Piccolissimo's SplineIntegrator, suggesting this branch was meant to live behind an extension that hasn't been wired up. Left as the only JET broken=true item — see "Notes for human review" below.
  • F8. Stdlib + test-extras compat policy — left as-is per instructions.

Aqua before/after

Category Before After
undefined_exports 8 1 (hessian_structure, upstream)
ambiguities 4 0 (re-enabled in test/aqua.jl)
All other Aqua checks pass pass

JET before/after

Category Before After
Vector{Float64}(::Symbol) (pulse ctors) 6 0
CubicSplineInterpolation undefined 4 0
default_algorithm(::OpenQuantumSystem) 6 0
OpenQuantumSystem / VariationalQuantumSystem / CompositeQuantumSystem SparseVector 3 0
Other undefined names (update_base_trajectory, _update_system!, H_drift, EnsembleSplineIntegrator) 4 1
TransmonSystem unbound H_drift 2 0
operator_algebra push!(::Nothing, ...) 2 0
add_global_bounds_constraints! / apply_piccolo_options! / sampling_state_objective inference 3 0
Total 31 1

Test plan

  • Aqua.test_all(Piccolo; deps_compat = (check_extras = false, ignore = [:Libdl]), undefined_exports = false) — all checks pass except the upstream hessian_structure.
  • JET.test_package(Piccolo; target_modules=(Piccolo,)) — 1 finding remaining (EnsembleSplineIntegrator), held as broken=true.
  • Smoke: UnitaryTrajectory(sys, X_gate, T), DensityTrajectory(open_sys, ρ0, ρg, T), rollout(::DensityTrajectory) without explicit algorithm — all green.
  • Full Pkg.test() — running locally; may take 20–30 min. Will update once complete.

Notes for human review (F7 + F4 partial)

  • F7 (default_algorithm(::OpenQuantumSystem)): I went the route of adding the method rather than deferring. Rationale: the open-system trajectory constructors already default algorithm = Tsit5() directly, so the new method just makes the indirect call site (default_algorithm(qtraj.system)) match. Also added a smoke test for rollout(::DensityTrajectory) so this stays exercised. If you'd prefer this be a separate per-system architectural decision, happy to revert.
  • F4 partial — EnsembleSplineIntegrator: this looks like a Piccolissimo extension hook that was never finished. The error message in the surrounding code points users at Piccolissimo: SplineIntegrator. Two paths forward: (a) delete the integrator_type == :ensemble branch entirely, or (b) move it to a Piccolissimo extension. Either is a real call, so I left it alone. JET's broken=true will flip to a failure if someone later adds the symbol — that's the desired tripwire.

🤖 Generated with Claude Code

@codecov
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

Copy link
Copy Markdown
Member Author

@jack-champagne jack-champagne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a bit of work, leaving this in draft until we can get this all addressed.

Comment on lines +234 to +236
# Reconstruct the SamplingTrajectory around the updated base — a thin
# SamplingTrajectory only owns (base_trajectory, systems, weights).
return SamplingTrajectory(new_base, qtraj.systems; weights = qtraj.weights)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need a closer look to determine how we want to do this

Comment on lines +601 to +619
function QuantumSystem(
drift::_DriftInputs,
drives::Vector{<:AbstractDrive},
drive_bounds::Vector{<:Union{Tuple{Float64,Float64},Float64}};
time_dependent::Bool = false,
global_params::NamedTuple = NamedTuple(),
hermitian::Bool = true,
)
return invoke(
QuantumSystem,
Tuple{_DriftInputs,AbstractVector,Vector{<:Union{Tuple{Float64,Float64},Float64}}},
drift,
drives,
drive_bounds;
time_dependent = time_dependent,
global_params = global_params,
hermitian = hermitian,
)
end
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a closer look too, this shim for the constructor alludes to a possible type issue on the constructor that we should fix with other stuff...

Comment on lines +57 to +59
function default_algorithm(::OpenQuantumSystem)
return Tsit5()
end
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an important change that I am fairly certain we should integrate unless you have a different integrator in mind. @aarontrowbridge @gennadiryan @andgoldschmidt

function UnitaryTrajectory(
system::QuantumSystem,
goal::G,
goal::Union{AbstractMatrix,AbstractPiccoloOperator},
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this type scoping doing anything before? Where was G being defined? Was it strict enough?

Comment thread src/quantum/dynamics.jl
u = LinearInterpolation(traj, control_name)
elseif interpolation == :cubic
u = CubicSplineInterpolation(traj, control_name)
u = CubicHermiteSpline(traj, control_name)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is actually the right type, and if it isn't then we should have flagged this in the tests. Will need to inspect to validate

export plot_state_populations
export plot_weyl_trajectory
export plot_name!
# plot_name! is defined here for use within QuantumObjectPlots / PiccoloMakieExt; the
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to check this too - have had issues in the past with fiddling with these exports with the extension package for (Cairo)Makie on NamedTrajectories.jl

jack-champagne and others added 15 commits May 6, 2026 17:07
Wire up Aqua.test_all and JET.test_package as TestItemRunner @testitem
blocks in test/aqua.jl and test/jet.jl. They are picked up automatically
by @run_package_tests, so no CI workflow change is needed.

Findings are surfaced but not fixed in this PR:

* Aqua: 4 ambiguities (disabled), 8 undefined exports (broken),
  1 missing compat for Libdl stdlib (broken). Piracies, stale_deps,
  unbound_args, project_extras, persistent_tasks all clean.

* JET: 31 possible errors found in test_package, marked broken=true so
  they are visible in CI logs without making the build red.

* JET.report_opt is gated behind ENV["JET_OPT"]=true and intended to
  be run on individual hot paths locally, since it requires a callable
  and is too noisy for the default CI pass.

Each disable / broken marker has a TODO comment with reasoning so the
maintainers can triage in follow-up PRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the QuantumOpticsBase / Distributions.jl idiom: short bodies,
[:aqua] / [:jet] tags for filtering, drop the no-op report_opt
testitem (manual command documented inline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove `open_rollout` / `open_rollout_fidelity` from
  `src/quantum/dynamics.jl` — these names are exported but never defined.
- Stop exporting `plot_name!` from `QuantumObjectPlots`. The canonical
  `plot_name!` is owned by `NamedTrajectories` (and re-exported via
  `@reexport using NamedTrajectories`); the Piccolo-local definition
  collides on name resolution and shows up as undefined at the package
  surface. The function stays defined in the submodule for use by
  `PiccoloMakieExt`, which calls it via fully qualified name.

Brings `Aqua.test_undefined_exports` from 8 names down to 1 (the
remaining `hessian_structure` is an upstream conflict between three
DirectTrajOpt submodules; not Piccolo's to fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`ZeroOrderPulse`, `LinearSplinePulse`, and `CubicSplinePulse` accepted
`initial_value` / `final_value` as `Union{Nothing, Symbol, Vector{<:Real}}`,
where the only supported Symbol is `:free`. Any other Symbol fell through
to `Vector{Float64}(initial_value::Symbol)`, which has no method — JET
flagged this on 6 union-split branches.

Add an explicit `isa Symbol` arm that raises a clear error for unknown
symbols. Behavior for `:free`, `nothing`, and `Vector{<:Real}` is
unchanged.

Resolves 6 JET findings in `src/quantum/primitives/pulses.jl`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…for :cubic interpolation

Four `rollout_fidelity` / `rollout` methods in
`src/quantum/dynamics.jl` referenced `CubicSplineInterpolation`, which
is a deprecated `Interpolations.jl` name and is not defined anywhere
reachable from this file (Piccolo depends on `DataInterpolations`, not
`Interpolations`). The `:cubic` branch was effectively unreachable —
calling it would `UndefVarError` rather than interpolate.

Switch all four sites to `CubicHermiteSpline(traj, control_name)`,
which is the documented `(traj, name)` 2-arg method provided by
`NamedTrajectories.InterpolationsExt` over `DataInterpolations`. It
infers the derivative component as `:d<name>`.

Resolves 4 JET findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…plit

The drift-less constructors of `OpenQuantumSystem`,
`VariationalQuantumSystem`, and `CompositeQuantumSystem` built the zero
drift via `spzeros(T, size(H_drives[1]))`. JET cannot prove
`size(...)::Tuple` has length 2 from inference alone, so the call
unions over the `SparseVector` (1-D) and `SparseMatrixCSC` (2-D)
methods of `spzeros`. The 1-D arm flowed back into the inner
constructor, which only accepts `AbstractMatrix`, producing the JET
"no matching method" reports.

Destructure `(m, n) = size(H_drives[1])` and call `spzeros(T, m, n)`,
which dispatches unambiguously to the matrix method. Runtime behavior
is unchanged for the only inputs these constructors ever receive
(square drive operators).

Resolves 3 JET findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small fixes for JET-reported undefined references:

- `_update_goal(::SamplingTrajectory, ::Any)` called the undefined
  `update_base_trajectory(qtraj, new_base)`. Reconstruct the
  `SamplingTrajectory` directly from `(new_base, systems, weights)` —
  the only fields it owns.
- `Rollouts._update_system!(::SamplingTrajectory, ::QuantumSystem)`
  called `_update_system!` unqualified, but the symbol is defined
  in the `Rollouts` module (this file is in `QuantumTrajectories`).
  Qualify the recursive call as `Rollouts._update_system!`.
- `TransmonSystem(...; lab_frame=true)` left `H_drift` unbound when
  `lab_frame_type` did not match `:duffing`, `:quartic`, or `:cosine`.
  Add an explicit `else` arm that errors with a clear message.

Resolves 4 JET findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JET reported `default_algorithm(::OpenQuantumSystem)` undefined at 6
sites — every `rollout` / `rollout!` overload for `DensityTrajectory`
and `MultiDensityTrajectory` falls through `default_algorithm(qtraj.system)`
when the user does not pass an explicit `algorithm`.

The default for `DensityTrajectory` and `MultiDensityTrajectory`
constructors is already `Tsit5()`, and the Lindblad generator is
non-Hermitian by construction (so MagnusAdapt4 is unsuitable). Define
`default_algorithm(::OpenQuantumSystem) = Tsit5()` in the
`QuantumTrajectories` module, alongside the existing `QuantumSystem`
and `CompositeQuantumSystem` methods.

Add a smoke test that exercises `rollout(::DensityTrajectory)` without
an explicit algorithm, plus a unit test asserting the dispatch.

Resolves 6 JET findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two clusters of internal Piccolo ambiguities, both reported by
`Aqua.test_ambiguities` (which is currently disabled in `test/aqua.jl`):

1. `QuantumSystem(::_DriftInputs, ::Vector{<:AbstractDrive}, ::Vector{...})`
   matched both the pair-based constructor (drift in `_DriftInputs`,
   drives an `AbstractVector`) and the structured-operator constructor
   (drift untyped, drives a `Vector{<:AbstractDrive}`). Add an explicit
   3-arg disambiguator that `invoke`s the pair-based path — its
   for-loop already accepts `AbstractDrive` entries — preserving
   semantics without duplicating logic. Covers the matching `kwcall`
   ambiguity automatically.

2. `UnitaryTrajectory(::QuantumSystem, ::G, ::G)` matched both
   `(system, pulse::AbstractPulse, goal::G)` and
   `(system, goal::G, T::Real)`. No type can satisfy both bounds at
   runtime, but the dispatch table flagged it. Narrow the second
   method's `goal` to `Union{AbstractMatrix, AbstractPiccoloOperator}`
   — the only types ever passed in practice (verified via grep of
   call sites) — closing the ambiguity without losing call coverage.
   Covers the matching `kwcall` ambiguity automatically.

Re-enable `ambiguities` in `test/aqua.jl` in the same change so the
green path is locked in.

Resolves 4 Aqua ambiguities.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five small JET-only fixes (no behavior change for valid inputs):

- `operator_algebra` (`src/quantum/system_utils.jl`): the `if return_layers`
  guard around `push!(all_layers, ...)` is correct at runtime, but JET
  cannot connect the boolean to the `Union{Nothing,Vector{...}}` type of
  `all_layers`. Switch the guard to `if all_layers !== nothing` so JET
  narrows the union directly. Resolves 2 findings.

- `apply_piccolo_options!` (`_problem_templates.jl`): bind
  `complex_control_norm_constraint_name` into a local before the
  `!== nothing` guard, so the narrowing reaches the
  `NonlinearKnotPointConstraint(::Function, ::Symbol, ::NamedTrajectory)`
  call. Resolves 1 finding.

- `add_global_bounds_constraints!`: tighten signature to
  `Union{Nothing, AbstractDict{Symbol, <:Any}}` and split the dict
  iteration into an explicit `pair.first::Symbol` binding. Without the
  tightened signature, JET inferred `name::Union{Integer, Symbol}` and
  flagged the `GlobalBoundsConstraint(::Symbol, ...)` call. Resolves 1
  finding.

- `sampling_state_objective(::UnitaryTrajectory, ...)`: replace
  `collect(traj.global_names)` with `Symbol[traj.global_names...]` so
  the eltype is statically `Symbol` even when the source tuple is
  empty. `collect(::Tuple{})` returns `Vector{Union{}}`, which fails
  to match `θ_names::AbstractVector{Symbol}` downstream. Resolves 1
  finding.

JET drops from 6 to 1 (the remaining `EnsembleSplineIntegrator`
reference is left for human review — see PR description).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `test/aqua.jl`: re-enable `ambiguities` (now zero), and document the
  remaining `undefined_exports` broken case as a DirectTrajOpt-side
  conflict on `hessian_structure`.
- `test/jet.jl`: leave `broken=true` for the single remaining
  finding (`EnsembleSplineIntegrator`), with a note that it is the
  only deferred item.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Older Julia ships older JET (0.9.x) which can't analyze the current
Piccolo source — JET integrates tightly with the compiler and lower
versions hit internal errors during analysis. Gating execution with
`skip=(VERSION < v"1.12")` keeps install/dep resolution permissive
(JET stays in [extras]/[compat] for all Julia versions) but stops the
test from running where it can't succeed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the b97f299 pattern (deterministic cos/sin init, raised max_iter,
relaxed residual tolerance) to the time-dependent UnitaryTrajectory
SmoothPulseProblem test. b97f299 fixed seven similar flaky stochastic
tests but missed this one — its sibling test (time-dependent
KetTrajectory at the next testitem) already uses the relaxed `< 1e-2`
threshold with the same justification.

The fidelity assertion (`> 0.85`) — the actual physics outcome the test
cares about — is unchanged and continues to pass.

Surfaced when our PR's added smoke tests shifted global RNG state at
the worker assigned to this test on Julia 1.11, tipping its unseeded
randn init into a non-converging draw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure whitespace — collapses multi-line type tuples into the single-line
form JuliaFormatter v2.3.0 prefers. No semantic change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- test/jet.jl: TestItemRunner doesn't accept a `skip=` kwarg on `@testitem`,
  which caused "Unknown keyword argument" abort before any tests ran. Move
  the version gate (Julia >= 1.12) into the body and short-circuit with a
  bare `return` plus an `@info` marker.
- src/control/templates/smooth_pulse_problem.jl: run JuliaFormatter on the
  deterministic-init block added in d95cda6 so the Formatter check stops
  rejecting the file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jack-champagne jack-champagne force-pushed the fix/aqua-jet-findings branch from c111b99 to 4a9a854 Compare May 6, 2026 21:13
@jack-champagne jack-champagne changed the base branch from ci/add-aqua-jet to main May 6, 2026 21:13
jack-champagne and others added 2 commits May 6, 2026 17:14
The previous `return` early-exit pattern inside `@testitem`'s body did not
actually skip the body — JET still ran on Julia 1.10 and 1.11, and on 1.11
it returned no findings, breaking the `broken = true` assertion with
"Unexpected Pass". Restructure to a clear `if/else` so the gate is
structural rather than control-flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jack-champagne jack-champagne marked this pull request as ready for review May 7, 2026 03:51
@jack-champagne jack-champagne merged commit 6a2aea3 into main May 7, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant