Skip to content

Initialization of model variables appears to not work in this nonlinear example #4202

Description

@tomdstone

For context, I'm modeling a specific MRI sequence and trying to find optimal parameters. Basically, I can set one of the variables (called TR) fixed and then outside of JuMP do a grid search to optimize it, but when I insert it into my optimization I am unable to optimize as even though I provide inbounds parameters for initialization, the model appears to not be using some of them. I have gotten this exact same issue with both MadNLP.jl and Ipopt.jl

First, some auxiliary functions and structs (this is the relevant stuff copied over from a larger project so its a little gross, sorry):

using Pkg
Pkg.activate(temp=true)
Pkg.add(["JuMP", "MadNLP"])
# Pkg.add(["JuMP", "Ipopt"])

using JuMP
using MadNLP
# using Ipopt

const γ = 2.675e8 # rad / s / T

## Structs used

Base.@kwdef struct Scanner
    B0=3.
    B1=1e-5
    Gmax=300e-3
    Smax=730
    ADC_Δt=1e-6
end

Base.@kwdef struct ScalingParams
    propG=1.0
    propSlew=1.0
end

Base.@kwdef struct TissueParams
    T₁=0.8
    T₂=50e-3
    D=1e-10
end

Base.@kwdef struct dwSSFPParams
    α
    G
    S
    τ
    TR
end

## Functions used

b_buxton(dwp::dwSSFPParams) =* dwp.G * dwp.τ)^2 * dwp.TR

β_buxton(dwp::dwSSFPParams) =* dwp.G * dwp.τ)^2 * dwp.τ

function M⁻(;
    tp::TissueParams,
    dwp::dwSSFPParams,
    b = b_buxton(dwp),
    β = β_buxton(dwp),
    bD = b * tp.D,
    βD = β * tp.D,
    M₀ = 3.,
)
    A₁ = exp(-bD)
    A₂ = exp(-βD)
    A₂3rd = exp(-βD / 3) # A₂ ^ (1/3)= cosd(dwp.α)
    sα = sind(dwp.α)

    E₁ = exp(-dwp.TR / tp.T₁)
    E₂ = exp(-dwp.TR / tp.T₂)

    s = E₂ * A₁ * A₂3rd^(-4) * (1 - E₁ * cα) + E₂ * A₂3rd^(-1) * (cα - 1)
    r = 1 - E₁ *+ E₂^2 * A₁ * A₂3rd * (cα - E₁)
    K = (1 - E₁ * A₁ *- E₂^2 * A₁^2 * A₂3rd^(-2) * (E₁ * A₁ - cα)) / (E₂ * A₁ * A₂3rd^(-4) * (1 + cα) * (1 - E₁ * A₁))
    F₁ = K - sqrt(K^2 - A₂^2)

    return - M₀ * (1 - E₁) * E₂ * A₂3rd^(-2) * (F₁ - E₂ * A₁ * A₂3rd^2) */ (r - F₁ * s)
end

function b_eff(dwp_diff::dwSSFPParams; tp::TissueParams, M₀::Real = 3.)
    dwp_non  = dwSSFPParams= dwp_diff.α, G = 0., S = 0., τ = 0., TR = dwp_diff.TR)

    return -1 / tp.D * (log(M⁻(; tp, dwp = dwp_diff, M₀)) - log(M⁻(; tp, dwp = dwp_non, M₀)))
end

function η(S, dwp::dwSSFPParams, readout_time)
    return S * sqrt(readout_time / dwp.TR)
end

Now, I set up the model

b = 1e9
readout_time = 40e-3
sp = ScalingParams()
scanner = Scanner()
tp = TissueParams()
rf_time = 6e-3
init_ == 10., τ = 2e-3, S = 0.95 * (scanner.Smax * sp.propSlew), ξ = 1e-4, TR = 60e-3)

dead_time = 2e-3

model = Model(MadNLP.Optimizer)

@variable(model, 1. <= α <= 179.)
@variable(model, 0. <= τ)
@variable(model, 0. <= S <= scanner.Smax * sp.propSlew)
@variable(model, 0. <= ξ)
@variable(model, 0. <= TR)

@constraint(model, model[] <= model[])
@constraint(model, model[] * model[:S] <= scanner.Gmax * sp.propG)
@constraint(model, model[] + model[] + rf_time + readout_time + dead_time <= model[:TR])
@constraint(model, b == b_eff(
    dwSSFPParams(model[], model[] * model[:S], model[:S], model[], model[:TR]);
    tp = tp,
    M₀ = scanner.B0
))

set_start_value(model[], init_.α)
set_start_value(model[], init_.τ)
set_start_value(model[:S], init_.S)
set_start_value(model[], min(0.95 * scanner.Gmax * sp.propG / init_.S, init_.ξ))
set_start_value(model[:TR], init_.TR)

@variable(model, 0 <= signal)
@constraint(model, model[:signal] == M⁻(;
    tp = tp,
    dwp = dwSSFPParams(model[], model[] * model[:S], model[:S], model[], model[:TR]),
    M₀ = scanner.B0
))

