Skip to content

Add passive_momentum option to neutral_mixed - #592

Open
Vandoo wants to merge 5 commits into
boutproject:masterfrom
Vandoo:passive_momentum_v2
Open

Vandoo wants to merge 5 commits into
boutproject:masterfrom
Vandoo:passive_momentum_v2

Conversation

@Vandoo

@Vandoo Vandoo commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Re-applies the previously-proposed neutral_passive_momentum feature onto current master (the original branch had drifted ~600 commits behind).

New options:

  • passive_momentum: evolve only the neutral density; momentum and pressure are passive (pressure is not added to the solver).
  • temperature_from: take Tn from another species (e.g. the ion, Tn=Ti).
  • evolve_pressure: defaults to !passive_momentum.
  • anomalous_transport_momentum (AnomalousDiffusion): toggle anomalous transport of parallel momentum. (not nessesary).

In passive_momentum mode NVn is set from an ion-equilibrium relation NVn = Nn_eq * Vi, with Nn_eq built from cx/iz/rec collision frequencies, and nu_cx/nu_iz/nu_rec/Nn_eq exposed as diagnostics.

The preconditioner gains a density-only branch (I - gammaDiv_perp(DnnGrad_perp))*ddt(Nn) when pressure is not evolved. Pressure/momentum terms, sources, diagnostics, time-derivative scaling, freeze_low_density and CHECK blocks are gated on the new flags. Reading another species' temperature declares the required GuardedOptions read permission.

Tested on a 3D MAST-U CD case: builds clean, runs to completion with finite fields, reproduces the original branch's passive behaviour (Tn tracks Ti to ~0.1%, 7 evolved fields, consistent neutral diagnostics).

@codecov

codecov Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 38.32335% with 103 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.34%. Comparing base (2a79cf5) to head (b0d09b2).

Files with missing lines Patch % Lines
src/neutral_mixed.cxx 36.80% 91 Missing and 12 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #592      +/-   ##
==========================================
- Coverage   57.73%   57.34%   -0.39%     
==========================================
  Files          97       97              
  Lines       10013    10112      +99     
  Branches     1442     1475      +33     
==========================================
+ Hits         5781     5799      +18     
- Misses       3652     3725      +73     
- Partials      580      588       +8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Re-applies the previously-proposed neutral_passive_momentum feature onto
current master (the original branch had drifted ~600 commits behind).

New options:
- passive_momentum: evolve only the neutral density; momentum and pressure
  are passive (pressure is not added to the solver).
- temperature_from: take Tn from another species (e.g. the ion, Tn=Ti).
- evolve_pressure: defaults to !passive_momentum.
- anomalous_transport_momentum (AnomalousDiffusion): toggle anomalous
  transport of parallel momentum.

In passive_momentum mode NVn is set from an ion-equilibrium relation
NVn = Nn_eq * Vi, with Nn_eq built from cx/iz/rec collision frequencies,
and nu_cx/nu_iz/nu_rec/Nn_eq exposed as diagnostics. The preconditioner
gains a density-only branch (I - gamma*Div_perp(Dnn*Grad_perp))*ddt(Nn)
when pressure is not evolved. Pressure/momentum terms, sources, diagnostics,
time-derivative scaling, freeze_low_density and CHECK blocks are gated on
the new flags. Reading another species' temperature declares the required
GuardedOptions read permission.

Tested on a 3D MAST-U Super-X case: builds clean, runs to completion with
finite fields, reproduces the original branch's passive behaviour
(Tn tracks Ti to ~0.1%, 7 evolved fields, consistent neutral diagnostics).
@Vandoo
Vandoo force-pushed the passive_momentum_v2 branch from 29f1eb4 to 5aa5ae8 Compare June 29, 2026 14:51
Comment thread src/neutral_mixed.cxx
solver->add(Nn, std::string("N") + name);
solver->add(Pn, std::string("P") + name);

