From 6b58e39de358a633fe85f6463cb130b79367e690 Mon Sep 17 00:00:00 2001 From: Carlos Castillo Passi Date: Sun, 24 May 2026 15:46:07 -0700 Subject: [PATCH 1/6] docs: support Plotly animation embeds --- docs/EmbeddPlotlyJSSyncPlotLiterate.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/EmbeddPlotlyJSSyncPlotLiterate.jl b/docs/EmbeddPlotlyJSSyncPlotLiterate.jl index 7e22f26a3..950ca0061 100644 --- a/docs/EmbeddPlotlyJSSyncPlotLiterate.jl +++ b/docs/EmbeddPlotlyJSSyncPlotLiterate.jl @@ -21,6 +21,9 @@ function Base.show(io::IO, ::MIME"text/html", fig::PlotlyJS.SyncPlot) # Copy is required because we pop layout width/height for Plotly's inner JSON. # Mutating fig.plot directly would change size(fig) and user-visible behavior. plot = copy(fig.plot) + plot.frames = fig.plot.frames + plot.config = deepcopy(fig.plot.config) + plot.config.displayModeBar = false default_width = layout_to_html_default_size!(plot.layout.fields, :width, get(plot.layout.fields, :width, nothing)) default_height = layout_to_html_default_size!(plot.layout.fields, :height, get(plot.layout.fields, :height, nothing)) @@ -28,6 +31,7 @@ function Base.show(io::IO, ::MIME"text/html", fig::PlotlyJS.SyncPlot) PlotlyJS.PlotlyBase.to_html( html_buffer, plot; + autoplay=false, full_html=true, include_plotlyjs="cdn", default_width=default_width, From 4d981e77cab1bb2b5442b15f0421c040261bdc7c Mon Sep 17 00:00:00 2001 From: Carlos Castillo Passi Date: Sun, 24 May 2026 15:46:07 -0700 Subject: [PATCH 2/6] docs: add adiabatic RF pulse tutorial --- .../3.tutorials/adiabatic_pulses/BIR4.mat | Bin 29822 -> 0 bytes .../adiabatic_pulses/BIR4InversionB0B1.jl | 53 ---- .../adiabatic_pulses/BIR4InversionProfile.jl | 37 --- .../adiabatic_pulses/HSInversionProfile.jl | 30 --- .../3.tutorials/lit-04b-AdiabaticRFPulse.jl | 255 ++++++++++++++++++ 5 files changed, 255 insertions(+), 120 deletions(-) delete mode 100644 examples/3.tutorials/adiabatic_pulses/BIR4.mat delete mode 100644 examples/3.tutorials/adiabatic_pulses/BIR4InversionB0B1.jl delete mode 100644 examples/3.tutorials/adiabatic_pulses/BIR4InversionProfile.jl delete mode 100644 examples/3.tutorials/adiabatic_pulses/HSInversionProfile.jl create mode 100644 examples/3.tutorials/lit-04b-AdiabaticRFPulse.jl diff --git a/examples/3.tutorials/adiabatic_pulses/BIR4.mat b/examples/3.tutorials/adiabatic_pulses/BIR4.mat deleted file mode 100644 index 83ff7fcdd47a9b3e66a96b1bdc119088ad142163..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29822 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

