From 0f21bc110010409a49cdde4acffbc700dfe74d1c Mon Sep 17 00:00:00 2001 From: aaron Date: Thu, 27 Aug 2026 17:23:24 -0400 Subject: [PATCH 1/3] skills(piccolissimo): entitled usage skill Usage-altitude guide for the entitled Piccolissimo tier: spline-integrator selection for Piccolo problems, adjoint robustness objectives + RobustControlProblem, Gauss-Newton solver configuration (iteration caps, tolerances, eval_hessian / exact_hessian levers), Magnus algorithm selection + substep sizing, warm-start idioms. Delivery-mode caveat: issimo entitlement, HP-mode sysimage or private-registry sandbox; not in the default provisioned env. Content policy per spec-20260827-162500 A1.2/A1.5: every symbol verified against package source; configuration-level only (no Hessian/Jacobian structure, no module trees, no src/ paths, no how-to-modify recipes, no roadmap). Part of #615 --- .../extension/skills/piccolissimo/SKILL.md | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 packages/extension/skills/piccolissimo/SKILL.md diff --git a/packages/extension/skills/piccolissimo/SKILL.md b/packages/extension/skills/piccolissimo/SKILL.md new file mode 100644 index 00000000..b1b4201c --- /dev/null +++ b/packages/extension/skills/piccolissimo/SKILL.md @@ -0,0 +1,163 @@ +--- +name: piccolissimo +description: Piccolissimo.jl usage — the entitled fast-path tier for smooth-pulse problems. Spline-faithful integrators for Piccolo problems, adjoint robustness objectives, Gauss–Newton solver configuration, Magnus algorithm selection, and warm-start idioms. Use when authoring a solve.jl in a Piccolissimo-enabled environment. +surface: entitled +entitlement: issimo +--- + +# Piccolissimo — usage guide + +Piccolissimo is the entitled fast-path tier for smooth-pulse problems: you author +Piccolo problems exactly as usual (`SplinePulseProblem`, `SmoothPulseProblem`, +`UnitaryTrajectory`, …) and plug in Piccolissimo's faster integrators, robustness +objectives, and solver configuration on top. `using Piccolo, Piccolissimo` reaches +both surfaces. + +> **Delivery mode (read first).** Piccolissimo requires the **issimo entitlement**. +> It is delivered either as the **HP-mode prebuilt sysimage** or via a +> **private-registry sandbox environment** — it is **NOT in the default provisioned +> solve env**. Recipes in this skill run only in those environments; smoke-test +> signatures in the actual environment before launching. + +## Selecting the spline integrator + +Loading Piccolissimo registers a `SplineIntegrator` into Piccolo's integrator +registry. In direct problem construction you pass the integrator explicitly: + +```julia +using Piccolo, Piccolissimo + +sys = QuantumSystem(H_drift, H_drives, drive_bounds) # your model, as usual +pulse = CubicSplinePulse(u_init, du_init, times) # spline pulse +qtraj = UnitaryTrajectory(sys, pulse, U_goal) + +N = 50 +integrator = SplineIntegrator(qtraj, N) # spline-faithful dynamics +qcp = SplinePulseProblem(qtraj, N; integrator = integrator, Q = 100.0, du_bound = 10.0) + +solve!(qcp; max_iter = 200, tol = 1e-8) +``` + +Three facts worth internalizing: + +- **Piccolo deliberately has no `:spline` backend of its own.** Passing + `integrator_type = :spline` to `SplinePulseProblem` errors with instructions; + the only shipped Piccolo backend is `:pwc` (`BilinearIntegrator`), which models + the drive as piecewise-constant and ignores `:du` — a spline pulse optimized + against it is not the pulse its name promises. +- **`global_names` requires this integrator.** Optimizing global variables + (frequencies, couplings) with `SplinePulseProblem(qtraj, N; global_names = [...])` + errors unless you pass a globals-aware integrator — the spline integrator above, + constructed with `global_names = [:ω, ...]`. +- **Algorithm choice is a constructor kwarg.** `SplineIntegrator(qtraj, N; + alg = Tsit5Alg())` is the default; see the Magnus section below for the other + algorithms and when to reach for them. + +On ket and multiket trajectories the constructor additionally accepts +`exact_hessian` (request the exact second-order sensitivity path where implemented) +and `use_ket_sensitivity` (ket-level sensitivity propagation). Some combinations +error at construction rather than silently falling back — if a requested algorithm +does not support the sensitivity mode you asked for, you will know immediately. + +## Robustness objectives + +Two public objectives make a pulse robust to parameter error, and one wrapper +composes them into a solvable problem: + +- `AdjointRobustnessObjective` — operator-space adjoint sensitivity objective. +- `KetAdjointRobustnessObjective(integrator, error_operators, goal, traj; Q = 1.0)` + — ket-space variant, constructed from an exponential integrator over a + `KetTrajectory` (or its multiket sibling over `MultiKetTrajectory` with a vector + of goals). +- `RobustControlProblem(qcp; kwargs...)` — wraps an existing solved problem to + minimize error susceptibility subject to a fidelity floor. Deep-copies the + trajectory and constraints, composes the robustness term with the original + regularizers, and adds a `FinalUnitaryFidelityConstraint`-style floor — the same + composition pattern as `MinimumTimeProblem`. + +```julia +# qcp: a solved unitary problem (spline- or bilinear-integrated) +rcp = RobustControlProblem(qcp; + error_operators = [E_detune, E_amp], # Hermitian error matrices + sys = sys, # REQUIRED when qcp integrates with SplineIntegrator + final_fidelity = 0.9999, # fidelity floor constraint + Q_robustness = 1.0, # weight on the robustness term +) +solve!(rcp; max_iter = 200) +``` + +`keep_infidelity_objective = true` keeps the original infidelity term in the +objective alongside robustness (default `false` — the floor constraint carries +fidelity). `error_operators` are the Hermitian matrices whose susceptibility you +want minimized (e.g. a detuning shift, an amplitude-scale error). + +## Gauss–Newton solver configuration + +The spline integrator's default second-order mode is Gauss–Newton. Configuration +guidance, all at the `solve!`/constructor level: + +- **Iteration caps and tolerances** pass straight through `solve!` as Ipopt + options: `solve!(qcp; max_iter = 200, tol = 1e-8, constr_viol_tol = 1e-6, + acceptable_tol = 1e-6)`. `max_iter` 100–300 is the working band for spline + problems; tighten `tol` when you need the last digit of fidelity. +- **Linear-drive models** (affine drive coefficients — the standard transmon + bilinear form) are the GN path's home regime: keep the defaults. +- **Nonlinear-drive models** (models with drive coefficients like $|\alpha|^2$ + — dispersive transmon-cavity) converge poorly on the GN spline path. Two + configuration escapes: + - `solve!(qcp; eval_hessian = false)` — switches Ipopt to L-BFGS, which + routinely fixes the stall on these models; + - or solve the same problem on the exponential integrator, whose second-order + path is exact for its piecewise-constant controls. +- **Ket/multiket problems** can pass `exact_hessian = true` to the + `SplineIntegrator` constructor to request the exact second-order sensitivity + path. The unitary path does not offer this flag. + +## Magnus algorithms and substep sizing + +The spline integrator's forward-propagation algorithm is a kwarg — these are the +choices and their accuracy knobs: + +| Algorithm | Constructor | Accuracy knob | +|---|---|---| +| `Tsit5Alg()` (default) | `Tsit5Alg(; adaptive = true, tol = 1e-6, ode_h = 0.1)` | `tol` (adaptive) / `ode_h` (fixed) | +| `MagnusGL4Alg` | `MagnusGL4Alg(; n_steps = 10, tol = 1e-6)` | `n_steps` — steps per knot interval, the **sole** accuracy knob (`tol` is inert) | +| `MagnusAdapt4Alg` | `MagnusAdapt4Alg(; tol = 1e-6)` | `tol` — adaptive on unitary problems; on ket problems it routes to the fixed-step cell and `tol` is inert (a warning fires — control accuracy via `MagnusGL4Alg(n_steps = …)`) | +| `ChebyshevAlg` | `ChebyshevAlg(; n_sub = :auto, bracket = nothing, …)` | `n_sub = :auto` self-sizes each interval at construction and freezes after; ket-only, matrix-free | + +Rule of thumb: keep the default `Tsit5Alg` for ordinary problems; reach for the +Magnus algorithms when stiffness, long gates, or large-$\|H\|$ regimes actually +demand them (e.g. `MagnusGL4Alg(n_steps ≈ 50)` for a deep Rydberg blockade, where +the default under-resolves and optimizer fidelity diverges from a fine re-rollout). +For simple bilinear single-qubit gates the plain path usually converges faster +than the Magnus variants. + +For manual substep sizing of the fixed-step cells, two exported diagnostics do +the arithmetic for you: `suggest_n_sub(H_drift, H_drives, bracket, coeff!, Ψ0, Δt; +phase_budget = 2.0, dyn_tol = 1e-8, grad_tol = 1e-6)` returns a substep count for +one interval, and `expl_discretization_error(...; same kwargs, n_sub)` reports the +estimated discretization error at a given count. Sizing is frozen after +construction — a mid-solve re-size is never silently attempted. + +## Warm-start idioms + +The pulse round-trips through disk, and a solved problem rehydrates directly: + +```julia +# after a solved run — extract and save the optimized pulse +pulse_v2 = CubicSplinePulse(get_trajectory(qcp)) # rehydrate from the solved trajectory +JLD2.jldsave("pulse.jld2"; pulse = pulse_v2, fidelity = 0.9998) + +# next script — reload and warm-start +pulse = load_pulse("pulse.jld2") # returns the pulse (only the "pulse" key) +qtraj = UnitaryTrajectory(sys, pulse, U_goal) +qcp = SplinePulseProblem(qtraj) # native knot times — best for warm-starting +solve!(qcp; max_iter = 60) +``` + +- `SplinePulseProblem(qtraj)` with no knot count uses the pulse's **native knot + times** — the intended warm-start path. +- `CubicSplinePulse(controls, derivatives, times)` builds a spline pulse from raw + knot data when you have it. +- `load_pulse` returns only the pulse object; bundle metadata (fidelity, gate + name) with `JLD2.jldsave(...; pulse = ..., fidelity = ...)` at save time. From 29bd35e77a2888076225bdbfcda9b7efff73a692 Mon Sep 17 00:00:00 2001 From: aaron Date: Thu, 27 Aug 2026 17:23:24 -0400 Subject: [PATCH 2/3] skills(intonatissimo): entitled usage skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Usage-altitude guide for the entitled Intonatissimo tier: measurement-matching objective trio (MeasurementMatchingObjective / MeasurementParam / update_targets! / update_weights!), the subproblem family (build_subproblem, build_calibration_subproblem, update_subproblem!, update_pinned_values!), public-API ILC loop usage over Intonato's PulseTuningProblem chassis (ILCStrategy modes + kwargs, Learn declarations, compute_ilc_targets semantics), and the error-channel surface at call level with the environment branch-pairing caveat. Delivery-mode caveat: issimo entitlement, HP-mode sysimage or private-registry sandbox; not in the default provisioned env. Content policy per spec-20260827-162500 A1.2/A1.5: public-API loop usage only — strategy internals stay in the internal dev skill; every symbol verified against package source. Part of #615 --- .../extension/skills/intonatissimo/SKILL.md | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 packages/extension/skills/intonatissimo/SKILL.md diff --git a/packages/extension/skills/intonatissimo/SKILL.md b/packages/extension/skills/intonatissimo/SKILL.md new file mode 100644 index 00000000..b1080aea --- /dev/null +++ b/packages/extension/skills/intonatissimo/SKILL.md @@ -0,0 +1,174 @@ +--- +name: intonatissimo +description: Intonatissimo.jl usage — the entitled calibration tier. Measurement-matching objectives, NLP subproblem construction/updates, and the public-API ILC loop over Intonato's PulseTuningProblem chassis. Use when authoring a closed-loop pulse-tuning solve.jl in an Intonatissimo-enabled environment. +surface: entitled +entitlement: issimo +--- + +# Intonatissimo — usage guide + +Intonatissimo is the entitled calibration tier: the measurement-matching +objective family, the control/calibration subproblem constructors, and the ILC +tuning strategy, all riding Intonato's public `PulseTuningProblem` loop. One +import reaches everything — `using Intonatissimo` re-exports the Intonato +chassis (which re-exports Piccolo), so `PulseTuningProblem`, `solve!`, +`Measurement`, `Learn` and friends are all in scope. + +> **Delivery mode (read first).** Intonatissimo requires the **issimo +> entitlement**. It is delivered either as the **HP-mode prebuilt sysimage** or +> via a **private-registry sandbox environment** — it is **NOT in the default +> provisioned solve env**. Recipes in this skill run only in those environments; +> smoke-test signatures in the actual environment before launching. + +## Measurement-matching objective + +`MeasurementMatchingObjective(model, y_target, traj; Q = 1.0, weights = nothing)` +builds a `KnotPointObjective` whose loss at each measurement knot is +$\sum_i \big(w_i (g_i(x) - y_i)\big)^2$ — the objective that matches simulated +measurements to a target vector inside an NLP. + +```julia +model = MeasurementModel(state_name(qtraj), [pop(), pop(), pop()], [1, 25, 50]) +kpo, params = MeasurementMatchingObjective(model, y_target, traj; weights = w_vec) +``` + +- Returns **two** values: the objective (compose it like any other + `AbstractObjective`) and a `Vector{MeasurementParam}` — **keep the params**. +- `MeasurementParam` is a mutable shared-reference container for one measurement + point's target and weights. Mutate through the references between outer + iterations; rebuilding the objective allocates and loses warm-start structure. +- `update_targets!(params, y_target)` — retarget in place. `y_target` is a + `Vector{Measurement}` (a `Measurement` wraps one measurement vector plus its + knot index). +- `update_weights!(params, w_flat)` — write the flattened per-element effective + weights in model element order (measurement-major), in place. +- `weights` are per-measurement task-importance vectors. The legacy `Q` / + `Q_meas` kwargs are deprecated aliases that fold into task weights — pass + `weights`. + +## Subproblem family + +The subproblem constructors build the per-iteration control (and calibration) +NLPs that reuse the original problem's dynamics and constraints: + +| Call | Role | +|---|---| +| `build_subproblem(qcp, model, y_target; R_tr, R_θ, Q_meas)` | Control NLP: measurement matching + per-component trust-region weights (`R_tr`, e.g. `(u = 1e-2, du = 1e-1)`) + global-variable regularizers (`R_θ`, e.g. `(ω = 1.0,)`) → `SubproblemHandle` | +| `build_subproblem(qcp, model, y_target; include_original_objective = true)` | Same, with the QCP's original objective (including infidelity) composed in — use when measurement-matching alone doesn't drive fidelity | +| `build_calibration_subproblem(qcp, model, y_exp; R_θ, Q_cal)` | Calibration NLP: matches **raw** experiment data `y_exp` (not ILC-shifted targets), states free, everything else pinned | +| `update_subproblem!(handle, y_target, z_ref[, model])` | In-place iteration step: retarget, recenter trust-region baselines on `z_ref`, warm-start the trajectory; the 4-arg form also refreshes precision weights at the current prediction | +| `update_pinned_values!(handle, z_ref)` | Refresh a calibration subproblem's pin values to the new reference trajectory | +| `make_global_regularizer(name, traj, R)` | Quadratic penalty $(R/2)\|\theta - \text{baseline}\|^2$ on one global variable — compose your own `R_θ` set if the constructors' NamedTuple form doesn't express it | + +The discipline that makes this fast: **build once, update in place**. The +`SubproblemHandle` holds shared references to the mutable internals, so +`update_subproblem!` touches only targets, baselines, and the warm start — +no reconstruction between outer iterations. + +```julia +handle = build_subproblem(qcp, model, y_target; R_tr = (u = 1e-2, du = 1e-1)) +solve!(handle.prob; max_iter = 200) +update_subproblem!(handle, y_target_next, z_ref) # next outer iteration, same problem +``` + +## Driving the public ILC loop + +The loop chassis is Intonato's `PulseTuningProblem`: it wraps a solved +`QuantumControlProblem`, an experiment, and a measurement model, and runs the +outer propose → run experiment → retarget loop. The strategy kwarg selects the +inner step — pass an `ILCStrategy` to get Intonatissimo's ILC. + +```julia +using Intonatissimo + +mm = MeasurementModel(state_name(qtraj), [pop(), pop(), pop()], [1, 25, 50]) +expmt = SimulatedExperiment(qtraj_true, mm; rng = MersenneTwister(0)) # or a HardwareExperiment + +ptp = PulseTuningProblem(qcp, expmt, mm; strategy = ILCStrategy()) +solve!(ptp; max_iter = 5, tol = 1e-3, γ = 0.8, min_nominal_fidelity = 0.8) +# ptp.qcp is updated in place with the tuned result +``` + +- **Plain ILC** is the bare `ILCStrategy()` — no parameter calibration. +- `solve!` kwargs: `max_iter` (outer iterations, default 5), `tol` (measurement + error convergence, default 1e-3), `γ` (trust-region schedule factor, default + 0.8), `line_search` (Armijo backtracking, default `true`), `ipopt_options` + (forwarded to the inner NLP solve), `max_rejections` (early stop after n + consecutive line-search rejections), `polyak_avg` (average the last n + iterates before syncing), `min_nominal_fidelity` (refuses to run on a + poorly-converged pulse; set 0 to skip). +- The goal measurements are resolved **once** at solve start and fixed for the + whole solve — pass `y_goal = [...]` to `PulseTuningProblem` to supply them + explicitly. +- The device model the loop plans against defaults to the nominal model of the + QCP's system; pass `device_model = ...` to plan against something predictive + (e.g. a parametric-Hamiltonian sysid model) instead. + +### Calibration: learnable device parameters + +`ILCStrategy(mode; kwargs...)` adds parameter calibration on top of plain ILC. +`mode` is `Alternating()` (a separate calibration NLP before each control NLP — +the robust default when controls outnumber measurements) or `Joint()` +(parameters and controls co-optimized in one NLP — use under strong measurement +coverage). Learnable parameters are declared once as `Learn` values keyed by +system-global name; each `Learn`'s `weight` becomes that parameter's +regularization strength, and the constructor validates the QCP against the +declarations before any device time is spent. + +```julia +strat = ILCStrategy(Alternating(); + learn = (ω = Learn(ω_nom; bounds = (0.5ω_nom, 2.0ω_nom), weight = 5.0),), + Q_cal = 1.0, # calibration-NLP measurement weight + pin_Δt = true, # pin per-knot Δt across outer iterations + cal_eval_hessian = false, # calibration NLP on Ipopt L-BFGS (the fast default) +) +ptp = PulseTuningProblem(qcp, expmt, mm; strategy = strat) +solve!(ptp; max_iter = 5) +``` + +- `Learn(init; bounds = (lo, hi), weight = 1.0)` — `bounds` is required (an + unbounded learnable global is the failure mode this declaration exists to + prevent); `Learn(; bounds = ...)` starts from the nominal value. +- A mode object with an empty `learn` set errors — bare `ILCStrategy()` is the + plain-ILC form. +- `include_fidelity_in_u_nlp = true` also keeps the QCP's original objective in + the control NLP, driving fidelity directly when measurement-matching has + nontrivial kernel directions. +- The feedforward step itself is one call if you drive targets by hand: + `y_target = compute_ilc_targets(y_goal, y_model, y_exp)` — element-wise + $y_\text{goal} + y_\text{model} - y_\text{exp}$, pre-compensating the + estimated model error $\hat\varepsilon = y_\text{exp} - y_\text{model}$. + +## Error channels (call level) + +For updates confined to a labelled set of dissipative error channels, the +public surface is four calls: + +```julia +chans = [ErrorChannel(:T1_12, L_t1, 1e-3), ErrorChannel(:dephase, L_phi, 2e-4)] +sub = error_channel_subspace(integrator, traj, chans) # labelled gradient directions + orthonormal span +δx_p = project_step(δx, sub) # project a proposed step onto the span +c = capture(sub, direction) # fraction of the direction's energy the span explains +``` + +- `ErrorChannel(label::Symbol, L, γ)` — jump operator `L`, rate `γ > 0`, and a + label (e.g. `:T1_12`, `:leakage_2`, `:dephase`) that rides through for + attribution. +- `error_channel_subspace(integ, traj, channels; P_enc = nothing)` — one + normalized gradient direction per channel plus the orthonormal span. The + result carries an honest rank audit: structurally dependent channel sets + (e.g. two diagonal channels differing by a multiple of identity) are dropped + from the basis with a warning — read `size(sub.basis, 2)` for the working + dimension, never assume your channel count. +- `capture` is the diagnostic for "are these K channels enough to explain this + residual direction?" — a low value says the residual has components your + channel set doesn't span. + +> **Environment pairing requirement.** The encoded-decay kernels this family +> consumes (the conjugation-frames / encoded-decay-rate kernels, imported from +> Piccolissimo as `conjugation_frames` and companions) are present only when +> your Piccolissimo environment is paired with the conjugation-kernels branch +> (`wip/414-conjugation-kernels` at the time of writing) rather than the +> mainline checkout. If the pairing is wrong, `using Intonatissimo` fails at +> load with a missing-name error — re-pair the environment; the calls above do +> not change. From b3b5b5b0a6878ff1ea5e125eface8445aa594218 Mon Sep 17 00:00:00 2001 From: aaron Date: Thu, 27 Aug 2026 17:41:52 -0400 Subject: [PATCH 3/3] skills(intonatissimo): drop internal WIP branch identifier from world-readable pairing caveat; wording (review); skills(piccolissimo): n_sub positional nit --- packages/extension/skills/intonatissimo/SKILL.md | 4 ++-- packages/extension/skills/piccolissimo/SKILL.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/extension/skills/intonatissimo/SKILL.md b/packages/extension/skills/intonatissimo/SKILL.md index b1080aea..a225ec3b 100644 --- a/packages/extension/skills/intonatissimo/SKILL.md +++ b/packages/extension/skills/intonatissimo/SKILL.md @@ -61,7 +61,7 @@ NLPs that reuse the original problem's dynamics and constraints: | `make_global_regularizer(name, traj, R)` | Quadratic penalty $(R/2)\|\theta - \text{baseline}\|^2$ on one global variable — compose your own `R_θ` set if the constructors' NamedTuple form doesn't express it | The discipline that makes this fast: **build once, update in place**. The -`SubproblemHandle` holds shared references to the mutable internals, so +`SubproblemHandle` holds shared references to the objective's mutable parameters, so `update_subproblem!` touches only targets, baselines, and the warm start — no reconstruction between outer iterations. @@ -168,7 +168,7 @@ c = capture(sub, direction) # fraction of the dire > consumes (the conjugation-frames / encoded-decay-rate kernels, imported from > Piccolissimo as `conjugation_frames` and companions) are present only when > your Piccolissimo environment is paired with the conjugation-kernels branch -> (`wip/414-conjugation-kernels` at the time of writing) rather than the +> (pre-paired by your delivery contact) rather than the > mainline checkout. If the pairing is wrong, `using Intonatissimo` fails at > load with a missing-name error — re-pair the environment; the calls above do > not change. diff --git a/packages/extension/skills/piccolissimo/SKILL.md b/packages/extension/skills/piccolissimo/SKILL.md index b1b4201c..8b15d703 100644 --- a/packages/extension/skills/piccolissimo/SKILL.md +++ b/packages/extension/skills/piccolissimo/SKILL.md @@ -135,7 +135,7 @@ than the Magnus variants. For manual substep sizing of the fixed-step cells, two exported diagnostics do the arithmetic for you: `suggest_n_sub(H_drift, H_drives, bracket, coeff!, Ψ0, Δt; phase_budget = 2.0, dyn_tol = 1e-8, grad_tol = 1e-6)` returns a substep count for -one interval, and `expl_discretization_error(...; same kwargs, n_sub)` reports the +one interval, and `expl_discretization_error(...; same kwargs, n_sub)` (positional) reports the estimated discretization error at a given count. Sizing is frozen after construction — a mid-solve re-size is never silently attempted.