evolve_momentum = options["evolve_momentum"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It looks like when passive_momentum is enabled, you need to disable evolve_momentum manually or else it'll be evolved.

One solution could be to make evolve_momentum default to !passive_momentum, and raise a BoutException when both are enabled.

Comment thread src/neutral_mixed.cxx
if (!evolve_pressure && !temperature_from.empty()) {
Tn = get<Field3D>(localstate["temperature"]);
Pn = Nn * Tn;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What happens if evolve_pressure = False and temperature_from = ""?

Comment thread src/neutral_mixed.cxx
/////////////////////////////////////////////////////
// Neutral density
TRACE("Neutral density");
ddt(Nn) = -FV::Div_par_mod<ParLimiter>(Nn, Vn, sound_speed,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The density equation needs Vn. But at this point in the code, Vn is taken from the state, instead of the later recalculation from the ion velocity! This makes it inconsistent.

Usually both Vn and Nn are evolved by the solver in transform(). This means both the density and momentum equations read a consistent, immutable state to prepare time derivatives for the next iteration. In this normal case, it's OK to have the momentum equation after density.

However, now you are recalculating Vn in finally() from the ion velocity. This means that now finally() is inconsistent, with the density equation getting the "previous iteration".

If I'm right, this would still run but probably cause performance issues. I think you need to move the velocity calculation to the top.

Comment thread src/neutral_mixed.cxx Outdated
}

Field3D Nn_eq = (Nnlim * nu_cx + Ne * nu_rec) / softFloor(nu_cx + nu_iz, density_floor);
NVn = Nn_eq * U;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is missing AA, so is off by a factor of 2 for d.

Comment thread src/neutral_mixed.cxx Outdated
}
}

Field3D Nn_eq = (Nnlim * nu_cx + Ne * nu_rec) / softFloor(nu_cx + nu_iz, density_floor);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There is a bug here, it floors collisionality with density_floor. The correct floor to use here is Rnn, which is derived from the "maximum neutral mean free path" neutral_lmax.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would also probably floor nu_cx and nu_rec to Rnn in the numerator for consistency, otherwise you'll have strange results.

Comment thread src/anomalous_diffusion.cxx Outdated
.doc("Allow anomalous diffusion into sheath?")
.withDefault<bool>(false);

anomalous_transport_momentum = options["anomalous_transport_momentum"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What is this used for? The anomalous transport of momentum is just the viscosity eta. To disable it, just set eta to zero.

Comment thread src/neutral_mixed.cxx Outdated

} else if (passive_momentum) {
// NVn set from an equilibrium flow with the ions (not evolved):
// NVn = Nn_eq * Vi, Nn_eq = (Nn*nu_cx + Ne*nu_rec) / (nu_cx + nu_iz)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I haven't quite understood how this works, and why you aren't using plain Nn. Let's discuss this offline. It would be good to explain it in documentation. If these collisionalities are really needed, then I would ask that they are encapsulated in a function defined somewhere in the top of the file so they don't break up the flow so much. Maybe you could do this for the other collisionality block as well at that point. I can help with this if you need!

This is really my fault, I wrote this collisionality system and it's super ugly, so sorry... I will refactor it at some point!

Comment thread src/neutral_mixed.cxx Outdated
}
}

Field3D Nn_eq = (Nnlim * nu_cx + Ne * nu_rec) / softFloor(nu_cx + nu_iz, density_floor);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Equation is missing the pressure gradient term.

Also, could we rearrange this a bit for clarity? Calculating an intermediate Nn_eq makes it look like we are calculating density, which can be confusing on first read. I would prefer if we calculated NVn straight away, even if it's a long equation. Don't forget the mass factor as per the comment below.