qq2exVDjbH2%P>Va4HdVczHl5W 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 V?M?4XSEpn(ZO2Ob_#cU3Sc_`UdlCQu diff --git a/examples/3.tutorials/adiabatic_pulses/BIR4InversionB0B1.jl b/examples/3.tutorials/adiabatic_pulses/BIR4InversionB0B1.jl deleted file mode 100644 index 4264503f6..000000000 --- a/examples/3.tutorials/adiabatic_pulses/BIR4InversionB0B1.jl +++ /dev/null @@ -1,53 +0,0 @@ -# This document replicates the results of Figure 15g of the paper "The Return of the Frequency Sweep: -# Designing Adiabatic Pulses for Contemporary NMR" by Michael Garwood and Lance DelaBarre. -using KomaMRI, MAT, PlotlyJS, LinearAlgebra - -RF_wf = matread("./examples/4.adiabatic_pulses/BIR4.mat") -for R = [28] #Product duration and bandwidth - B1_nom = 13.5e-6; w1_2pi_Hz = γ*B1_nom - Trfs = [7] * 1e-3 # (4:0.1:7.5) * 1e-3 #ms - f0s = R ./ (2Trfs) # R = (2 fmax) * Trf - ΔB1s = Array(0.3:0.01:1.5) # % - NB0s = 200 - B0_max = 100 #+-100Hz - Gz = B0_max / γ - z = range(-1, 1, NB0s) - B0s = Array(γ*Gz*z) - - #Init - score = zeros(length(f0s), length(Trfs), length(ΔB1s)) - MagXY = zeros(ComplexF64, NB0s, length(ΔB1s)) - MagZ = zeros(ComplexF64, NB0s, length(ΔB1s)) - N = prod(size(score)) - counter = 1 - for (i, f0) = enumerate(f0s), (j, Trf) = enumerate(Trfs), (k, ΔB1) = enumerate(ΔB1s) - b1 = B1_nom * ΔB1 - B1 = b1 * RF_wf["b1"][:] - Δf = RF_wf["df"][:] * f0 - dt = Trf / length(B1) - T90 = .5e-3 - B190 = 90 / (360 * γ * T90) - rf90 = Sequence() - @addblock rf90 += (RF(ΔB1 * B190, T90, 0, 0), z=Grad(Gz, T90, 0)) - bir4 = Sequence() - @addblock bir4 += (RF(B1, Trf, Δf, 0), z=Grad(Gz, Trf, 0)) - seq = Sequence() - # @addblock seq += rf90 - @addblock seq += bir4 - # @addblock seq += rf90 - sim_params = Dict{String,Any}("Δt_rf"=>dt) - - M = simulate_slice_profile(seq; sim_params, z) - # display(plot_seq(seq)) - - MagXY[:, counter] = M.xy - MagZ[:, counter] = M.z - println("################ $counter / $N = $(round(counter/N*100; digits=3)) % ################") - counter = counter + 1 - end - # Heatmap - fmax = round(f0s[1] * 1e-3; digits=2) - p1 = plot(heatmap(y=B0s, x=ΔB1s, z=abs.(MagXY), colorscale="Jet"), Layout(title="MXY BIR-4 Trf=7 ms R=$R fmax=$fmax kHz")) - p2 = plot(heatmap(y=B0s, x=ΔB1s, z=real.(MagZ), colorscale="Jet"), Layout(title="MZ BIR-4 Trf=7 ms R=$R fmax=$fmax kHz")) - display([p1; p2]) -end diff --git a/examples/3.tutorials/adiabatic_pulses/BIR4InversionProfile.jl b/examples/3.tutorials/adiabatic_pulses/BIR4InversionProfile.jl deleted file mode 100644 index 5d55f41f7..000000000 --- a/examples/3.tutorials/adiabatic_pulses/BIR4InversionProfile.jl +++ /dev/null @@ -1,37 +0,0 @@ -# This document replicates the results of Figure 15g of the paper "The Return of the Frequency Sweep: -# Designing Adiabatic Pulses for Contemporary NMR" by Michael Garwood and Lance DelaBarre. -using KomaMRI, MAT, PlotlyJS, LinearAlgebra, ProgressMeter - -RF_wf = matread("./examples/4.adiabatic_pulses/BIR4.mat") -R = 200 #Product duration and bandwidth -B1 = 120e-6; w1_2pi_Hz = γ*B1 -Trfs = [2] * 1e-3 #(4:0.1:7.5) * 1e-3 #ms -f0s = R ./ (2Trfs) # R = 2 fmax * Trf -ΔB1s = 0.8:0.2:1.2 # % -#Init -score = zeros(length(f0s), length(Trfs), length(ΔB1s)) -p = [scatter() for i=1:length(f0s), j=1:length(Trfs), k=1:length(ΔB1s), l=1:2] -N = prod(size(score)) -count = 1 -for (i, f0) = enumerate(f0s), (j, Trf) = enumerate(Trfs), (k, ΔB1) = enumerate(ΔB1s) - b1 = 120e-6 * ΔB1 - B1 = b1 * RF_wf["b1"][:] - Δf = RF_wf["df"][:] * f0 - dt = Trf / length(B1) - fmax_sim = 10e3 - Gz = fmax_sim / γ - seq = Sequence() - @addblock seq += (RF(B1, Trf, Δf, 0), z=Grad(Gz, Trf, 0)) - sim_params = Dict{String,Any}("Δt_rf"=>dt) - - z = range(-1, 1, 200) - M = simulate_slice_profile(seq; sim_params, z) - - f = γ*Gz*z - p[i,j,k,1] = scatter(x=f,y=(1 .- M.z)/2,name="B1/B1_nom=$ΔB1") - # p[i,j,k,2] = scatter(x=f,y=abs.(M.xy),name="Mxy, B1/B1_nom=$ΔB1") - println("################ $count / $N = $(round(count/N*100; digits=3)) % ################") - global count = count + 1 -end -# Heatmap -plot(p[:], Layout(title="Fraction Refocused (BIR-4, Trf=$(Trfs[1]*1e3)ms, TBP=Trf*BW=$(R))")) diff --git a/examples/3.tutorials/adiabatic_pulses/HSInversionProfile.jl b/examples/3.tutorials/adiabatic_pulses/HSInversionProfile.jl deleted file mode 100644 index 6e0c6d262..000000000 --- a/examples/3.tutorials/adiabatic_pulses/HSInversionProfile.jl +++ /dev/null @@ -1,30 +0,0 @@ -using KomaMRI, PlotlyJS -# RF Pulse Paramters, https://onlinelibrary.wiley.com/doi/epdf/10.1002/jmri.26021?saml_referrer -b1max = 13e-6 #Peak amplitude (uT) -Trf = 18.3e-3 #Pulse duration (ms) -β = 4e2 #frequency modulation param (rad/s) -μ = 6 #phase modulation parameter (dimensionless) -fmax = μ * β / (2π) # 2fmax = BW -# Adiabatic condition b1max >> β*√μ/γ: -b1max > β*sqrt(μ)/(2π*γ) -# Pulse Shape -t = range(-Trf/2, Trf/2, 201) -B1 = b1max .* sech.(β.*t) -Δf = -fmax .* tanh.(β.*t) -# Sequence generation -fmax_sim = 2e3 -Gz = fmax_sim / γ -seq = Sequence() -@addblock seq += (RF(B1, Trf, Δf, 0), z=Grad(Gz, Trf, 0)) -p1 = plot_seq(seq; max_rf_samples=Inf, slider=false) -KomaMRI.get_flip_angles(seq)[1] -# Simulation -sim_params = Dict{String,Any}("Δt_rf"=>t[2]-t[1]) -z = range(-1, 1, 400) -M = simulate_slice_profile(seq; sim_params, z) -# Plot -f = γ*Gz*z -s1 = scatter(x=f,y=abs.(M.xy),name="|Mxy|") -s2 = scatter(x=f,y=M.z,name="Mz") -p2 = plot([s1,s2], Layout(title="Hyperbolic-Secant (HS) Adiabatic Inversion Pulse (μ=$μ, β=$β rad/s)", xaxis_title="Frequency [Hz]")) -[p1; p2] diff --git a/examples/3.tutorials/lit-04b-AdiabaticRFPulse.jl b/examples/3.tutorials/lit-04b-AdiabaticRFPulse.jl new file mode 100644 index 000000000..8626c2215 --- /dev/null +++ b/examples/3.tutorials/lit-04b-AdiabaticRFPulse.jl @@ -0,0 +1,255 @@ +# # Adiabatic RF Pulse + +using KomaMRI #hide +sys = Scanner(); #hide + +# In this tutorial, we will build a hyperbolic-secant (HS) adiabatic inversion +# pulse. The key point is that KomaMRI can keep the RF frequency modulation +# explicit, then simulate the RF-frame dynamics and the resulting +# ``B_0``/``B_1`` robustness. + +# ## Defining a frequency-modulated pulse +# +# > Parameters follow the default ``\mathrm{HS}_{4,6}`` pulse in Figure 1(c) of this +# > [JMRI adiabatic inversion example](https://doi.org/10.1002/jmri.26021). + +b1max = 13.5e-6 +Trf = 18.3e-3 +β̂ = 4 +μ = 6 +β = 2 * β̂ / Trf; + +# For an HS pulse, the hardest point for adiabatic following is near the +# center of the sweep, where ``B_1`` is maximal and +# ``|\mathrm{d}\Delta\omega/\mathrm{d}t|=\mu\beta^2``. Requiring the RF precession rate to +# dominate that sweep rate gives the threshold used in the paper: +# +# ```math +# \frac{(\gamma_\mathrm{rad} B_1)^2}{\mu\beta^2} \ge 1 +# \quad\Rightarrow\quad +# B_1 \geq \frac{\sqrt{\mu}\beta}{\gamma_\mathrm{rad}}. +# ``` + +b1_threshold = β * sqrt(μ) / (2π * γ) +b1max > b1_threshold + +# First, we define the RF amplitude as a hyperbolic secant and the frequency +# sweep as a hyperbolic tangent. + +t = range(-Trf / 2, Trf / 2, 201) +B1 = b1max .* sech.(β .* t) +Δf = -μ * β .* tanh.(β .* t) ./ (2π); + +# The ``\Delta f`` argument of `RF` can be a scalar, for a constant RF offset +# such as a slice offset, or a waveform, as in this frequency-modulated pulse. + +f = range(-2e3, 2e3, 161) |> collect +seq = Sequence() +@addblock seq += RF(B1, Trf, Δf, 0); + +# This representation is native to KomaMRI. Pulseq RF events do not store +# frequency-modulation waveforms, so exporting this sequence with `write_seq` +# requires first converting the sweep into RF phase samples. KomaMRI may do this +# conversion automatically in the future. + +# ## Plotting an adiabatic pulse +# +# `plot_seq` can show both views. The default plot keeps ``\Delta f(t)`` +# explicit; the `freq_in_phase` keyword shows the same pulse after moving the +# frequency sweep into the RF phase. + +using PlotlyJS #hide +function show_only_traces!(p, names) #hide + for trace in p.plot.data #hide + name = get(trace.fields, :name, "") #hide + trace.fields[:showlegend] = false #hide + trace.fields[:visible] = name in names #hide + end #hide + return p #hide +end #hide +function scale_trace_y!(p, name, scale) #hide + for trace in p.plot.data #hide + get(trace.fields, :name, "") == name || continue #hide + y = trace.fields[:y] #hide + trace.fields[:customdata] = y #hide + trace.fields[:y] = scale .* y #hide + trace.fields[:hovertemplate] = "(%{x:.4f} ms, Δf_FM: %{customdata:.4f} kHz)" #hide + end #hide + return p #hide +end #hide +p_freq = plot_seq(seq; max_rf_samples=Inf, slider=false, height=360, title="Frequency-modulated RF", showlegend=false) +p_phase = plot_seq(seq; freq_in_phase=true, max_rf_samples=Inf, slider=false, height=360, title="Phase-modulated RF", showlegend=false) +show_only_traces!(p_freq, ("|B1|_AM", "Δf_FM")) #hide +show_only_traces!(p_phase, ("|B1|_AM", "∠B1_AM")) #hide +scale_trace_y!(p_freq, "Δf_FM", 9) #hide +p_rf = [p_freq p_phase] #hide +relayout!(p_rf, height=380, margin=attr(t=56)) #hide +p_rf #hide +#jl display(p_rf) + +# Keeping the frequency sweep explicit is useful because KomaMRI simulates a +# frequency-modulated RF pulse in the RF rotating frame. If the same pulse is +# first converted to sampled phase modulation, the simulation is more sensitive +# to the RF time sampling. + +# ## Comparing RF and rotating frames +# +# Using a callback, we can record the magnetization during the RF pulse. + +trajectory = NamedTuple[] +call_every_N_blocks = 1 + +record_traj = Callback( + call_every_N_blocks, + (progress_info, sim_blocks_info, device_data, sim_params) -> begin + j = last(sim_blocks_info.parts[progress_info.block]) + push!(trajectory, (; + Mxy=device_data.Xt.xy[1], Mz=device_data.Xt.z[1], + ψ=device_data.seqd.ψ[j], B1=device_data.seqd.B1[j], Δf=device_data.seqd.Δf[j], + )) + end, +) + +sim_params = KomaMRICore.default_sim_params() +sim_params["return_type"] = "state" +sim_params["max_rf_block_length"] = 1; # very inefficient; just for plots +obj0 = Phantom(; x=[0.0], Δw=[0.0]); +simulate(obj0, seq, sys; sim_params, callbacks=(record_traj,), verbose=false); + +function adiabatic_frame(p) #hide + ωeff = (-real(p.B1), -imag(p.B1), p.Δf / γ) #hide + ω̂rf = ωeff ./ sqrt(sum(abs2, ωeff)) #hide + Mxy_rf = p.Mxy * cis(-p.ψ) #hide + ω̂xy_rot = complex(ω̂rf[1], ω̂rf[2]) * cis(p.ψ) #hide + return (; #hide + Mrf=(real(Mxy_rf), imag(Mxy_rf), p.Mz), #hide + Mrot=(real(p.Mxy), imag(p.Mxy), p.Mz), #hide + ω̂rf, #hide + ω̂rot=(real(ω̂xy_rot), imag(ω̂xy_rot), ω̂rf[3]), #hide + ) #hide +end #hide +trajectory_frames = adiabatic_frame.(trajectory) #hide +xyz(points) = ntuple(i -> getindex.(points, i), 3) #hide +Mrf_xyz = xyz(getproperty.(trajectory_frames, :Mrf)) #hide +Mrot_xyz = xyz(getproperty.(trajectory_frames, :Mrot)) #hide +ω̂rf_xyz = xyz(getproperty.(trajectory_frames, :ω̂rf)) #hide +ω̂rot_xyz = xyz(getproperty.(trajectory_frames, :ω̂rot)) #hide +anim_idx = unique(round.(Int, range(1, length(trajectory_frames), 36))) #hide +function sphere_mesh(; nθ=36, nφ=18) #hide + θ = range(0, 2π, nθ) #hide + φ = range(0, π, nφ) #hide + return ( #hide + x=[sin(p) * cos(t) for p in φ, t in θ], #hide + y=[sin(p) * sin(t) for p in φ, t in θ], #hide + z=[cos(p) for p in φ, t in θ], #hide + ) #hide +end #hide +sphere = sphere_mesh() #hide +function bloch_traces(i, M, ω̂; scene="scene", showlegend=true) #hide + path = scatter3d(; #hide + x=M[1][1:i], y=M[2][1:i], z=M[3][1:i], #hide + mode="lines", name="M path", scene=scene, showlegend=showlegend, #hide + line=attr(color="#111827", width=6), #hide + ) #hide + magnetization = scatter3d(; #hide + x=[0, M[1][i]], y=[0, M[2][i]], z=[0, M[3][i]], #hide + mode="lines+markers", name="M", scene=scene, showlegend=showlegend, #hide + line=attr(color="#dc2626", width=7), #hide + marker=attr(color="#dc2626", size=4), #hide + ) #hide + effective_field = scatter3d(; #hide + x=[0, ω̂[1][i]], y=[0, ω̂[2][i]], z=[0, ω̂[3][i]], #hide + mode="lines+markers", name="ωeff", scene=scene, showlegend=showlegend, #hide + line=attr(color="#2563eb", width=7), #hide + marker=attr(color="#2563eb", size=4), #hide + ) #hide + return [ #hide + path, #hide + magnetization, #hide + effective_field, #hide + ] #hide +end #hide +bloch_sphere(scene) = [surface(; x=sphere.x, y=sphere.y, z=sphere.z, opacity=0.14, showscale=false, name="Bloch sphere", scene=scene, colorscale=[[0, "#dbe4ef"], [1, "#dbe4ef"]])] #hide +function animation_frame(i) #hide + traces = [ #hide + bloch_traces(i, Mrf_xyz, ω̂rf_xyz)..., #hide + bloch_traces(i, Mrot_xyz, ω̂rot_xyz; scene="scene2", showlegend=false)..., #hide + ] #hide + return frame(; name=string(i), data=traces, traces=[1, 2, 3, 5, 6, 7]) #hide +end #hide +function bloch_scene(domain) #hide + axis(range) = attr(; #hide + range=range, title=attr(text=""), ticks="", #hide + showgrid=false, showbackground=false, showticklabels=false, zeroline=false, #hide + ) #hide + return attr(; #hide + domain=domain, aspectmode="cube", #hide + xaxis=axis([-1, 1]), yaxis=axis([-1, 1]), zaxis=axis([-1, 1]), #hide + camera=attr(eye=attr(x=1.31, y=1.31, z=0.75), up=attr(x=0, y=0, z=1)), #hide + ) #hide +end #hide +base_traces = [ #hide + bloch_sphere("scene")..., #hide + bloch_traces(first(anim_idx), Mrf_xyz, ω̂rf_xyz)..., #hide + bloch_sphere("scene2")..., #hide + bloch_traces(first(anim_idx), Mrot_xyz, ω̂rot_xyz; scene="scene2", showlegend=false)..., #hide +] #hide +frames = animation_frame.(anim_idx) #hide +p_bloch = plot( #hide + base_traces, #hide + Layout(; #hide + height=340, margin=attr(l=0, r=0, t=32, b=0), #hide + scene=bloch_scene(attr(x=[0.0, 0.48], y=[0.0, 0.93])), #hide + scene2=bloch_scene(attr(x=[0.52, 1.0], y=[0.0, 0.93])), #hide + annotations=[ #hide + attr(text="RF frame", x=0.24, y=0.98, xref="paper", yref="paper", xanchor="center", showarrow=false, font=attr(size=18)), #hide + attr(text="Rotating frame", x=0.76, y=0.98, xref="paper", yref="paper", xanchor="center", showarrow=false, font=attr(size=18)), #hide + ], #hide + updatemenus=[ #hide + attr(; #hide + type="buttons", direction="right", x=0, y=1.0, #hide + xanchor="left", yanchor="top", #hide + buttons=[attr(label="Play", method="animate", args=[nothing, attr(frame=attr(duration=80, redraw=true), fromcurrent=true)])], #hide + ), #hide + ], #hide + ), #hide + frames, #hide +) #hide +#jl display(p_bloch) + +# The blue vector is the normalized rotation axis +# ``\hat{\boldsymbol{\omega}}_\mathrm{eff}``, where +# ``\boldsymbol{\omega}_\mathrm{eff} = -\gamma \mathbf{B}_\mathrm{eff}``. With this sign convention, +# the magnetization precesses right-handed around the plotted axis. + +# ## HS adiabatic pulse B0 and B1 robustness +# +# To showcase the off-resonance and ``B_1`` robustness of this type of pulse, we +# can show its effect in a heatmap with ``B_0 \in [-2, 2]\,\mathrm{kHz}`` and +# ``B_1 \in [0, 16]\,\mu\mathrm{T}``: + +obj = Phantom(; x=zeros(length(f)), Δw=2π .* f); +b1_scales = range(0.05, 1.2, 47) |> collect; +sim_params = Dict{String, Any}("return_type" => "state"); + +Mz = map(b1_scales) do scale + seq_scale = Sequence() + @addblock seq_scale += RF(scale .* B1, Trf, Δf, 0) + simulate(obj, seq_scale, sys; sim_params, verbose=false).z +end; + +b1_axis = 1e6 .* b1max .* b1_scales #hide +threshold = 1e6 * b1_threshold #hide +Mz_map = round.(reduce(hcat, Mz); digits=3) #hide +p_hs = plot( #hide + [ #hide + heatmap(; x=f, y=b1_axis, z=permutedims(Mz_map), zmin=-1, zmax=1, colorscale="RdBu", colorbar=attr(title="Mz")), #hide + scatter(; x=f, y=fill(threshold, length(f)), name="threshold", mode="lines", hoverinfo="skip", line=attr(color="#a62023", dash="dash", width=3)), #hide + ], #hide + Layout(; title="HS4,6 inversion profile", xaxis_title="Off-resonance [Hz]", yaxis_title="B1,max [μT]", height=420, margin=attr(t=64)), #hide +) #hide +p_hs #hide +#jl display(p_hs) + +# The dashed line is the analytic adiabatic threshold for this HS pulse, where we +# can see that the inversion is achieved after the threshold is surpassed. From e94dea75aeab15d156534ef272f95e5554054c8c Mon Sep 17 00:00:00 2001 From: Carlos Castillo Passi Date: Sun, 24 May 2026 17:14:43 -0700 Subject: [PATCH 3/6] docs: fix chemical shift phantom plot layout --- examples/3.tutorials/lit-03-ChemicalShiftEPI.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/3.tutorials/lit-03-ChemicalShiftEPI.jl b/examples/3.tutorials/lit-03-ChemicalShiftEPI.jl index 8fdeeac84..bab0ef4be 100644 --- a/examples/3.tutorials/lit-03-ChemicalShiftEPI.jl +++ b/examples/3.tutorials/lit-03-ChemicalShiftEPI.jl @@ -8,8 +8,14 @@ sys = Scanner(); #hide obj = brain_phantom2D() # a slice of a brain p1 = plot_phantom_map(obj, :T2 ; height=400, width=400, view_2d=true) p2 = plot_phantom_map(obj, :Δw ; height=400, width=400, view_2d=true) -#md [p1 p2] #hide -#jl display([p1 p2]) +p = [p1 p2] #hide +p.plot.layout.fields[:xaxis2][:scaleanchor] = "y2" #hide +p.plot.layout.fields[:xaxis1][:domain] = [0.0, 0.40] #hide +p.plot.layout.fields[:xaxis2][:domain] = [0.58, 0.98] #hide +p.plot.data[1].fields[:marker][:colorbar][:x] = 0.46 #hide +p.plot.data[2].fields[:marker][:colorbar][:x] = 1.03 #hide +#md p #hide +#jl display(p) # At the left, you can see the ``T_2`` map of the phantom, # and at the right, the off-resonance ``\Delta\omega``. From ce19373f6a6965a1034738986eaff6a1d3abd638 Mon Sep 17 00:00:00 2001 From: Carlos Castillo Passi Date: Sun, 24 May 2026 17:22:56 -0700 Subject: [PATCH 4/6] docs: clean up slice-selective recon plot --- examples/3.tutorials/lit-04-3DSliceSelective.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/3.tutorials/lit-04-3DSliceSelective.jl b/examples/3.tutorials/lit-04-3DSliceSelective.jl index 14ad80602..4f19e6908 100644 --- a/examples/3.tutorials/lit-04-3DSliceSelective.jl +++ b/examples/3.tutorials/lit-04-3DSliceSelective.jl @@ -58,5 +58,9 @@ image = reconstruction(acq, reconParams) p4 = plot_image(abs.(image[:, :, 1]); height=360, title="Slice 1") p5 = plot_image(abs.(image[:, :, 2]); height=360, title="Slice 2") p6 = plot_image(abs.(image[:, :, 3]); height=360, title="Slice 3") -#md [p4 p5 p6] #hide -#jl display([p4 p5 p6]) +p = [p4 p5 p6] #hide +p.plot.layout.fields[:yaxis2][:scaleanchor] = "x2" #hide +p.plot.layout.fields[:yaxis3][:scaleanchor] = "x3" #hide +foreach(t -> t.fields[:showscale] = false, p.plot.data) #hide +#md p #hide +#jl display(p) From 9c6d1715e8c45960bd6a940f4f9c35051c9301c1 Mon Sep 17 00:00:00 2001 From: Carlos Castillo Passi Date: Sun, 24 May 2026 17:31:24 -0700 Subject: [PATCH 5/6] docs: fix tutorial plot layout --- examples/3.tutorials/lit-03-ChemicalShiftEPI.jl | 8 ++++++-- examples/3.tutorials/lit-04-3DSliceSelective.jl | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/examples/3.tutorials/lit-03-ChemicalShiftEPI.jl b/examples/3.tutorials/lit-03-ChemicalShiftEPI.jl index bab0ef4be..c445dcf3e 100644 --- a/examples/3.tutorials/lit-03-ChemicalShiftEPI.jl +++ b/examples/3.tutorials/lit-03-ChemicalShiftEPI.jl @@ -10,10 +10,14 @@ p1 = plot_phantom_map(obj, :T2 ; height=400, width=400, view_2d=true) p2 = plot_phantom_map(obj, :Δw ; height=400, width=400, view_2d=true) p = [p1 p2] #hide p.plot.layout.fields[:xaxis2][:scaleanchor] = "y2" #hide +p.plot.layout.fields[:yaxis2][:title] = "" #hide p.plot.layout.fields[:xaxis1][:domain] = [0.0, 0.40] #hide p.plot.layout.fields[:xaxis2][:domain] = [0.58, 0.98] #hide -p.plot.data[1].fields[:marker][:colorbar][:x] = 0.46 #hide -p.plot.data[2].fields[:marker][:colorbar][:x] = 1.03 #hide +for (trace, x) in zip(p.plot.data, (0.43, 1.02)) #hide + trace.fields[:marker][:colorbar][:x] = x #hide + trace.fields[:marker][:colorbar][:len] = 0.55 #hide + trace.fields[:marker][:colorbar][:thickness] = 14 #hide +end #hide #md p #hide #jl display(p) diff --git a/examples/3.tutorials/lit-04-3DSliceSelective.jl b/examples/3.tutorials/lit-04-3DSliceSelective.jl index 4f19e6908..7031acb68 100644 --- a/examples/3.tutorials/lit-04-3DSliceSelective.jl +++ b/examples/3.tutorials/lit-04-3DSliceSelective.jl @@ -59,8 +59,14 @@ p4 = plot_image(abs.(image[:, :, 1]); height=360, title="Slice 1") p5 = plot_image(abs.(image[:, :, 2]); height=360, title="Slice 2") p6 = plot_image(abs.(image[:, :, 3]); height=360, title="Slice 3") p = [p4 p5 p6] #hide -p.plot.layout.fields[:yaxis2][:scaleanchor] = "x2" #hide -p.plot.layout.fields[:yaxis3][:scaleanchor] = "x3" #hide foreach(t -> t.fields[:showscale] = false, p.plot.data) #hide +for (i, xref) in enumerate(("x", "x2", "x3")) #hide + xaxis = Symbol("xaxis", i) #hide + yaxis = Symbol("yaxis", i) #hide + p.plot.layout.fields[yaxis][:scaleanchor] = xref #hide + p.plot.layout.fields[yaxis][:constrain] = "domain" #hide + p.plot.layout.fields[xaxis][:range] = [-0.5, Nx - 0.5] #hide + p.plot.layout.fields[yaxis][:range] = [-0.5, Ny - 0.5] #hide +end #hide #md p #hide #jl display(p) From e3c4932f665b2a67d680afd329e0afec687f0838 Mon Sep 17 00:00:00 2001 From: Carlos Castillo Passi Date: Sun, 24 May 2026 20:37:41 -0700 Subject: [PATCH 6/6] docs: resolve tutorial preview issues --- examples/3.tutorials/lit-03-ChemicalShiftEPI.jl | 6 +++++- examples/3.tutorials/lit-04-3DSliceSelective.jl | 1 + examples/3.tutorials/lit-04b-AdiabaticRFPulse.jl | 4 ++-- examples/3.tutorials/lit-06-DiffusionMotion.jl | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/3.tutorials/lit-03-ChemicalShiftEPI.jl b/examples/3.tutorials/lit-03-ChemicalShiftEPI.jl index c445dcf3e..2faad835d 100644 --- a/examples/3.tutorials/lit-03-ChemicalShiftEPI.jl +++ b/examples/3.tutorials/lit-03-ChemicalShiftEPI.jl @@ -10,9 +10,13 @@ p1 = plot_phantom_map(obj, :T2 ; height=400, width=400, view_2d=true) p2 = plot_phantom_map(obj, :Δw ; height=400, width=400, view_2d=true) p = [p1 p2] #hide p.plot.layout.fields[:xaxis2][:scaleanchor] = "y2" #hide -p.plot.layout.fields[:yaxis2][:title] = "" #hide p.plot.layout.fields[:xaxis1][:domain] = [0.0, 0.40] #hide p.plot.layout.fields[:xaxis2][:domain] = [0.58, 0.98] #hide +for axis in (:xaxis1, :yaxis1, :xaxis2, :yaxis2) #hide + p.plot.layout.fields[axis][:title] = "" #hide + p.plot.layout.fields[axis][:ticks] = "" #hide + p.plot.layout.fields[axis][:showticklabels] = false #hide +end #hide for (trace, x) in zip(p.plot.data, (0.43, 1.02)) #hide trace.fields[:marker][:colorbar][:x] = x #hide trace.fields[:marker][:colorbar][:len] = 0.55 #hide diff --git a/examples/3.tutorials/lit-04-3DSliceSelective.jl b/examples/3.tutorials/lit-04-3DSliceSelective.jl index 7031acb68..4968f7330 100644 --- a/examples/3.tutorials/lit-04-3DSliceSelective.jl +++ b/examples/3.tutorials/lit-04-3DSliceSelective.jl @@ -47,6 +47,7 @@ p3 = plot_signal(raw; slider=false, height=300) # Finally, we reconstruct the acquiered images. ## Get the acquisition data +raw.params["trajectory"] = "other" acq = AcquisitionData(raw) ## Setting up the reconstruction parameters and perform reconstruction diff --git a/examples/3.tutorials/lit-04b-AdiabaticRFPulse.jl b/examples/3.tutorials/lit-04b-AdiabaticRFPulse.jl index 8626c2215..5c5e88cef 100644 --- a/examples/3.tutorials/lit-04b-AdiabaticRFPulse.jl +++ b/examples/3.tutorials/lit-04b-AdiabaticRFPulse.jl @@ -146,7 +146,7 @@ function sphere_mesh(; nθ=36, nφ=18) #hide end #hide sphere = sphere_mesh() #hide function bloch_traces(i, M, ω̂; scene="scene", showlegend=true) #hide - path = scatter3d(; #hide + path_trace = scatter3d(; #hide x=M[1][1:i], y=M[2][1:i], z=M[3][1:i], #hide mode="lines", name="M path", scene=scene, showlegend=showlegend, #hide line=attr(color="#111827", width=6), #hide @@ -164,7 +164,7 @@ function bloch_traces(i, M, ω̂; scene="scene", showlegend=true) #hide marker=attr(color="#2563eb", size=4), #hide ) #hide return [ #hide - path, #hide + path_trace, #hide magnetization, #hide effective_field, #hide ] #hide diff --git a/examples/3.tutorials/lit-06-DiffusionMotion.jl b/examples/3.tutorials/lit-06-DiffusionMotion.jl index 0021e848b..0fa4c1f62 100644 --- a/examples/3.tutorials/lit-06-DiffusionMotion.jl +++ b/examples/3.tutorials/lit-06-DiffusionMotion.jl @@ -5,7 +5,7 @@ using PlotlyJS #hide using Random, Suppressor #hide # The purpose of this tutorial is to showcase the simulation of diffusion-related effects. -# For this, we are going to define a [`path`](@ref) motion to simulate the Brownian motion of spins. +# For this, we are going to define a [`KomaMRI.path`](@ref) motion to simulate the Brownian motion of spins. # This is not the most efficient way of simulating diffusion, but it is a good way to understand the phenomenon. # In particular, we will going to simulate isotropic diffusion, characterized by the Apparent Diffusion Coefficient (ADC). @@ -21,7 +21,7 @@ obj = Phantom(; T2 = ones(Nspins) * 100e-3, ); -# Now we will define the Brownian motion of the spins using the [`path`](@ref) motion definition. +# Now we will define the Brownian motion of the spins using the [`KomaMRI.path`](@ref) motion definition. # The motion will be defined by the displacements in the x, y, and z directions (`dx`, `dy`, and `dz`) # of the spins. The displacements will be generated by a random walk with mean square displacement #