Modeling and solver workflow¶
This guide connects Morana’s public objects into a calculation. Use the quickstart for the introductory runnable calculation and the maintained examples for larger cases. The source-derived Python reference is canonical for exact signatures, attributes, and methods. The theory and numerical conventions explain the governing equations and numerical derivations.
The workflow is:
- Define the planar lattice and material layout.
- Define or import materials, assign boundaries, and add a source when needed.
- Create and solve a
ProblemConfiguration. - Inspect the returned
Result, including its immutable input snapshot, balance, and execution report.
API stability¶
Morana is in early development. Its public Python API may change, including through incompatible changes, without a backward-compatibility guarantee. For a released calculation, record the exact Morana version, its version DOI, and the dependency environment. For calculations from the changing development branch, also record the exact Git commit. Review the changelog before upgrading.
Build a problem¶
ProblemConfiguration is the
mutable owner of a problem definition, while
ProblemConfigurationSnapshot
is its immutable, independently owned representation.
morana.solvers.finite_volume provides stateless
finite-volume solve functions that accept either a configuration or its
snapshot. Solver-produced Result objects
retain the snapshot used for the calculation, so later edits to the original
configuration cannot change result provenance. A snapshot can also be solved
again or converted to an independent mutable configuration with
to_configuration().
Define geometry and material layout¶
Create one HexPlanarMesh, then
assign keys to one or more immutable
MaterialSlice objects and stack
them with MaterialMesh.stack(...).
Slices are ordered bottom to top and must all reference the same planar mesh.
A one-layer stack represents a finite hex-z problem. Reflective bottom and top
faces reduce it to the usual two-dimensional radial problem.
MaterialSlice.from_openmc_rings(...)
accepts the documented OpenMC-style ring order. Unassigned positions receive
the built-in excluded key "0"; additional
ExcludedRegion definitions are
local to the material mesh. Each layer has its own compact active-cell order,
so active-cell counts may differ between layers. See
geometry and indexing for lattice IDs, face topology, axial
ordering, and the distinction between planar_id and slice-local active_id.
Add materials¶
Each Material associates one layout key
with optional cross sections and a display color. Material names, layout keys,
and excluded-region kinds are human-readable identifiers: they must contain a
non-whitespace character and only printable characters, while preserving exact
spelling. Material names are also the keys used by slices, sources, and the
configuration mapping. xs=None is useful for layout inspection, but a solve
requires cross sections for every active material and one common group count
across them.
CrossSections requires group vectors
D in cm and sigma_a in 1 / cm, a square sigma_s matrix in 1 / cm,
and an explicit fission value. These numerical inputs must be finite and
nonnegative; zero diffusion is accepted. Supply a zero sigma_s matrix when
there is no scattering.
Ordinary P0 scattering events use sigma_s[g_from, g_to]. The optional
multiplicity_matrix[g_from, g_to] specifies scattering-neutron emission per
event; None is the compact unit-multiplicity convention. A nonfissile value
uses fission=None. A fissionable value supplies FissionData containing
either compact SeparableFission(nu_sigma_f, chi) or general
FissionTransfer(fission_transfer=...) data. Both expose canonical
fission_transfer[g_from, g_to], and the selected representation is retained
in configuration snapshots and result archives.
Scattering and fission therefore share an incident-to-outgoing array
orientation. Assembly places a transfer from g_from to g_to in the
g_to row and g_from column. The
theory guide
defines the resulting signed scattering coupling, fission emission, and
balances. One important modeling constraint is checked during loss assembly:
zero and other nonnegative scattering multiplicities are valid, including
same-group values above one, but the assembled loss diagonal must remain
positive after scattering coupling and leakage are included. In a one-group
material, self-scattering cancels only at unit multiplicity; the
theory guide
derives this limit explicitly.
Materials and cross sections are immutable. Construct a replacement, then use
ProblemConfiguration.replace_material() or set_materials() to install it.
See inspection and output for color precedence.
Import OpenMC MGXS material data¶
CrossSections.from_openmc_mgxs_hdf5(...) reads one explicitly selected
macroscopic material and temperature from a supported OpenMC runtime-MGXS HDF5
library. It constructs the same checked CrossSections used by native Python
inputs, including optional scattering multiplicity, separable or general
fission production, and recoverable-energy data. The caller wraps the returned
cross sections in a Material with a Morana name and display color.
The required diffusion argument selects total-cross-section or P1
transport-corrected coefficients. OpenMC is not imported at runtime, and no
geometry, layout, source, or boundary data are read. See the
OpenMC MGXS import guide for the accepted HDF5 artifact,
data and unit contracts, warnings, limitations, and a runnable example.
Add a fixed source¶
Fixed-source calculations may use a built-in volumetric source, an inhomogeneous boundary contribution, or both. The available built-in volumetric source definitions are:
UniformSourceapplies the same group vector to every active cell;MaterialSourceassigns a group vector by material name; andCellSourcesupplies explicit, possibly ragged, group-major arrays for the bottom-to-top layers.
Source values are volumetric in n / cm^3 / s and follow each layer’s compact
active-ID order. Assembly checks finiteness, nonnegativity, and compatibility
with the active layout and group count; constructing a source alone does not
establish those properties. MaterialSource requires an entry for every active
material, including an explicit zero vector for materials without a source.
Omit source (or clear it with set_source(None)) for a boundary-only
fixed-source calculation. A criticality solve does not accept an
independent source.
The Python reference defines each source form’s exact
array contract. Sources are immutable; construct a replacement and install it
with ProblemConfiguration.set_source().
Assign boundaries¶
BoundaryConditionSet pairs
a BoundaryCondition with a
topology-only selector. A broad globally() assignment can provide a fallback,
while physical-exterior and excluded-interface selectors refine individual
parts of the domain. Every exposed face must resolve before loss assembly or a
solve.
Morana supports reflective, vacuum, prescribed-flux, scalar Robin, partial-current-return, and incoming-current conditions. Nonzero prescribed flux or current contributes to a fixed-source right-hand side; criticality requires homogeneous resolved boundary data.
The boundary conditions and face selection guide explains the selector hierarchy, condition catalog, precedence rules, coverage checks, and supported scope. The exposed-boundary theory derives the corresponding finite-volume equations.
Configure and run the solver¶
Assemble the configuration¶
Create ProblemConfiguration from the mesh, material mapping, material mesh,
boundary set, and optional source. It checks that every active key identifies a
known material, that material names do not collide with excluded-region keys,
and that the material mesh uses the same mesh. Use
set_boundary(),
add_boundary(),
assign_material(),
and set_source()
when changing an existing problem.
set_materials(), replace_material(), and set_material_mesh() replace
checked material definitions or layout, while set_name() changes its
provenance label. Its public state is read-only: direct field replacement raises
AttributeError, and its material mapping is read-only. assign_material()
takes an explicit bottom-to-top axial index and OpenMC lattice position, and
changes only that layer.
The material mapping may also contain definitions that no active mesh cell
uses. unused_material_names reports those names across all axial
layers; call check_no_unused_materials() when a workflow requires every
configured material to be assigned. check_no_unused_materials() raises
ValueError listing the unused names.
Select the solve mode¶
Import solve_fixed_source or solve_keff from morana.solvers.finite_volume.
Each function accepts a ProblemConfiguration or a
ProblemConfigurationSnapshot. A mutable configuration is captured at entry;
a supplied snapshot is used directly. The functions retain no matrix,
factorization, or preconditioner after they return; a successful Result
contains the same snapshot and immutable settings. Snapshot capture is not
synchronized, so capture the solve input before another thread can edit the
configuration:
| Solve mode | Solve function call | Problem prerequisite | Boundary and source restrictions |
|---|---|---|---|
| Fixed source | solve_fixed_source(configuration, settings=None); omitting settings creates default FixedSourceSettings for that call |
A usable loss-minus-fission system | Volumetric and boundary sources are optional; with neither, a nonsingular system returns zero flux. |
| Criticality | solve_keff(configuration, normalization, settings=None); normalization is a FissionSourceNormalization or PowerNormalization, and omitting settings creates default KeffSettings for that call |
Positive fission production | Boundaries must be homogeneous, and the configuration must not contain an independent source. |
Choose numerical policies¶
FixedSourceSettings and KeffSettings are separate immutable value objects.
Their linear_solve and inner_linear_solve fields respectively own a typed
DirectLinearSolveSettings or GmresLinearSolveSettings policy. Direct sparse
solving is the default; GmresLinearSolveSettings selects restarted SciPy
GMRES from a zero initial guess with one typed
NoPreconditioner, JacobiPreconditioner, or IluPreconditioner. Jacobi
uses the assembled diagonal, while threshold-ILU uses its configured drop
tolerance and fill-factor limit. Fixed-source GMRES constructs its
preconditioner for that one solve. Criticality constructs a direct
factorization or GMRES preconditioner once per solve_keff() call and reuses
that setup for the call’s inner right-hand sides.
The theory and numerical conventions
define the direct/GMRES residual, left preconditioning, ordinary power
iteration, and fixed-Wielandt transformation. All numerical controls are
finite; residual controls must be positive, while the flux-roundoff tolerance
may be zero. KeffSettings owns explicit max_outer_iterations and the three
criticality convergence tolerances. Its eigenvalue_iteration policy defaults
to PowerIterationSettings() and may instead be
WielandtShiftSettings(shift_inverse_keff=...). The shift is never adapted;
an unusable shifted operator, failed inner solve, nonfinite inverse
multiplication factor, or candidate flux below the negative roundoff limit
raises ValueError rather than falling back to ordinary power iteration.
In both solve modes, the flux-roundoff threshold is relative to the candidate
vector’s largest absolute component.
Understand execution and convergence¶
Direct fixed-source execution uses SciPy
spsolve.
GMRES execution uses SciPy
gmres
and records its completed Krylov-iteration count. Criticality direct solving
reuses one SciPy
splu
factorization during power iteration. Every completed direct or GMRES solve
must meet the selected policy’s independently calculated true-relative-residual
bound after SciPy returns; backend convergence alone is insufficient.
Criticality records multiplication-factor, flux-shape, and equation-residual
histories. The independently checked linear residual applies to the selected
ordinary or shifted inner operator; the outer residual always checks the
original unshifted eigenvalue equation.
For fixed source, the optional configured volumetric source and any
inhomogeneous resolved boundary terms form the right-hand side of the coupled
loss-minus-fission system. The configured direct or GMRES solution must meet
its typed policy’s true-residual bound, and only negative flux within
flux_nonnegativity_tolerance times the solution’s largest magnitude is
cleaned to zero. The result has keff=None and one recorded linear solve.
For criticality, Morana starts fission-source-normalized power iteration from
a positive vector. Multiplication-factor change, volume-weighted flux-shape
change, and the relative eigenvalue-equation residual must all meet their
explicit tolerances within max_outer_iterations.
Normalize criticality flux¶
A criticality solve requires either FissionSourceNormalization with a
positive rate or PowerNormalization with a positive target in W. The
requested normalization scales the converged flux without changing k_eff.
FissionSourceNormalization applies to the physical fission-neutron
production functional derived from the selected fission data, and
PowerNormalization applies to the recoverable-power functional,
\(\int \kappa\Sigma_f\phi\,dV\). Neither includes a
\(1/k_\mathrm{eff}\) factor. That factor belongs only to the effective
fission source, \(F\phi/k_\mathrm{eff}\), in the eigenvalue equation and
the corresponding criticality balance diagnostic.
PowerNormalization(power=...) uses W and requires every fissionable active
material to provide CrossSections.fission.kappa_sigma_f in eV / cm. Missing
data reject the solve before finite-volume assembly, preventing a partial
power response.
Morana converts the input to joules using the exact SI eV-to-J relation; it
neither assumes a fission multiplicity nor converts separable nu_sigma_f to
a fission rate. Failure to converge raises RuntimeError, while invalid scope,
singular operators, missing power data, or failed numerical checks raise
ValueError.
The multigroup criticality example
provides groupwise kappa_sigma_f alongside the fission-neutron data and
normalizes the result to 100 kW. Use it as a complete example of specifying
materials, the physical target, and iteration controls together.
Interpret a result¶
Result owns a bottom-to-top tuple of
read-only group-major scalar-flux arrays in n / cm^2 / s. Each layer has
shape (groups, active_cells_in_layer) and is selected explicitly with
flux_layer(axial_index). Solver and archive reconstruction checks match every
layer’s compact active-cell count to the retained configuration snapshot.
That snapshot captures named layer assignments, excluded-region definitions,
boundary assignments, materials and cross sections, and the optional built-in
source. Its public object properties return fresh immutable values rather than
live references to retained numerical storage. Use to_configuration() when
an independent mutable problem definition is needed.
The immutable result also owns mode-specific settings, normalization, balance,
and execution diagnostics. normalization is absent for fixed-source results
and required for criticality results; solve_mode is derived from the typed
settings and report. A fixed-source result stores its single
LinearSolveReport directly. A KeffSolveReport stores one
KeffOuterIterationReport per power iteration, including the multiplication
factor, flux convergence, and nested linear report. Its iterations property
counts those records, final_outer_iteration accesses the last one, and
eigenvalue_iteration retains the selected ordinary or fixed-Wielandt policy.
Each linear report records its strategy and preconditioner, iteration count,
and independently calculated true relative residual.
For position-based inspection, use result.cell_at(axial_index, openmc_index).
It resolves the layer-local solver ID and returns a read-only
CellInspection value containing
the assigned material_key, the material’s cross_sections, and the read-only
all-group flux vector. Group ordering remains fast to thermal, so, for
example, cell.cross_sections.sigma_a[group] and cell.flux[group] describe
the same energy group. Excluded positions have no flux or cross sections and
raise ValueError; use result.configuration_snapshot.material_mesh.key_at()
when an excluded-region key itself is needed.
Use Result.save_to_disk(...) to retain a completed calculation in a versioned
non-pickle archive, and Result.load_from_disk(...) to restore it later. The
result archives guide defines the portable archive
contents and validation behavior.
Use Result.plot_matplotlib(group, axial_index) or
Result.plot_plotly(group, axial_index) to inspect a flux slice, and
Result.export_vtm(path) to export flux with the reconstructed material layout.
Plot indices must be in range, and a layer without active cells cannot be
plotted. See inspection and output for artifacts
and display conventions.
Result.balance is a FixedSourceBalance or KeffBalance record. Its
by_group mapping contains immutable group arrays in n / s, and its
by_layer_group mapping contains their bottom-to-top layer decomposition.
Contracting by_layer_group over its layers produces by_group.
balance.scalar derives the corresponding scalar volume-integrated terms, so
scalar and detailed values cannot disagree. balance.loss_fractions and
balance.source_normalized are likewise derived views; a zero-response
fixed-source result returns None for the affected view.
Criticality-result completion also independently recomputes its physical
normalization from the retained flux and configuration snapshot. A source-rate
result must reproduce the requested integrated fission-neutron production
within the result-validation tolerance. A power result must reproduce its converted kappa_sigma_f * flux response and
retain kappa_sigma_f for every fissionable active material. Consequently,
those checks apply to solver results and loaded archives.
The theory guide’s balance section defines the balance terms and signs.
The group and layer-group balance mappings use these keys:
- Fixed source:
source,boundary_source,scattering_coupling,fission_production,fission_emission,removal,absorption,radial_leakage,axial_leakage,net_scattering, andresidual. - Criticality:
fission_production,keff_source,scattering_coupling,removal,absorption,radial_leakage,axial_leakage,net_scattering, andresidual.
scattering_coupling is the signed scattering-neutron emission term derived
from ordinary scattering events and multiplicity; it is distinct from the
ordinary event-loss contribution retained in removal. Fixed-source
fission_emission and criticality keff_source are fission-neutron emission
terms, not scattering production.
Advanced operator access¶
Most workflows should use the solve functions above. For numerical inspection,
verification, or integration with another solver, morana.operators exposes
the compact cross-section, loss, fission, volumetric-source, and boundary-source
assembly stages independently. See
direct finite-volume operator assembly for their data
flow, packing, prerequisites, and ownership rules.
Capability boundaries¶
- Boundary selection supports scalar affine Robin, partial-current-return, and incoming-current response over the documented selector hierarchy. Its scope excludes arbitrary per-cell-face maps, exterior radial-direction splitting, and group-coupled boundary response; see the boundary guide.
- OpenMC integration is limited to selected macroscopic material data from the supported runtime-MGXS format. Geometry, material placement, sources, boundaries, tally-postprocessing stores, and microscopic number densities remain part of the surrounding application.
solve_keff()supports multigroup layered direct or GMRES inner solves with ordinary or fixed-Wielandt-shifted fission-source iteration. Start with the direct ordinary policy as a reference. Consider GMRES and its typed preconditioner for larger sparse systems, and use a fixed Wielandt shift only after a reference run identifies slow outer convergence and provides a safe inverse-k_effestimate below the dominant value. It rejects independent sources, nonhomogeneous boundary data, missing fission normalization, meshes without active or fissionable cells, and unusable ordinary or shifted operators.PowerNormalizationadditionally requires recoverablekappa_sigma_fdata for every fissionable active material.