Comment thread src/neutral_mixed.cxx Outdated
evolve_momentum = options["evolve_momentum"]
.doc("Evolve parallel neutral momentum?")
.withDefault<bool>(true);
passive_momentum = options["passive_momentum"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I know that this name is now used also in the PR name, but I think there is an opportunity here to better communicate what this model is doing.

In effect, this model replaces the neutral momentum equation with a pressure-diffusion equation, just like what we use for cross-field transport: it's a balance of the neutral pressure gradient and momentum channels from the reactions (CX, IZ and REC).

If we called this option parallel_pressure_diffusion, then it would be clear that the pressure-diffusion model is used to replace the momentum equation. What do you think?

When I was reading passive_momentum I was always assuming this means Vn = Vi, because that's kind of what it sounds like to me.

Vandoo added 2 commits July 3, 2026 11:18
… factor

Three fixes to the quasi-static NVn calculation in passive_momentum mode:
- Add missing parallel pressure-gradient term -(DnnNn/Pnlim)*Grad_par(Pn),
  the diffusive part of Bufferand 2024 eq (3) with D_n = Dnn/Tn.
- Replace softFloor(nu_cx+nu_iz, density_floor) with nu_cx+nu_iz+Rnn:
  density_floor is dimensionally wrong as a collision-frequency floor;
  Rnn (neutral-neutral) is the physically motivated regularisation,
  consistent with how Dnn itself avoids division by zero.
- Multiply both terms by AA: momentum sources in reaction.cxx use
  AA*velocity, so the quasi-static NVn = AA*[Nn_eq*U - D_n*Grad_par(Pn)].
  Previously missing for deuterium (AA=2), harmless only for hydrogen (AA=1).
…ure_from guard

Three changes from PR boutproject#592 review:
- Rename parallel_pressure_diffusion (was passive_momentum): the option replaces
  the neutral momentum equation with a quasi-static pressure-diffusion balance,
  which better describes the physics than the old name.
- evolve_momentum now defaults to !parallel_pressure_diffusion so users don't
  have to set it explicitly; raise BoutException if both are true simultaneously
  (was silently broken).
- Raise BoutException if parallel_pressure_diffusion=true but temperature_from
  is not set: without an ion temperature source Pn stays zero, causing silent
  failures downstream.
Vandoo added a commit to Vandoo/hermes-3 that referenced this pull request Jul 3, 2026
…ure_from guard

Three changes from PR boutproject#592 review:
- Rename parallel_pressure_diffusion (was passive_momentum): the option replaces
  the neutral momentum equation with a quasi-static pressure-diffusion balance,
  which better describes the physics than the old name.
- evolve_momentum now defaults to !parallel_pressure_diffusion so users don't
  have to set it explicitly; raise BoutException if both are true simultaneously
  (was silently broken).
- Raise BoutException if parallel_pressure_diffusion=true but temperature_from
  is not set: without an ion temperature source Pn stays zero, causing silent
  failures downstream.
# Conflicts:
#	src/anomalous_diffusion.cxx
#	src/neutral_mixed.cxx
@Vandoo

Vandoo commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator Author

Update the comment according to the new pattern:

Purpose

Add a reduced "parallel pressure-diffusion" neutral model to neutral_mixed, so a
neutral species can be run by evolving the density only, while the parallel neutral
flow is obtained from a quasi-static parallel momentum balance instead of a dynamical
momentum equation. This avoids resolving fast neutral parallel dynamics, giving a (possible)
cheaper neutral model for edge plasma simulations where the neutral parallel
motion is close to a pressure-diffusion balance (Bufferand et al. 2024, eqs 3-5).

replace the earlier neutral_passive_momentum PR (closed); re-implemented on current
master as branch passive_momentum_v2.

Change Summary

  • New option [<neutral>] parallel_pressure_diffusion (default false) in
    neutral_mixed. When true:
    • only Nn is evolved; evolve_momentum and evolve_pressure default to false
      (setting parallel_pressure_diffusion=true together with evolve_momentum=true
      is rejected with a clear error);
    • NVn is set algebraically each RHS from the quasi-static balance
      NVn = AA * ( Nn_eq * U - (Dnn/Tn) * Grad_par(Pn) ), i.e. an ion-drag
      convective part plus a parallel pressure-gradient diffusive part, with
      Nn_eq = (Nn*nu_cx + Ne*nu_rec) / (nu_cx + nu_iz + Rnn);
    • the neutral temperature is taken from another species via a new
      temperature_from option (e.g. temperature_from = d+), which is required in this
      mode; Pn = Nn * Tn.
  • Added a standalone evolve_pressure option to neutral_mixed (previously Pn was
    always evolved), so the density-only mode is possible.
  • Option renamed from passive_momentum to parallel_pressure_diffusion, and the
    evolve_momentum default and a temperature_from guard added, per review comments.
  • Assumes quasineutrality (Ni = Ne) in Nn_eq for now.

Validation

  • Builds and runs on ARCHER2 (PrgEnv-gnu / GCC) and Pitagora against current master.
  • Exercised on the MAST-U connected-double-null nn_cd case (132x28) in density-only
    mode (evolve_pressure=false, evolve_momentum=false, temperature_from=d+):
    multi-day restarted runs are stable, fields remain finite, and Td tracks Ti as
    intended.
  • Note: in early-phase testing the neutral behaviour differs from the full neutral
    model - divertor electron density (Ne_div) is somewhat lower, Te and Ti are similar,
    neutral density (Nd) is lower and neutral temperature (Td) is higher. This is expected
    given the reduced parallel closure, but is flagged for reviewers as a difference to be
    aware of when substituting this mode for the full model.

AI Assistance

  • An LLM assistant was used to port the original neutral_passive_momentum branch onto
    the more recent master (as passive_momentum_v3), and to help debug the errors found
    during review (e.g. those Mike identified). All resulting changes were reviewed and
    tested by the author before inclusion.

Documentation

  • Option documentation is provided inline via the .doc(...) strings for
    parallel_pressure_diffusion, evolve_pressure, and temperature_from.

Review Notes

  • Nn_eq currently assumes Ni = Ne.
  • temperature_from is mandatory in this mode; the code errors clearly if it is unset.

# Conflicts:
#	src/anomalous_diffusion.cxx
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.

2 participants