@objective(
    model,
    Max,
    η(
        model[:signal],
        dwSSFPParams(model[], model[] * model[:S], model[:S], model[], model[:TR]),
        readout_time
    )
)

# checking that starting values are good

dwp_init = dwSSFPParams(
    model[] |> start_value,
    (model[] |> start_value) * (model[:S] |> start_value),
    model[:S] |> start_value,
    model[] |> start_value,
    model[:TR] |> start_value
)

signal_init = M⁻(;
    tp = tp,
    dwp = dwp_init,
    M₀ = scanner.B0
)

η(
    signal_init,
    dwp_init,
    readout_time
)

b_eff(
    dwp_init;
    tp = tp,
    M₀ = scanner.B0
) # starts near, but not at, correct b value

actually optimizing

optimize!(model)

results = (
    α = value(model[]),
    τ = value(model[]),
    ξ = value(model[]),
    G = value(model[]) * value(model[:S]),
    S = value(model[:S]),
    TR = value(model[:TR]),
    η = objective_value(model),
    signal = value(model[:signal]),
    b = value(b_eff(
        dwSSFPParams(model[], model[] * model[:S], model[:S], model[], model[:TR]);
        tp = tp,
        M₀ = scanner.B0
    )),
    status = termination_status(model)
)

note that S * ξ > scanner.Gmax * sp.propG, is significantly too large, which makes K=Inf and thus F₁ becomes NaN in the M⁻ equation.
Also note that ξ, τ, and S are not the values I initially set them to, while α and TR are.

# the values that it has result in NaNs, thus optimization fails

dwp_res = dwSSFPParams(results.α, results.G, results.S, results.τ, results.TR)

M⁻(;
    tp = tp,
    dwp = dwp_res,
    M₀ = scanner.B0
)

b_eff(
    dwp_res;
    tp = tp,
    M₀ = scanner.B0
)

η(
    M⁻(;
        tp = tp,
        dwp = dwp_res,
        M₀ = scanner.B0
    ),
    dwp_res,
    readout_time
)

But when I fix the TR, we get the following:

TR = 60e-3
b = 1e9
scanner = Scanner()
tp = TissueParams()
rf_time = 6e-3
init_ == 10., τ = 2e-3, S = 100., ξ = 1e-4)

model2 = Model(MadNLP.Optimizer)
# model2 = Model(Ipopt.Optimizer)

@variable(model2, 1. <= α <= 179.)
@variable(model2, 0. <= τ <= TR) # needed the <= TR/2 for convergence stability
@variable(model2, 0. <= S <= scanner.Smax)
@variable(model2, 0. <= ξ <= TR/2) # needed the <= TR/2 for convergence stability

@constraint(model2, model2[] <= model2[])
@constraint(model2, model2[] + model2[] + rf_time <= TR)
@constraint(model2, model2[] * model2[:S] <= scanner.Gmax)

set_start_value(model2[:S], init_.S)
set_start_value(model2[], init_.α)
set_start_value(model2[], init_.τ)
set_start_value(model2[], init_.ξ)

@constraint(model2, b == b_eff(
    dwSSFPParams(model2[], model2[] * model2[:S], model2[:S], model2[], TR);
    tp = tp,
    M₀ = scanner.B0
))

@variable(model2, 0 <= signal)
@constraint(model2, model2[:signal] == M⁻(;
    tp = tp,
    dwp = dwSSFPParams(model2[], model2[] * model2[:S], model2[:S], model2[], TR),
    M₀ = scanner.B0
))

@objective(
    model2,
    Max,
    η(
        model2[:signal],
        dwSSFPParams(model2[], model2[] * model2[:S], model2[:S], model2[], TR),
        TR - rf_time - model2[] - model2[]
    )
)

dwp_init2 = dwSSFPParams(
    model2[] |> start_value,
    (model2[] |> start_value) * (model2[:S] |> start_value),
    model2[:S] |> start_value,
    model2[] |> start_value,
    TR
)

signal_init2 = M⁻(;
    tp = tp,
    dwp = dwp_init2,
    M₀ = scanner.B0
)

η(signal_init2, dwp_init2, readout_time)

b_eff(dwp_init2; tp = tp,M₀ = scanner.B0)

optimize!(model2)

results2 = (
    α = value(model2[]),
    τ = value(model2[]),
    ξ = value(model2[]),
    G = value(model2[]) * value(model2[:S]),
    S = value(model2[:S]),
    η = objective_value(model2),
    signal = value(model2[:signal]),
    status = termination_status(model2)
)

All these numbers are reasonable:

dwp_res2 = dwSSFPParams(results2.α, results2.G, results2.S, results2.τ, TR)

b_eff(dwp_res2; tp, M₀=scanner.B0)

signal_res2 = M⁻(;
    tp = tp,
    dwp = dwp_res2,
    M₀ = scanner.B0
)

η(
    signal_res2,
    dwp_res2,
    TR - (results2.τ + results2.ξ + rf_time)
)

Inspecting the results tuples, it seems like the first optimizer starts at a place that is not in the domain even though I gave it a spot in the domain. What could be going on here?

Mac M4 Max, Julia version 1.12.6

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions