• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Open-Sn / opensn / 27259186575

10 Jun 2026 06:11AM UTC coverage: 77.473% (+0.2%) from 77.306%
27259186575

push

github

web-flow
Merge pull request #1084 from wdhawkins/cmfd_tutorial

Adding CMFD tutorial.

24792 of 32001 relevant lines covered (77.47%)

82088528.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.97
/python/lib/solver.cc
1
// SPDX-FileCopyrightText: 2025 The OpenSn Authors <https://open-sn.github.io/opensn/>
2
// SPDX-License-Identifier: MIT
3

4
#include "python/lib/py_wrappers.h"
5
#include <pybind11/functional.h>
6
#include "framework/logging/log.h"
7
#include "framework/runtime.h"
8
#include "framework/field_functions/field_function_grid_based.h"
9
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/acceleration/discrete_ordinates_keigen_acceleration.h"
10
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/acceleration/cmfd_acceleration.h"
11
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/acceleration/scdsa_acceleration.h"
12
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/acceleration/smm_acceleration.h"
13
#include "modules/linear_boltzmann_solvers/discrete_ordinates_curvilinear_problem/discrete_ordinates_curvilinear_problem.h"
14
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/discrete_ordinates_problem.h"
15
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/io/discrete_ordinates_problem_io.h"
16
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/solvers/transient_solver.h"
17
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/solvers/steady_state_solver.h"
18
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/solvers/nl_keigen_solver.h"
19
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/solvers/pi_keigen_solver.h"
20
#include "modules/linear_boltzmann_solvers/lbs_problem/io/lbs_problem_io.h"
21
#include "modules/linear_boltzmann_solvers/lbs_problem/lbs_problem.h"
22
#include "modules/linear_boltzmann_solvers/lbs_problem/compute/lbs_compute.h"
23
#include "modules/solver.h"
24
#include <pybind11/numpy.h>
25
#include <algorithm>
26
#include <cstddef>
27
#include <cstdint>
28
#include <map>
29
#include <memory>
30
#include <string>
31
#include <vector>
32

33
namespace opensn
34
{
35

36
// Wrap problem
37
void
38
WrapProblem(py::module& slv)
966✔
39
{
40
  // clang-format off
41
  // problem base
42
  auto problem = py::class_<Problem, std::shared_ptr<Problem>>(
966✔
43
    slv,
44
    "Problem",
45
    R"(
46
    Base class for all problems.
47

48
    Wrapper of :cpp:class:`opensn::Problem`.
49
    )"
50
  );
966✔
51
  // clang-format on
52
}
966✔
53

54
// Wrap solver
55
void
56
WrapSolver(py::module& slv)
966✔
57
{
58
  // clang-format off
59
  // solver base
60
  auto solver = py::class_<Solver, std::shared_ptr<Solver> >(
966✔
61
    slv,
62
    "Solver",
63
    R"(
64
    Base class for all solvers.
65

66
    Wrapper of :cpp:class:`opensn::Solver`.
67
    )"
68
  );
966✔
69
  solver.def(
966✔
70
    "Initialize",
71
    &Solver::Initialize,
966✔
72
    "Initialize the solver."
73
  );
74
  solver.def(
966✔
75
    "Execute",
76
    &Solver::Execute,
966✔
77
    "Execute the solver."
78
  );
79
  solver.def(
966✔
80
    "Advance",
81
    &Solver::Advance,
966✔
82
    "Advance time values function."
83
  );
84
  // clang-format on
85
}
966✔
86

87
// Wrap LBS solver
88
void
89
WrapLBS(py::module& slv)
966✔
90
{
91
  // clang-format off
92
  // LBS problem
93
  auto lbs_problem = py::class_<LBSProblem, std::shared_ptr<LBSProblem>, Problem>(
966✔
94
    slv,
95
    "LBSProblem",
96
    R"(
97
    Base class for all linear Boltzmann problems.
98

99
    Wrapper of :cpp:class:`opensn::LBSProblem`.
100
    )"
101
  );
966✔
102
  lbs_problem.def(
966✔
103
    "GetScalarFluxFieldFunction",
104
    [](LBSProblem& self, bool only_scalar_flux)
966✔
105
    {
106
      py::list field_function_list_per_group;
666✔
107
      for (unsigned int group = 0; group < self.GetNumGroups(); group++)
25,303✔
108
      {
109
        if (only_scalar_flux)
24,637✔
110
        {
111
          field_function_list_per_group.append(self.CreateScalarFluxFieldFunction(group, 0));
31,312✔
112
        }
113
        else
114
        {
115
          py::list field_function_list_per_moment;
8,981✔
116
          for (unsigned int moment = 0; moment < self.GetNumMoments(); moment++)
37,168✔
117
          {
118
            field_function_list_per_moment.append(self.CreateScalarFluxFieldFunction(group, moment));
56,374✔
119
          }
120
          field_function_list_per_group.append(field_function_list_per_moment);
8,981✔
121
        }
8,981✔
122
      }
123
      return field_function_list_per_group;
666✔
124
    },
×
125
    R"(
126
    Return scalar-flux or flux-moment field functions grouped by energy group.
127

128
    Parameters
129
    ----------
130
    only_scalar_flux : bool, default=True
131
        If True, returns only the zeroth moment (scalar flux) field function for each group.
132
        If False, returns all moment field functions for each group.
133

134
    Returns
135
    -------
136
    Union[List[pyopensn.fieldfunc.FieldFunctionGridBased], List[List[pyopensn.fieldfunc.FieldFunctionGridBased]]]
137
        If ``only_scalar_flux=True``:
138
        ``result[g]`` is the scalar-flux field function for group ``g``.
139

140
        If ``only_scalar_flux=False``:
141
        ``result[g][m]`` is the field function for group ``g`` and moment ``m``.
142

143
    Notes
144
    -----
145
    Field functions are created on demand from the current solver state.
146

147
    The returned field functions are snapshots of the solver state at creation time; they are not
148
    refreshed automatically if the solver state changes.
149

150
    They support ``CanUpdate()`` and ``Update()`` while their owning problem is still alive.
151
    Calling ``Update()`` explicitly refreshes the same field-function object from the solver's
152
    latest state.
153

154
    Calling ``GetScalarFluxFieldFunction(only_scalar_flux=False)`` creates all requested
155
    moments from the current ``phi`` iterate at the time of the call.
156

157
    In the nested form (``only_scalar_flux=False``), the moment index varies fastest
158
    within each group (inner index = moment, outer index = group).
159
    )",
160
    py::arg("only_scalar_flux") = true
966✔
161
  );
162
  lbs_problem.def(
1,932✔
163
    "CreateFieldFunction",
164
    static_cast<std::shared_ptr<FieldFunctionGridBased> (LBSProblem::*)(
1,932✔
165
      const std::string&, const std::string&, double)>(&LBSProblem::CreateFieldFunction),
166
    R"(
167
    Create a named scalar field function derived from a 1D XS or ``power``.
168

169
    Parameters
170
    ----------
171
    name : str
172
        Name to assign to the returned field function.
173
    xs_name : str
174
        Built-in 1D XS name, custom XS name, or the special value ``power``.
175
    power_normalization_target : float, default=-1.0
176
        If positive, scale the derived field function so that the raw power field would
177
        integrate to this total power.
178

179
    Notes
180
    -----
181
    The returned field function is created on demand from the current scalar-flux iterate.
182
    For ordinary XS names this computes ``sum_g xs[g] * phi_g`` at each node.
183

184
    The returned field function is a snapshot of the solver state at creation time; it is not
185
    refreshed automatically if the solver state changes. It supports ``CanUpdate()`` and
186
    ``Update()`` while its owning problem is still alive. Calling ``Update()`` explicitly
187
    recomputes the same field function from the solver's latest state.
188

189
    If ``xs_name == "power"``, the same power-generation formula used elsewhere by the solver
190
    is applied on demand.
191

192
    If ``power_normalization_target > 0``, the returned field function is scaled using the power
193
    implied by the current scalar flux. This scaling affects only the returned field function;
194
    it does not mutate the solver's internal ``phi`` vectors.
195

196
    The returned field function is a fresh object created for this call. It is not
197
    automatically updated by later solves or timesteps.
198
    )",
199
    py::arg("name"),
1,932✔
200
    py::arg("xs_name"),
966✔
201
    py::arg("power_normalization_target") = -1.0
966✔
202
  );
203
  lbs_problem.def(
966✔
204
    "GetTime",
205
    &LBSProblem::GetTime,
966✔
206
    R"(
207
    Get the current simulation time in seconds.
208
    )"
209
  );
210
  lbs_problem.def(
966✔
211
    "GetTimeStep",
212
    &LBSProblem::GetTimeStep,
966✔
213
    R"(
214
    Get the current timestep size.
215
    )"
216
  );
217
  lbs_problem.def(
966✔
218
    "ComputeFissionRate",
219
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
966✔
220
    {
221
      const std::vector<double>* phi_ptr = nullptr;
×
222
      if (scalar_flux_iterate == "old")
×
223
      {
224
        phi_ptr = &self.GetPhiOldLocal();
×
225
      }
226
      else if (scalar_flux_iterate == "new")
×
227
      {
228
        phi_ptr = &self.GetPhiNewLocal();
×
229
      }
230
      else
231
      {
232
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
233
      }
234
      return ComputeFissionRate(self, *phi_ptr);
×
235
    },
236
    R"(
237
    Computes the total fission rate.
238

239
    Parameters
240
    ----------
241
    scalar_flux_iterate : {'old', 'new'}
242
        Specifies which scalar flux vector to use in the calculation.
243
            - 'old': Use the previous scalar flux iterate.
244
            - 'new': Use the current scalar flux iterate.
245

246
    Returns
247
    -------
248
    float
249
        The total fission rate.
250

251
    Raises
252
    ------
253
    ValueError
254
        If `scalar_flux_iterate` is not 'old' or 'new'.
255
    )",
256
    py::arg("scalar_flux_iterate")
966✔
257
  );
258
  lbs_problem.def(
966✔
259
    "ComputeFissionProduction",
260
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
2,506✔
261
    {
262
      const std::vector<double>* phi_ptr = nullptr;
1,540✔
263
      if (scalar_flux_iterate == "old")
1,540✔
264
      {
265
        phi_ptr = &self.GetPhiOldLocal();
88✔
266
      }
267
      else if (scalar_flux_iterate == "new")
1,452✔
268
      {
269
        phi_ptr = &self.GetPhiNewLocal();
1,452✔
270
      }
271
      else
272
      {
273
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
274
      }
275
      return ComputeFissionProduction(self, *phi_ptr);
1,540✔
276
    },
277
    R"(
278
    Computes the total fission production (nu*fission).
279

280
    Parameters
281
    ----------
282
    scalar_flux_iterate : {'old', 'new'}
283
        Specifies which scalar flux vector to use in the calculation.
284
            - 'old': Use the previous scalar flux iterate.
285
            - 'new': Use the current scalar flux iterate.
286

287
    Returns
288
    -------
289
    float
290
        The total fission production.
291

292
    Raises
293
    ------
294
    ValueError
295
        If `scalar_flux_iterate` is not 'old' or 'new'.
296
    )",
297
    py::arg("scalar_flux_iterate")
966✔
298
  );
299
  lbs_problem.def(
966✔
300
    "GetPhiOldLocal",
301
    [](LBSProblem& self)
1,062✔
302
    {
303
      return convert_vector(self.GetPhiOldLocal());
96✔
304
    },
305
    R"(
306
    Get the previous scalar flux iterate (local vector).
307

308
    Returns
309
    -------
310
    memoryview
311
        Memory view of the local old scalar flux vector.
312
    )"
313
  );
314
  lbs_problem.def(
966✔
315
    "GetPhiNewLocal",
316
    [](LBSProblem& self)
1,586✔
317
    {
318
      return convert_vector(self.GetPhiNewLocal());
620✔
319
    },
320
    R"(
321
    Get the current scalar flux iterate (local vector).
322

323
    Returns
324
    -------
325
    memoryview
326
        Memory view of the local new scalar flux vector.
327
    )"
328
  );
329
  lbs_problem.def(
966✔
330
    "WriteFluxMoments",
331
    [](LBSProblem& self, const std::string& file_base)
998✔
332
    {
333
      LBSSolverIO::WriteFluxMoments(self, file_base);
32✔
334
    },
335
    R"(
336
    Write flux moments to file.
337

338
    Parameters
339
    ----------
340
    file_base: str
341
        File basename.
342
    )",
343
    py::arg("file_base")
966✔
344
  );
345
  lbs_problem.def(
966✔
346
    "CreateAndWriteSourceMoments",
347
    [](LBSProblem& self, const std::string& file_base)
970✔
348
    {
349
      std::vector<double> source_moments = self.MakeSourceMomentsFromPhi();
4✔
350
      LBSSolverIO::WriteFluxMoments(self, file_base, source_moments);
4✔
351
    },
4✔
352
    R"(
353
    Write source moments from latest flux iterate to file.
354

355
    Parameters
356
    ----------
357
    file_base: str
358
        File basename.
359
    )",
360
    py::arg("file_base")
966✔
361
  );
362
  lbs_problem.def(
966✔
363
    "ReadFluxMomentsAndMakeSourceMoments",
364
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
966✔
365
    {
366
      auto ext_src_moments = self.GetExtSrcMomentsLocal();
×
367
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, ext_src_moments);
×
368
      self.SetExtSrcMomentsFrom(ext_src_moments);
×
369
      log.Log() << "Making source moments from flux file.";
×
370
      const auto temp_phi = self.GetPhiOldLocal();
×
371
      self.SetPhiOldFrom(self.GetExtSrcMomentsLocal());
×
372
      self.SetExtSrcMomentsFrom(self.MakeSourceMomentsFromPhi());
×
373
      self.SetPhiOldFrom(temp_phi);
×
374
    },
×
375
    R"(
376
    Read flux moments and compute corresponding source moments.
377

378
    Parameters
379
    ----------
380
    file_base: str
381
        File basename.
382
    single_file_flag: bool
383
        True if all flux moments are in a single file.
384
    )",
385
    py::arg("file_base"),
1,932✔
386
    py::arg("single_file_flag")
966✔
387
  );
388
  lbs_problem.def(
966✔
389
    "ReadSourceMoments",
390
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
970✔
391
    {
392
      auto ext_src_moments = self.GetExtSrcMomentsLocal();
4✔
393
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, ext_src_moments);
4✔
394
      self.SetExtSrcMomentsFrom(ext_src_moments);
4✔
395
    },
4✔
396
    R"(
397
    Read source moments from file.
398

399
    Parameters
400
    ----------
401
    file_base: str
402
        File basename.
403
    single_file_flag: bool
404
        True if all source moments are in a single file.
405
    )",
406
    py::arg("file_base"),
1,932✔
407
    py::arg("single_file_flag")
966✔
408
  );
409
  lbs_problem.def(
966✔
410
    "ReadFluxMoments",
411
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
966✔
412
    {
413
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag);
×
414
    },
415
    R"(
416
    Read flux moment data.
417

418
    Parameters
419
    ----------
420
    file_base: str
421
        File basename.
422
    single_file_flag: bool
423
        True if all flux moments are in a single file.
424
    )",
425
    py::arg("file_base"),
1,932✔
426
    py::arg("single_file_flag")
966✔
427
  );
428
  lbs_problem.def(
966✔
429
    "WriteAngularFluxes",
430
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
970✔
431
    {
432
      DiscreteOrdinatesProblemIO::WriteAngularFluxes(self, file_base);
4✔
433
    },
434
    R"(
435
    Write angular flux data to file.
436

437
    Parameters
438
    ----------
439
    file_base: str
440
        File basename.
441
    )",
442
    py::arg("file_base")
966✔
443
  );
444
  lbs_problem.def(
966✔
445
    "ReadAngularFluxes",
446
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
970✔
447
    {
448
      DiscreteOrdinatesProblemIO::ReadAngularFluxes(self, file_base);
4✔
449
    },
450
    R"(
451
    Read angular fluxes from file.
452

453
    Parameters
454
    ----------
455
    file_base: str
456
        File basename.
457
    )",
458
    py::arg("file_base")
966✔
459
  );
460
  lbs_problem.def(
966✔
461
    "SetPointSources",
462
    [](LBSProblem& self, py::kwargs& params)
966✔
463
    {
464
      for (auto [key, value] : params)
×
465
      {
466
        auto c_key = key.cast<std::string>();
×
467
        if (c_key == "clear_point_sources")
×
468
          self.ClearPointSources();
×
469
        else if (c_key == "point_sources")
×
470
        {
471
          auto sources = value.cast<py::list>();
×
472
          for (auto source : sources)
×
473
          {
474
            self.AddPointSource(source.cast<std::shared_ptr<PointSource>>());
×
475
          }
476
        }
×
477
        else
478
          throw std::runtime_error("Invalid argument provided to SetPointSources.\n");
×
479
      }
×
480
    },
×
481
    R"(
482
    Set or clear point sources.
483

484
    Parameters
485
    ----------
486
    clear_point_sources: bool, default=False
487
        If true, all current the point sources of the problem are deleted.
488
    point_sources: List[pyopensn.source.PointSource]
489
        List of new point sources to be added to the problem.
490
    )"
491
  );
492
  lbs_problem.def(
966✔
493
    "SetVolumetricSources",
494
    [](LBSProblem& self, py::kwargs& params)
1,014✔
495
    {
496
      for (auto [key, value] : params)
144✔
497
      {
498
        auto c_key = key.cast<std::string>();
48✔
499
        if (c_key == "clear_volumetric_sources")
48✔
500
          self.ClearVolumetricSources();
24✔
501
        else if (c_key == "volumetric_sources")
24✔
502
        {
503
          auto sources = value.cast<py::list>();
24✔
504
          for (auto source : sources)
72✔
505
          {
506
            self.AddVolumetricSource(source.cast<std::shared_ptr<VolumetricSource>>());
48✔
507
          }
508
        }
24✔
509
        else
510
          throw std::runtime_error("Invalid argument provided to SetVolumetricSources.\n");
×
511
      }
48✔
512
    },
48✔
513
    R"(
514
    Set or clear volumetric sources.
515

516
    Parameters
517
    ----------
518
    clear_volumetric_sources: bool, default=False
519
        If true, all current the volumetric sources of the problem are deleted.
520
    volumetric_sources: List[pyopensn.source.VolumetricSource]
521
        List of new volumetric sources to be added to the problem.
522
    )"
523
  );
524
  lbs_problem.def(
966✔
525
    "SetXSMap",
526
    [](LBSProblem& self, py::kwargs& params)
1,318✔
527
    {
528
      BlockID2XSMap xs_map;
352✔
529
      for (auto [key, value] : params)
1,056✔
530
      {
531
        auto c_key = key.cast<std::string>();
352✔
532
        if (c_key == "xs_map")
352✔
533
        {
534
          auto xs_entries = value.cast<py::list>();
352✔
535
          for (auto entry : xs_entries)
1,056✔
536
          {
537
            InputParameters xs_entry_pars = LBSProblem::GetXSMapEntryBlock();
352✔
538
            xs_entry_pars.AssignParameters(pyobj_to_param_block("", entry.cast<py::dict>()));
352✔
539
            const auto& block_ids =
352✔
540
              xs_entry_pars.GetParam("block_ids").GetVectorValue<unsigned int>();
352✔
541
            auto xs = xs_entry_pars.GetSharedPtrParam<MultiGroupXS>("xs");
352✔
542
            for (const auto& block_id : block_ids)
704✔
543
              xs_map[block_id] = xs;
352✔
544
          }
352✔
545
        }
352✔
546
        else
547
          throw std::runtime_error("Invalid argument provided to SetXSMap.\n");
×
548
      }
352✔
549
      self.SetBlockID2XSMap(xs_map);
352✔
550
    },
352✔
551
    R"(
552
    Replace the block-id to cross-section map.
553

554
    Parameters
555
    ----------
556
    xs_map: List[Dict]
557
        A list of block-id to cross-section mapping dictionaries. Each dictionary supports:
558
          - block_ids: List[int] (required)
559
              Mesh block ids to associate with the cross section.
560
          - xs: pyopensn.xs.MultiGroupXS (required)
561
              Cross section object.
562

563
    Notes
564
    -----
565
    The problem is refreshed immediately after replacing the map. Material metadata,
566
    precursor storage, GPU carriers, and derived solver state owned by the concrete
567
    problem are rebuilt for the new cross sections.
568

569
    If ``options.use_precursors=True``, this flag remains active across XS-map
570
    changes even when the current map has no precursor-bearing material. In that
571
    case delayed-neutron source terms are simply inactive until a later map provides
572
    precursor data again.
573

574
    Existing precursor concentrations are remapped by local cell and precursor-family
575
    index. When the new material for a cell has fewer precursor families, extra old
576
    families are dropped. When it has more families, newly introduced families are
577
    initialized to zero. If a cell changes through a material with zero precursors,
578
    the precursor history for that cell is discarded and any later reintroduced
579
    precursor families start from zero.
580

581
    If any fissionable material in the new map contains delayed-neutron precursor
582
    data and ``options.use_precursors=True``, all fissionable materials in the map
583
    must contain precursor data. Non-fissionable materials may have zero precursors.
584

585
    Forward/adjoint mode toggles via :meth:`LBSProblem.SetAdjoint` do not change this map.
586
    The ``MultiGroupXS`` objects themselves are mutable and shared by pointer. If the same
587
    ``MultiGroupXS`` handle is shared across multiple problems, toggling adjoint mode in one
588
    problem also changes the transport mode seen by the others.
589
    )"
590
  );
591
  lbs_problem.def(
966✔
592
    "ZeroPhi",
593
    [](LBSProblem& self)
1,158✔
594
    {
595
      self.ZeroPhi();
192✔
596
    },
597
    R"(
598
    Zero the scalar-flux vectors (``phi_old`` and ``phi_new``) in-place.
599
    )"
600
  );
601
  lbs_problem.def(
966✔
602
    "SetAdjoint",
603
    [](LBSProblem& self, bool adjoint)
966✔
604
    {
605
      self.SetAdjoint(adjoint);
24✔
606
    },
607
    py::arg("adjoint") = true,
966✔
608
    R"(
609
    Set forward/adjoint transport mode.
610

611
    Parameters
612
    ----------
613
    adjoint: bool, default=True
614
        ``True`` enables adjoint mode and ``False`` enables forward mode.
615

616
    Notes
617
    -----
618
    This is one of two supported mode-setting paths in Python:
619
      1. ``options={'adjoint': ...}`` in the problem constructor.
620
      2. ``SetAdjoint(...)`` (this method).
621

622
    If this changes mode, OpenSn performs a full mode-transition reset:
623
      - Materials are reinitialized in the selected mode.
624
      - Point and volumetric sources are cleared.
625
      - Boundary conditions are cleared.
626
      - Scalar and angular flux vectors are zeroed.
627

628
    If this is called with the same mode as the current setting, no reset is performed.
629

630
    The block-id to cross-section map is preserved across the transition.
631
    However, the mode change is applied to the mapped ``MultiGroupXS`` objects themselves.
632
    If those objects are shared with other problems, they observe the same mode toggle.
633

634
    After a mode change, reapply the desired driving terms before solving, typically:
635
      - :meth:`LBSProblem.SetPointSources`
636
      - :meth:`LBSProblem.SetVolumetricSources`
637
      - :meth:`DiscreteOrdinatesProblem.SetBoundaryOptions`
638

639
    This routine is intentionally destructive with respect to source/boundary/flux state
640
    to avoid hidden coupling between forward and adjoint setups.
641
    )"
642
  );
643
  lbs_problem.def(
966✔
644
    "SetForward",
645
    &LBSProblem::SetForward,
966✔
646
    R"(
647
    Set forward transport mode.
648

649
    Equivalent to ``SetAdjoint(False)``.
650
    )"
651
  );
652
  lbs_problem.def(
966✔
653
    "IsAdjoint",
654
    &LBSProblem::IsAdjoint,
966✔
655
    R"(
656
    Return ``True`` if the problem is in adjoint mode, otherwise ``False``.
657
    )"
658
  );
659
  lbs_problem.def(
966✔
660
    "IsTimeDependent",
661
    &LBSProblem::IsTimeDependent,
966✔
662
    R"(
663
    Return ``True`` if the problem is in time-dependent mode, otherwise ``False``.
664
    )"
665
  );
666

667
  // discrete ordinate solver
668
  auto do_problem = py::class_<DiscreteOrdinatesProblem, std::shared_ptr<DiscreteOrdinatesProblem>,
966✔
669
                               LBSProblem>(
670
    slv,
671
    "DiscreteOrdinatesProblem",
672
    R"(
673
    Base class for discrete ordinates problems in Cartesian geometry.
674

675
    This class implements the algorithms necessary to solve a problem using the discrete ordinates method.
676
    When paired with a solver base, the result is a solver instance configured for a specific problem type
677
    (steady-state, transient, adjoint, k-eigenvalue, etc.).
678

679
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesProblem`.
680
    )"
681
  );
966✔
682
  do_problem.def(
1,932✔
683
    py::init(
966✔
684
      [](py::kwargs& params)
1,286✔
685
      {
686
        return DiscreteOrdinatesProblem::Create(kwargs_to_param_block(params));
1,286✔
687
      }
688
    ),
689
    R"(
690
    Construct a discrete ordinates problem with Cartesian geometry.
691

692
    Parameters
693
    ----------
694
    mesh : MeshContinuum
695
        The spatial mesh.
696
    num_groups : int
697
        The total number of energy groups.
698
    groupsets : List[Dict], default=[]
699
        A list of input parameter blocks, each block provides the iterative properties for a
700
        groupset. Each dictionary supports:
701
          - groups_from_to: List[int] (required)
702
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
703
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
704
              Handle to an angular quadrature.
705
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
706
              Angle aggregation method to use during sweeping.
707
          - angle_aggregation_num_subsets: int, default=1
708
              Number of angle subsets used for aggregation.
709
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
710
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
711
              Iterative method used for inner linear solves.
712
          - l_abs_tol: float, default=1.0e-6
713
              Inner linear solver absolute residual tolerance.
714
          - l_max_its: int, default=200
715
              Inner linear solver maximum iterations.
716
          - gmres_restart_interval: int, default=30
717
              GMRES restart interval, if GMRES is used.
718
          - allow_cycles: bool, default=True
719
              Whether cyclic dependencies are allowed in sweeps.
720
          - apply_wgdsa: bool, default=False
721
              Enable within-group DSA for this groupset.
722
          - wgdsa_l_abs_tol: float, default=1.0e-4
723
              WGDSA linear absolute tolerance.
724
          - wgdsa_l_max_its: int, default=30
725
              WGDSA maximum iterations.
726
          - wgdsa_verbose: bool, default=False
727
              Verbose WGDSA output.
728
          - wgdsa_petsc_options: str, default=''
729
              PETSc options string for the WGDSA solver.
730
          - wgdsa_solver_policy: {'auto', 'direct', 'iterative', 'petsc_options'}, default='auto'
731
              WGDSA diffusion solver policy. ``auto`` uses a direct PETSc LU solve below
732
              ``wgdsa_direct_solve_threshold`` global unknowns and an iterative solve
733
              otherwise. ``petsc_options`` lets ``wgdsa_petsc_options`` control the
734
              PETSc KSP/PC setup.
735
          - wgdsa_direct_solve_threshold: int, default=20000
736
              Maximum global WGDSA diffusion unknown count for the automatic direct solve.
737
          - apply_tgdsa: bool, default=False
738
              Enable two-grid DSA for this groupset.
739
          - tgdsa_l_abs_tol: float, default=1.0e-4
740
              TGDSA linear absolute tolerance.
741
          - tgdsa_l_max_its: int, default=30
742
              TGDSA maximum iterations.
743
          - tgdsa_verbose: bool, default=False
744
              Verbose TGDSA output.
745
          - tgdsa_petsc_options: str, default=''
746
              PETSc options string for the TGDSA solver.
747
          - tgdsa_solver_policy: {'auto', 'direct', 'iterative', 'petsc_options'}, default='auto'
748
              TGDSA diffusion solver policy. ``auto`` uses a direct PETSc LU solve below
749
              ``tgdsa_direct_solve_threshold`` global unknowns and an iterative solve
750
              otherwise. ``petsc_options`` lets ``tgdsa_petsc_options`` control the
751
              PETSc KSP/PC setup.
752
          - tgdsa_direct_solve_threshold: int, default=20000
753
              Maximum global TGDSA diffusion unknown count for the automatic direct solve.
754
    xs_map : List[Dict], default=[]
755
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
756
          - block_ids: List[int] (required)
757
              Mesh block IDs to associate with the cross section.
758
          - xs: pyopensn.xs.MultiGroupXS (required)
759
              Cross-section object to assign to the specified blocks.
760
    boundary_conditions: List[Dict], default=[]
761
        A list containing tables for each boundary specification. Each dictionary supports:
762
          - name: str (required)
763
              Boundary name that identifies the specific boundary.
764
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
765
              Boundary type specification.
766
          - group_strength: List[float], optional
767
              Required when ``type='isotropic'``. Isotropic strength per group.
768
          - start_time: float, optional
769
              Active start time for isotropic boundaries only. Defaults to -infinity.
770
          - end_time: float, optional
771
              Active end time for isotropic boundaries only. Defaults to infinity.
772
          - function: AngularFluxFunction, optional
773
              Required when ``type='arbitrary'`` unless ``time_function`` is supplied. Callable
774
              that returns incoming angular flux from group and direction.
775
          - time_function: AngularFluxTimeFunction, optional
776
              Required when ``type='arbitrary'`` unless ``function`` is supplied. Callable that
777
              returns incoming angular flux from group, direction, and time.
778
        Isotropic boundaries may use ``start_time``/``end_time`` for simple on/off behavior.
779
        Arbitrary boundaries must specify exactly one of ``function`` or ``time_function``; use
780
        ``time_function`` for time-dependent arbitrary inflow and handle any active window inside
781
        that callback.
782
    point_sources: List[pyopensn.source.PointSource], default=[]
783
        A list of point sources.
784
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
785
        A list of volumetric sources.
786
    options : Dict, default={}
787
        A block of optional configuration parameters, including:
788
          - max_mpi_message_size: int, default=32768
789
          - restart_writes_enabled: bool, default=False
790
              Enable restart dump writes for solvers that support restart output.
791
          - write_delayed_psi_to_restart: bool, default=True
792
              Include delayed sweep angular-flux buffers. Full continuation restarts require
793
              these buffers whenever the problem has delayed sweep angular state, including
794
              partitioned parallel, reflected-boundary, and cyclic-sweep cases. These buffers
795
              are optional when a steady-state restart is used only as a transient initial
796
              condition because the transient initialization can reconstruct angular state
797
              from the flux moments.
798
          - write_angular_flux_to_restart: bool, default=True
799
              Include stored angular fluxes in restart dumps when ``save_angular_flux=True``.
800
              This is required for continuing a time-dependent restart, but optional when a
801
              steady-state restart is used only as a transient initial condition.
802
          - read_restart_path: str, default=''
803
              File stem for reading a full restart. The number of MPI ranks and partitioned
804
              state layout must match the run that wrote the restart files.
805
          - read_initial_condition_path: str, default=''
806
              File stem for reading restart data as an initial condition. A steady-state
807
              restart may be used by ``TransientSolver`` through this option.
808
          - write_restart_path: str, default=''
809
              File stem for restart dump writes. OpenSn appends the MPI rank and
810
              ``.restart.h5`` to this stem.
811
          - write_restart_time_interval: int, default=0
812
            (must be 0 or >=30)
813
          - use_precursors: bool, default=True
814
            Enable delayed-neutron precursor treatment. This is treated as user intent and remains
815
            active across later ``SetXSMap`` calls, even if the current XS map temporarily contains
816
            no precursor-bearing material. When XS maps are swapped, existing precursor
817
            concentrations are remapped by cell and precursor-family index; new families start at
818
            zero and removed families are discarded. If any fissionable material in the active map
819
            has precursor data and ``use_precursors=True``, all fissionable materials in that map
820
            must have precursor data. Non-fissionable materials may have zero precursors.
821
          - use_source_moments: bool, default=False
822
          - save_angular_flux: bool, default=False
823
            Store angular flux state (`psi`) for transient mode, angular-flux
824
            field functions, and angular-flux I/O.
825
          - adjoint: bool, default=False
826
          - verbose_inner_iterations: bool, default=True
827
            Print inner iteration details, including WGS and AGS iterations.
828
          - verbose_outer_iterations: bool, default=True
829
            Print outer solver progress, including PI/NLKE iterations and transient steps.
830
          - max_ags_iterations: int, default=100
831
          - ags_tolerance: float, default=1.0e-6
832
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
833
          - power_default_kappa: float, default=3.20435e-11
834
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
835
          - field_function_prefix: str, default=''
836
        These options are applied at problem creation.
837
    sweep_type : str, default="AAH"
838
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
839
        Both sweep types support time-dependent (transient) mode.
840
    time_dependent : bool, default=False
841
        If true, the problem starts in time-dependent mode. Otherwise it starts in
842
        steady-state mode. Requires ``options.save_angular_flux=True``.
843
        Both ``AAH`` and ``CBC`` sweep types support time-dependent mode.
844
    use_gpus : bool, default=False
845
        A flag specifying whether GPU acceleration is used for the sweep. Both
846
        ```AAH``` and ```CBC``` sweep types support GPU acceleration.
847
    )"
848
  );
849
  do_problem.def(
966✔
850
    "SetTimeDependentMode",
851
    &DiscreteOrdinatesProblem::SetTimeDependentMode,
966✔
852
    R"(
853
    Set the problem to time-dependent mode.
854

855
    Notes
856
    -----
857
    Switch problem from steady-state to time-dependent mode. This updates problem
858
    internals (sweep chunk mode and source-function) while preserving user boundary
859
    conditions and fixed sources.
860

861
    Requires ``options.save_angular_flux=True`` at problem creation.
862
    )"
863
  );
864
  do_problem.def(
966✔
865
    "SetSteadyStateMode",
866
    &DiscreteOrdinatesProblem::SetSteadyStateMode,
966✔
867
    R"(
868
    Set the problem to steady-state mode.
869

870
    Notes
871
    -----
872
    Switch problem from time-dependent to steady-state mode. This updates problem
873
    internals (sweep chunk mode and source-function) while preserving user boundary
874
    conditions and fixed sources.
875
    )"
876
  );
877
  do_problem.def(
966✔
878
    "IsTimeDependent",
879
    &DiscreteOrdinatesProblem::IsTimeDependent,
966✔
880
    R"(
881
    Return ``True`` if the problem is currently in time-dependent mode.
882
    )"
883
  );
884
  do_problem.def(
966✔
885
    "SetBoundaryOptions",
886
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
1,146✔
887
    {
888
      bool clear_boundary_conditions = false;
180✔
889
      std::vector<InputParameters> boundary_params;
180✔
890
      for (auto [key, value] : params)
716✔
891
      {
892
        auto c_key = key.cast<std::string>();
356✔
893
        if (c_key == "clear_boundary_conditions")
356✔
894
          clear_boundary_conditions = value.cast<bool>();
176✔
895
        else if (c_key == "boundary_conditions")
180✔
896
        {
897
          auto boundaries = value.cast<py::list>();
180✔
898
          for (auto boundary : boundaries)
968✔
899
          {
900
            InputParameters input = DiscreteOrdinatesProblem::GetBoundaryOptionsBlock();
608✔
901
            input.AssignParameters(pyobj_to_param_block("", boundary.cast<py::dict>()));
608✔
902
            boundary_params.push_back(std::move(input));
608✔
903
          }
608✔
904
        }
180✔
905
        else
906
          throw std::runtime_error("Invalid argument provided to SetBoundaryOptions.\n");
×
907
      }
356✔
908
      if (clear_boundary_conditions or not boundary_params.empty())
180✔
909
        self.SetBoundaryOptions(boundary_params, clear_boundary_conditions);
180✔
910
    },
180✔
911
    R"(
912
    Set or clear boundary conditions.
913

914
    Parameters
915
    ----------
916
    clear_boundary_conditions: bool, default=False
917
        If true, all current boundary conditions are deleted.
918
    boundary_conditions: List[Dict]
919
        A list of boundary condition dictionaries. Each dictionary supports:
920
          - name: str (required)
921
              Boundary name that identifies the specific boundary.
922
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
923
              Boundary type specification.
924
          - group_strength: List[float], optional
925
              Required when ``type='isotropic'``. Isotropic strength per group.
926
          - start_time: float, optional
927
              Active start time for isotropic boundaries only. Defaults to -infinity.
928
          - end_time: float, optional
929
              Active end time for isotropic boundaries only. Defaults to infinity.
930
          - function: AngularFluxFunction, optional
931
              Required when ``type='arbitrary'`` unless ``time_function`` is supplied. Callable
932
              that returns incoming angular flux from group and direction.
933
          - time_function: AngularFluxTimeFunction, optional
934
              Required when ``type='arbitrary'`` unless ``function`` is supplied. Callable that
935
              returns incoming angular flux from group, direction, and time.
936
        Isotropic boundaries may use ``start_time``/``end_time`` for simple on/off behavior.
937
        Arbitrary boundaries must specify exactly one of ``function`` or ``time_function``; use
938
        ``time_function`` for time-dependent arbitrary inflow and handle any active window inside
939
        that callback.
940

941
    Notes
942
    -----
943
    Mode transitions via :meth:`LBSProblem.SetAdjoint` clear all boundary conditions.
944
    Reapply boundaries with this method before solving in the new mode.
945
    )"
946
  );
947
  do_problem.def(
966✔
948
    "ZeroPsi",
949
    [](DiscreteOrdinatesProblem& self)
966✔
950
    {
951
      self.ZeroPsi();
×
952
    },
953
    R"(
954
    Zero the angular-flux storage.
955
    )"
956
  );
957
  do_problem.def(
966✔
958
    "GetPsi",
959
    [](DiscreteOrdinatesProblem& self)
1,014✔
960
    {
961
      const auto& psi = self.GetPsiNewLocal();
48✔
962
      py::list psi_list;
48✔
963
      for (const auto& vec : psi)
96✔
964
      {
965
        auto array = py::array_t<double>(static_cast<py::ssize_t>(vec.size()));
48✔
966
        std::copy(vec.begin(), vec.end(), static_cast<double*>(array.mutable_data()));
48✔
967
        psi_list.append(array);
48✔
968
      }
48✔
969
      return psi_list;
48✔
970
    },
×
971
    R"(
972
    Return psi as a list of NumPy arrays (float64).
973

974
    The arrays are copies of the current angular-flux state. Mutating them does not
975
    mutate the problem.
976
    )"
977
  );
978
  do_problem.def(
966✔
979
    "GetAngularFieldFunctionList",
980
    [](DiscreteOrdinatesProblem& self, py::list groups, py::list angles)
966✔
981
    {
982
      std::vector<unsigned int> group_ids;
×
983
      std::vector<size_t> angle_ids;
×
984
      group_ids.reserve(groups.size());
×
985
      angle_ids.reserve(angles.size());
×
986

987
      for (py::handle g : groups)
×
988
        group_ids.push_back(g.cast<unsigned int>());
×
989
      for (py::handle a : angles)
×
990
        angle_ids.push_back(a.cast<size_t>());
×
991

992
      auto ff_list = self.CreateAngularFluxFieldFunctionList(group_ids, angle_ids);
×
993
      py::list out;
×
994
      for (const auto& ff : ff_list)
×
995
        out.append(ff);
×
996
      return out;
×
997
    },
×
998
    R"(
999
    Create field functions for selected angular flux components.
1000

1001
    Note: You must enable angular flux storage (``save_angular_flux=True``) in
1002
    the problem options. For transient problems this is required. Otherwise
1003
    the returned field functions will remain zero.
1004

1005
    Example
1006
    -------
1007
    .. code-block:: python
1008

1009
       solver.Initialize()
1010
       solver.Execute()
1011
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
1012

1013
    For transient/time-dependent runs, each field function is still a snapshot. Either call
1014
    this after each timestep to create a fresh object or keep the returned object and call
1015
    ``Update()`` after each timestep before exporting or interpolating it. Two common patterns
1016
    are:
1017

1018
    1) Use ``TransientSolver.Execute()`` with a post-advance callback:
1019

1020
    .. code-block:: python
1021

1022
       solver = TransientSolver(problem=phys)
1023
       solver.Initialize()
1024
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
1025

1026
       def post_advance():
1027
           for ff in ang_ff:
1028
               ff.Update()
1029
           FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
1030

1031
       solver.SetPostAdvanceCallback(post_advance)
1032
       solver.Execute()
1033

1034
    2) Use a custom Python loop with ``TransientSolver.Advance()``:
1035

1036
    .. code-block:: python
1037

1038
       solver = TransientSolver(problem=phys)
1039
       solver.Initialize()
1040
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
1041
       for _ in range(num_steps):
1042
           solver.Advance()
1043
           for ff in ang_ff:
1044
               ff.Update()
1045
           FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
1046

1047
    Parameters
1048
    ----------
1049
    groups : List[int]
1050
        Global group indices to export.
1051
    angles : List[int]
1052
        Angle indices within the groupset quadrature for each group.
1053

1054
    Returns
1055
    -------
1056
    List[pyopensn.fieldfunc.FieldFunctionGridBased]
1057
        Field functions for the requested ``(group, angle)`` pairs. Each returned field function
1058
        is a snapshot, but supports ``CanUpdate()`` and ``Update()`` while its owning problem is
1059
        alive.
1060
    )",
1061
    py::arg("groups"),
1,932✔
1062
    py::arg("angles")
966✔
1063
  );
1064
  do_problem.def(
966✔
1065
    "ComputeLeakage",
1066
    [](DiscreteOrdinatesProblem& self, py::list bnd_names)
1,389✔
1067
    {
1068
      auto grid = self.GetGrid();
423✔
1069
      // get the supported boundaries
1070
      std::map<std::string, std::uint64_t> allowed_bd_names = grid->GetBoundaryNameMap();
423✔
1071
      std::map<std::uint64_t, std::string> allowed_bd_ids = grid->GetBoundaryIDMap();
423✔
1072
      const auto coord_sys = grid->GetCoordinateSystem();
423✔
1073
      const auto mesh_type = grid->GetType();
423✔
1074
      const auto dim = grid->GetDimension();
423✔
1075
      // get the boundaries to parse, preserving user order
1076
      std::vector<std::uint64_t> bndry_ids;
423✔
1077
      if (bnd_names.size() > 1)
423✔
1078
      {
1079
        for (py::handle name : bnd_names)
2,488✔
1080
        {
1081
          auto sname = name.cast<std::string>();
1,648✔
1082
          if (coord_sys == CoordinateSystemType::CYLINDRICAL && dim == 2)
1,648✔
1083
          {
1084
            if (sname == "xmin" || sname == "xmax" || sname == "ymin" || sname == "ymax")
24✔
1085
              throw std::runtime_error("ComputeLeakage: Boundary name '" + sname +
×
1086
                                       "' is invalid for cylindrical orthogonal meshes. "
1087
                                       "Use rmin, rmax, zmin, zmax.");
×
1088

1089
            if (mesh_type == MeshType::ORTHOGONAL)
24✔
1090
            {
1091
              if (sname == "rmin") sname = "xmin";
24✔
1092
              else if (sname == "rmax") sname = "xmax";
24✔
1093
              else if (sname == "zmin") sname = "ymin";
16✔
1094
              else if (sname == "zmax") sname = "ymax";
8✔
1095
            }
1096
          }
1097
          bndry_ids.push_back(allowed_bd_names.at(sname));
1,648✔
1098
        }
1,648✔
1099
      }
1100
      else
1101
      {
1102
        bndry_ids = self.GetGrid()->GetUniqueBoundaryIDs();
9✔
1103
      }
1104
      // compute the leakage
1105
      std::map<std::uint64_t, std::vector<double>> leakage = ComputeLeakage(self, bndry_ids);
423✔
1106
      const bool rz_ortho = (coord_sys == CoordinateSystemType::CYLINDRICAL &&
846✔
1107
                             mesh_type == MeshType::ORTHOGONAL && dim == 2);
423✔
1108

1109
      auto ToRZName = [](const std::string& name)
447✔
1110
      {
1111
        if (name == "xmin") return std::string("rmin");
24✔
1112
        if (name == "xmax") return std::string("rmax");
24✔
1113
        if (name == "ymin") return std::string("zmin");
16✔
1114
        if (name == "ymax") return std::string("zmax");
8✔
1115
        return name;
×
1116
      };
1117

1118
      // convert result to native Python
1119
      py::dict result;
423✔
1120
      for (const auto& bndry_id : bndry_ids)
2,077✔
1121
      {
1122
        const auto it = leakage.find(bndry_id);
1,654✔
1123
        if (it == leakage.end())
1,654✔
1124
          continue;
×
1125
        // construct numpy array and copy contents
1126
        const auto& grp_wise_leakage = it->second;
1,654✔
1127
        py::array_t<double> np_vector(py::ssize_t(grp_wise_leakage.size()));
1,654✔
1128
        auto buffer = np_vector.request();
1,654✔
1129
        auto *np_vector_data = static_cast<double*>(buffer.ptr);
1,654✔
1130
        std::copy(grp_wise_leakage.begin(), grp_wise_leakage.end(), np_vector_data);
1,654✔
1131
        std::string name = allowed_bd_ids.at(bndry_id);
1,654✔
1132
        if (rz_ortho)
1,654✔
1133
          name = ToRZName(name);
24✔
1134
        result[py::str(name)] = std::move(np_vector);
3,308✔
1135
      }
1,654✔
1136

1137
      return result;
423✔
1138
    },
846✔
1139
    R"(
1140
    Compute leakage for the problem.
1141

1142
    Parameters
1143
    ----------
1144
    bnd_names : List[str]
1145
        A list of boundary names for which leakage should be computed.
1146

1147
    Returns
1148
    -------
1149
    Dict[str, numpy.ndarray]
1150
        A dictionary mapping boundary names to group-wise leakage vectors.
1151
        Each array contains the outgoing angular flux (per group) integrated over
1152
        the corresponding boundary surface.
1153

1154
    Raises
1155
    ------
1156
    RuntimeError
1157
        If `save_angular_flux` option was not enabled during problem setup.
1158

1159
    ValueError
1160
        If one or more boundary ids are not present on the current mesh.
1161
    )",
1162
    py::arg("bnd_names")
966✔
1163
  );
1164

1165
  // discrete ordinates curvilinear problem
1166
  auto do_curvilinear_problem = py::class_<DiscreteOrdinatesCurvilinearProblem,
966✔
1167
                                           std::shared_ptr<DiscreteOrdinatesCurvilinearProblem>,
1168
                                           DiscreteOrdinatesProblem>(
1169
    slv,
1170
    "DiscreteOrdinatesCurvilinearProblem",
1171
    R"(
1172
    Base class for discrete ordinates problems in curvilinear geometry.
1173

1174
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesCurvilinearProblem`.
1175
    )"
1176
  );
966✔
1177
  do_curvilinear_problem.def(
1,932✔
1178
    py::init(
966✔
1179
      [](py::kwargs& params)
80✔
1180
      {
1181
        return DiscreteOrdinatesCurvilinearProblem::Create(kwargs_to_param_block(params));
80✔
1182
      }
1183
    ),
1184
    R"(
1185
    Construct a discrete ordinates problem for curvilinear geometry.
1186

1187
    Warnings
1188
    --------
1189
       DiscreteOrdinatesCurvilinearProblem is **experimental** and should be used with caution!
1190

1191
    Parameters
1192
    ----------
1193
    mesh : MeshContinuum
1194
        The spatial mesh.
1195
    coord_system : int
1196
        Coordinate system to use. Must be set to 2 (cylindrical coordinates).
1197
    num_groups : int
1198
        The total number of energy groups.
1199
    groupsets : list of dict
1200
        A list of input parameter blocks, each block provides the iterative properties for a
1201
        groupset. Each dictionary supports:
1202
          - groups_from_to: List[int] (required)
1203
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
1204
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
1205
              Handle to an angular quadrature.
1206
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
1207
              Angle aggregation method to use during sweeping.
1208
          - angle_aggregation_num_subsets: int, default=1
1209
              Number of angle subsets used for aggregation.
1210
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
1211
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
1212
              Iterative method used for inner linear solves.
1213
          - l_abs_tol: float, default=1.0e-6
1214
              Inner linear solver absolute residual tolerance.
1215
          - l_max_its: int, default=200
1216
              Inner linear solver maximum iterations.
1217
          - gmres_restart_interval: int, default=30
1218
              GMRES restart interval, if GMRES is used.
1219
          - allow_cycles: bool, default=True
1220
              Whether cyclic dependencies are allowed in sweeps.
1221
          - apply_wgdsa: bool, default=False
1222
              Enable within-group DSA for this groupset.
1223
          - wgdsa_l_abs_tol: float, default=1.0e-4
1224
              WGDSA linear absolute tolerance.
1225
          - wgdsa_l_max_its: int, default=30
1226
              WGDSA maximum iterations.
1227
          - wgdsa_verbose: bool, default=False
1228
              Verbose WGDSA output.
1229
          - wgdsa_petsc_options: str, default=''
1230
              PETSc options string for the WGDSA solver.
1231
          - wgdsa_solver_policy: {'auto', 'direct', 'iterative', 'petsc_options'}, default='auto'
1232
              WGDSA diffusion solver policy. ``auto`` uses a direct PETSc LU solve below
1233
              ``wgdsa_direct_solve_threshold`` global unknowns and an iterative solve
1234
              otherwise. ``petsc_options`` lets ``wgdsa_petsc_options`` control the
1235
              PETSc KSP/PC setup.
1236
          - wgdsa_direct_solve_threshold: int, default=20000
1237
              Maximum global WGDSA diffusion unknown count for the automatic direct solve.
1238
          - apply_tgdsa: bool, default=False
1239
              Enable two-grid DSA for this groupset.
1240
          - tgdsa_l_abs_tol: float, default=1.0e-4
1241
              TGDSA linear absolute tolerance.
1242
          - tgdsa_l_max_its: int, default=30
1243
              TGDSA maximum iterations.
1244
          - tgdsa_verbose: bool, default=False
1245
              Verbose TGDSA output.
1246
          - tgdsa_petsc_options: str, default=''
1247
              PETSc options string for the TGDSA solver.
1248
          - tgdsa_solver_policy: {'auto', 'direct', 'iterative', 'petsc_options'}, default='auto'
1249
              TGDSA diffusion solver policy. ``auto`` uses a direct PETSc LU solve below
1250
              ``tgdsa_direct_solve_threshold`` global unknowns and an iterative solve
1251
              otherwise. ``petsc_options`` lets ``tgdsa_petsc_options`` control the
1252
              PETSc KSP/PC setup.
1253
          - tgdsa_direct_solve_threshold: int, default=20000
1254
              Maximum global TGDSA diffusion unknown count for the automatic direct solve.
1255
    xs_map : list of dict
1256
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
1257
          - block_ids: List[int] (required)
1258
              Mesh block IDs to associate with the cross section.
1259
          - xs: pyopensn.xs.MultiGroupXS (required)
1260
              Cross-section object to assign to the specified blocks.
1261
    boundary_conditions: List[Dict], default=[]
1262
        A list containing tables for each boundary specification. Each dictionary supports:
1263
          - name: str (required)
1264
              Boundary name that identifies the specific boundary.
1265
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
1266
              Boundary type specification.
1267
          - group_strength: List[float], optional
1268
              Required when ``type='isotropic'``. Isotropic strength per group.
1269
          - start_time: float, optional
1270
              Active start time for isotropic boundaries only. Defaults to -infinity.
1271
          - end_time: float, optional
1272
              Active end time for isotropic boundaries only. Defaults to infinity.
1273
          - function: AngularFluxFunction, optional
1274
              Required when ``type='arbitrary'`` unless ``time_function`` is supplied. Callable
1275
              that returns incoming angular flux from group and direction.
1276
          - time_function: AngularFluxTimeFunction, optional
1277
              Required when ``type='arbitrary'`` unless ``function`` is supplied. Callable that
1278
              returns incoming angular flux from group, direction, and time.
1279
        Isotropic boundaries may use ``start_time``/``end_time`` for simple on/off behavior.
1280
        Arbitrary boundaries must specify exactly one of ``function`` or ``time_function``; use
1281
        ``time_function`` for time-dependent arbitrary inflow and handle any active window inside
1282
        that callback.
1283
    point_sources: List[pyopensn.source.PointSource], default=[]
1284
        A list of point sources.
1285
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
1286
        A list of volumetric sources.
1287
    options : dict, optional
1288
        A block of optional configuration parameters applied at problem creation, including:
1289
          - max_mpi_message_size: int, default=32768
1290
          - restart_writes_enabled: bool, default=False
1291
              Enable restart dump writes for solvers that support restart output.
1292
          - write_delayed_psi_to_restart: bool, default=True
1293
              Include delayed sweep angular-flux buffers. Full continuation restarts require
1294
              these buffers whenever the problem has delayed sweep angular state, including
1295
              partitioned parallel, reflected-boundary, and cyclic-sweep cases. These buffers
1296
              are optional when a steady-state restart is used only as a transient initial
1297
              condition because the transient initialization can reconstruct angular state
1298
              from the flux moments.
1299
          - write_angular_flux_to_restart: bool, default=True
1300
              Include stored angular fluxes in restart dumps when ``save_angular_flux=True``.
1301
              This is required for continuing a time-dependent restart, but optional when a
1302
              steady-state restart is used only as a transient initial condition.
1303
          - read_restart_path: str, default=''
1304
              File stem for reading a full restart. The number of MPI ranks and partitioned
1305
              state layout must match the run that wrote the restart files.
1306
          - read_initial_condition_path: str, default=''
1307
              File stem for reading restart data as an initial condition. A steady-state
1308
              restart may be used by ``TransientSolver`` through this option.
1309
          - write_restart_path: str, default=''
1310
              File stem for restart dump writes. OpenSn appends the MPI rank and
1311
              ``.restart.h5`` to this stem.
1312
          - write_restart_time_interval: int, default=0
1313
          - use_precursors: bool, default=True
1314
            Enable delayed-neutron precursor treatment. This remains active across later
1315
            ``SetXSMap`` calls, even if the current map temporarily has no precursor-bearing
1316
            material. When XS maps change, precursor concentrations are remapped by cell and
1317
            precursor-family index; newly introduced families start at zero and removed families
1318
            are discarded.
1319
          - use_source_moments: bool, default=False
1320
          - save_angular_flux: bool, default=False
1321
            Store angular flux state (`psi`) for transient mode, angular-flux
1322
            field functions, and angular-flux I/O.
1323
          - verbose_inner_iterations: bool, default=True
1324
            Print inner iteration details, including WGS and AGS iterations.
1325
          - verbose_outer_iterations: bool, default=True
1326
            Print outer solver progress, including PI/NLKE iterations and transient steps.
1327
          - max_ags_iterations: int, default=100
1328
          - ags_tolerance: float, default=1.0e-6
1329
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
1330
          - power_default_kappa: float, default=3.20435e-11
1331
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
1332
          - field_function_prefix: str, default=''
1333
    sweep_type : str, optional
1334
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
1335
        If ``time_dependent=True``, ``options.save_angular_flux=True`` is required.
1336
    )"
1337
  );
1338
}
966✔
1339

1340
// Wrap steady-state solver
1341
void
1342
WrapSteadyState(py::module& slv)
966✔
1343
{
1344
  const auto BalanceTableToDict = [](const BalanceTable& table)
986✔
1345
  {
1346
    py::dict values;
20✔
1347
    values["absorption_rate"] = table.absorption_rate;
20✔
1348
    values["production_rate"] = table.production_rate;
20✔
1349
    values["inflow_rate"] = table.inflow_rate;
20✔
1350
    values["outflow_rate"] = table.outflow_rate;
20✔
1351
    values["balance"] = table.balance;
20✔
1352
    if (table.initial_inventory.has_value())
20✔
1353
      values["initial_inventory"] = table.initial_inventory.value();
×
1354
    if (table.final_inventory.has_value())
20✔
1355
      values["final_inventory"] = table.final_inventory.value();
×
1356
    if (table.predicted_inventory_change.has_value())
20✔
1357
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1358
    if (table.actual_inventory_change.has_value())
20✔
1359
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1360
    if (table.inventory_residual.has_value())
20✔
1361
      values["inventory_residual"] = table.inventory_residual.value();
×
1362
    return values;
20✔
1363
  };
×
1364

1365
  // clang-format off
1366
  // steady state solver
1367
  auto steady_state_solver = py::class_<SteadyStateSourceSolver, std::shared_ptr<SteadyStateSourceSolver>,
966✔
1368
                                        Solver>(
1369
    slv,
1370
    "SteadyStateSourceSolver",
1371
    R"(
1372
    Steady state solver.
1373

1374
    Wrapper of :cpp:class:`opensn::SteadyStateSourceSolver`.
1375
    )"
1376
  );
966✔
1377
  steady_state_solver.def(
1,932✔
1378
    py::init(
966✔
1379
      [](py::kwargs& params)
689✔
1380
      {
1381
        return SteadyStateSourceSolver::Create(kwargs_to_param_block(params));
689✔
1382
      }
1383
    ),
1384
    R"(
1385
    Construct a steady state solver.
1386

1387
    Parameters
1388
    ----------
1389
    problem : pyopensn.solver.DiscreteOrdinatesProblem
1390
        Existing discrete ordinates problem instance.
1391

1392
    Notes
1393
    -----
1394
    If the problem was constructed with ``options={'read_restart_path': ...}``,
1395
    restart data is read during :meth:`Initialize`. If it was constructed with
1396
    ``options={'restart_writes_enabled': True, ...}``, a restart dump is written
1397
    after :meth:`Execute` completes.
1398

1399
    Restart files are rank-layout specific. A restart written with ``N`` MPI
1400
    ranks must be read with the same rank count and a compatible problem
1401
    definition.
1402
    )"
1403
  );
1404
  steady_state_solver.def(
966✔
1405
    "ComputeBalanceTable",
1406
    [BalanceTableToDict](const SteadyStateSourceSolver& self)
966✔
1407
    {
1408
      return BalanceTableToDict(self.ComputeBalanceTable());
20✔
1409
    },
1410
    R"(
1411
    Compute and return the global balance table using the solver's normalization.
1412
    This is a collective operation and must be called on all ranks.
1413

1414
    Returns
1415
    -------
1416
    dict
1417
        Dictionary with the following entries:
1418

1419
        - ``absorption_rate``:
1420
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1421
          groups and the full domain.
1422
        - ``production_rate``:
1423
          Global volumetric production/source rate used by the solver,
1424
          approximately ``integral Q dV`` summed over groups and the full domain.
1425
        - ``inflow_rate``:
1426
          Global incoming boundary contribution integrated over incoming
1427
          angular flux on boundaries.
1428
        - ``outflow_rate``:
1429
          Global outgoing boundary contribution accumulated from face outflow
1430
          tallies.
1431
        - ``balance``:
1432
          Rate balance,
1433
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1434

1435
    Notes
1436
    -----
1437
    This solver applies no extra normalization to the balance table.
1438
    )"
1439
  );
1440
  // clang-format on
1441
}
966✔
1442

1443
// Wrap transient solver
1444
void
1445
WrapTransient(py::module& slv)
966✔
1446
{
1447
  const auto BalanceTableToDict = [](const BalanceTable& table)
966✔
1448
  {
1449
    py::dict values;
×
1450
    values["absorption_rate"] = table.absorption_rate;
×
1451
    values["production_rate"] = table.production_rate;
×
1452
    values["inflow_rate"] = table.inflow_rate;
×
1453
    values["outflow_rate"] = table.outflow_rate;
×
1454
    values["balance"] = table.balance;
×
1455
    if (table.initial_inventory.has_value())
×
1456
      values["initial_inventory"] = table.initial_inventory.value();
×
1457
    if (table.final_inventory.has_value())
×
1458
      values["final_inventory"] = table.final_inventory.value();
×
1459
    if (table.predicted_inventory_change.has_value())
×
1460
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1461
    if (table.actual_inventory_change.has_value())
×
1462
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1463
    if (table.inventory_residual.has_value())
×
1464
      values["inventory_residual"] = table.inventory_residual.value();
×
1465
    return values;
×
1466
  };
×
1467
  // clang-format off
1468
  auto transient_solver =
966✔
1469
    py::class_<TransientSolver, std::shared_ptr<TransientSolver>, Solver>(
1470
      slv,
1471
      "TransientSolver",
1472
      R"(
1473
      Transient solver.
1474

1475
      Wrapper of :cpp:class:`opensn::TransientSolver`.
1476
      )"
1477
    );
966✔
1478
  transient_solver.def(
1,932✔
1479
    py::init(
966✔
1480
      [](py::kwargs& params)
460✔
1481
      {
1482
        return TransientSolver::Create(kwargs_to_param_block(params));
460✔
1483
      }
1484
    ),
1485
    R"(
1486
    Construct a transient solver.
1487

1488
    Parameters
1489
    ----------
1490
    pyopensn.solver.DiscreteOrdinatesProblem : DiscreteOrdinatesProblem
1491
        Existing discrete ordinates problem instance.
1492
    dt : float, optional, default=2.0e-3
1493
        Time step size used during the simulation.
1494
    stop_time : float, optional, default=0.1
1495
        Simulation end time.
1496
    theta : float, optional, default=0.5
1497
        Time differencing scheme parameter.
1498
    initial_state : str, optional, default="existing"
1499
        Initial state for the transient solve. Allowed values: existing, zero.
1500
        In "zero" mode, scalar flux vectors are reset to zero.
1501
    verbose : bool, optional, default=True
1502
        Enable verbose logging.
1503

1504
    Notes
1505
    -----
1506
    The associated problem must have ``save_angular_flux=True`` enabled. This
1507
    is required for transient problems.
1508

1509
    If the problem was constructed with ``options={'read_restart_path': ...}``,
1510
    restart data is read during :meth:`Initialize`. If it was constructed with
1511
    ``options={'read_initial_condition_path': ...}``, restart data is read as a
1512
    transient initial condition during :meth:`Initialize`, then the problem is
1513
    switched to time-dependent mode; a steady-state restart may be used in this
1514
    mode. In this initial-condition path, stored angular fluxes and delayed
1515
    sweep angular-flux buffers are optional in the steady-state restart; the
1516
    transient solver reconstructs angular state from the loaded flux moments.
1517
    If it was constructed with
1518
    ``options={'restart_writes_enabled': True, ...}``, timed restart dumps may
1519
    be written during :meth:`Execute` and a final restart dump is written when
1520
    execution completes.
1521

1522
    A full transient continuation restart read with ``read_restart_path`` is
1523
    different from a steady-state initial-condition restart read with
1524
    ``read_initial_condition_path``. Full transient continuation restarts require
1525
    angular flux state in the restart file; if the problem has delayed sweep
1526
    angular state, including partitioned parallel, reflected-boundary, or
1527
    cyclic-sweep cases, continuation also requires delayed sweep angular-flux
1528
    buffers in the restart. Restart files are rank-layout specific: use the same MPI rank
1529
    count and compatible problem definition when reading files written by a
1530
    previous run.
1531
    )"
1532
  );
1533
  transient_solver.def(
966✔
1534
    "SetTimeStep",
1535
    &TransientSolver::SetTimeStep,
966✔
1536
    R"(
1537
    Set the timestep size used by :meth:`Advance`.
1538

1539
    Parameters
1540
    ----------
1541
    dt : float
1542
        New timestep size.
1543
    )");
1544
  transient_solver.def(
966✔
1545
    "SetTheta",
1546
    &TransientSolver::SetTheta,
966✔
1547
    R"(
1548
    Set the theta parameter used by :meth:`Advance`.
1549

1550
    Parameters
1551
    ----------
1552
    theta : float
1553
        Theta value between 1.0e-16 and 1.
1554
    )");
1555
  transient_solver.def(
966✔
1556
    "Advance",
1557
    &TransientSolver::Advance,
966✔
1558
    R"(
1559
    Advance the solver by a single timestep.
1560

1561
    Notes
1562
    -----
1563
    You must call :meth:`Initialize` before calling :meth:`Advance` or
1564
    :meth:`Execute`.
1565
    )");
1566
  transient_solver.def(
966✔
1567
    "SetPreAdvanceCallback",
1568
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
966✔
1569
      &TransientSolver::SetPreAdvanceCallback),
1570
    R"(
1571
    Register a callback that runs before each advance within :meth:`Execute`.
1572

1573
    Parameters
1574
    ----------
1575
    callback : Optional[Callable[[], None]]
1576
        Function invoked before the solver advances a timestep. Pass None to clear.
1577
        If the callback modifies the timestep, the new value is used for the
1578
        upcoming step.
1579
    )");
1580
  transient_solver.def(
966✔
1581
    "SetPreAdvanceCallback",
1582
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
966✔
1583
      &TransientSolver::SetPreAdvanceCallback),
1584
    "Clear the PreAdvance callback by passing None.");
1585
  transient_solver.def(
966✔
1586
    "SetPostAdvanceCallback",
1587
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
966✔
1588
      &TransientSolver::SetPostAdvanceCallback),
1589
    R"(
1590
    Register a callback that runs after each advance within :meth:`Execute`.
1591

1592
    Parameters
1593
    ----------
1594
    callback : Optional[Callable[[], None]]
1595
        Function invoked after the solver advances a timestep. Pass None to clear.
1596
    )");
1597
  transient_solver.def(
966✔
1598
    "SetPostAdvanceCallback",
1599
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
966✔
1600
      &TransientSolver::SetPostAdvanceCallback),
1601
    "Clear the PostAdvance callback by passing None.");
1602
  transient_solver.def(
966✔
1603
    "ComputeBalanceTable",
1604
    [BalanceTableToDict](const TransientSolver& self)
966✔
1605
    {
1606
      return BalanceTableToDict(self.ComputeBalanceTable());
×
1607
    },
1608
    R"(
1609
    Compute and return the global balance table using the solver's normalization.
1610
    This is a collective operation and must be called on all ranks.
1611

1612
    Returns
1613
    -------
1614
    dict
1615
        Dictionary with the following entries:
1616

1617
        - ``absorption_rate``:
1618
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1619
          groups and the full domain.
1620
        - ``production_rate``:
1621
          Global volumetric production/source rate used by the solver,
1622
          approximately ``integral Q dV`` summed over groups and the full domain.
1623
        - ``inflow_rate``:
1624
          Global incoming boundary contribution integrated over incoming
1625
          angular flux on boundaries.
1626
        - ``outflow_rate``:
1627
          Global outgoing boundary contribution accumulated from face outflow
1628
          tallies.
1629
        - ``balance``:
1630
          Rate balance,
1631
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1632
        - ``initial_inventory``:
1633
          Total particle inventory at the start of the timestep, computed as
1634
          ``integral (1 / v_g) * phi_old dV`` summed over groups and the full domain.
1635
        - ``final_inventory``:
1636
          Total particle inventory at the end of the timestep, computed as
1637
          ``integral (1 / v_g) * phi_new dV`` summed over groups and the full domain.
1638
        - ``predicted_inventory_change``:
1639
          Inventory change predicted by the current timestep balance, computed as
1640
          ``dt * balance``.
1641
        - ``actual_inventory_change``:
1642
          Measured change in total particle inventory over the timestep, computed as
1643
          ``final_inventory - initial_inventory``.
1644
        - ``inventory_residual``:
1645
          Mismatch between the measured and predicted timestep inventory
1646
          changes, computed as
1647
          ``actual_inventory_change - predicted_inventory_change``.
1648

1649
    Notes
1650
    -----
1651
    This solver applies no extra normalization to the balance table.
1652

1653
    The transient inventory terms currently use the end-of-step rate balance to
1654
    estimate the timestep inventory change.
1655
    )"
1656
  );
1657
  slv.attr("BackwardEuler") = 1.0;
966✔
1658
  slv.attr("CrankNicolson") = 0.5;
1,932✔
1659
  // clang-format on
1660
}
966✔
1661

1662
// Wrap non-linear k-eigen solver
1663
void
1664
WrapNLKEigen(py::module& slv)
966✔
1665
{
1666
  const auto BalanceTableToDict = [](const BalanceTable& table)
966✔
1667
  {
1668
    py::dict values;
×
1669
    values["absorption_rate"] = table.absorption_rate;
×
1670
    values["production_rate"] = table.production_rate;
×
1671
    values["inflow_rate"] = table.inflow_rate;
×
1672
    values["outflow_rate"] = table.outflow_rate;
×
1673
    values["balance"] = table.balance;
×
1674
    if (table.initial_inventory.has_value())
×
1675
      values["initial_inventory"] = table.initial_inventory.value();
×
1676
    if (table.final_inventory.has_value())
×
1677
      values["final_inventory"] = table.final_inventory.value();
×
1678
    if (table.predicted_inventory_change.has_value())
×
1679
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1680
    if (table.actual_inventory_change.has_value())
×
1681
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1682
    if (table.inventory_residual.has_value())
×
1683
      values["inventory_residual"] = table.inventory_residual.value();
×
1684
    return values;
×
1685
  };
×
1686
  // clang-format off
1687
  // non-linear k-eigen solver
1688
  auto non_linear_k_eigen_solver = py::class_<NonLinearKEigenSolver, std::shared_ptr<NonLinearKEigenSolver>,
966✔
1689
                                              Solver>(
1690
    slv,
1691
    "NonLinearKEigenSolver",
1692
    R"(
1693
    Non-linear k-eigenvalue solver.
1694

1695
    Wrapper of :cpp:class:`opensn::NonLinearKEigenSolver`.
1696

1697
    Supports one or more groupsets when the groupsets run without WGDSA/TGDSA.
1698
    If WGDSA or TGDSA is enabled on any groupset, the nonlinear k-eigen solve
1699
    must use a single groupset.
1700
    )"
1701
  );
966✔
1702
  non_linear_k_eigen_solver.def(
1,932✔
1703
    py::init(
966✔
1704
      [](py::kwargs& params)
45✔
1705
      {
1706
        return NonLinearKEigenSolver::Create(kwargs_to_param_block(params));
45✔
1707
      }
1708
        ),
1709
    R"(
1710
    Construct a non-linear k-eigenvalue solver.
1711

1712
    Parameters
1713
    ----------
1714
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1715
        Existing discrete ordinates problem instance.
1716
        Multiple groupsets are supported when groupset WGDSA/TGDSA is disabled.
1717
        If WGDSA or TGDSA is enabled on any groupset, the nonlinear k-eigen
1718
        solve must use a single groupset.
1719
    nl_abs_tol: float, default=1.0e-8
1720
        Non-linear absolute tolerance.
1721
    nl_rel_tol: float, default=1.0e-8
1722
        Non-linear relative tolerance.
1723
    nl_sol_tol: float, default=1.0e-50
1724
        Non-linear solution tolerance.
1725
    nl_max_its: int, default=50
1726
        Non-linear algorithm maximum iterations.
1727
    l_abs_tol: float, default=1.0e-8
1728
        Linear absolute tolerance.
1729
    l_rel_tol: float, default=1.0e-8
1730
        Linear relative tolerance.
1731
    l_div_tol: float, default=1.0e6
1732
        Linear divergence tolerance.
1733
    l_max_its: int, default=50
1734
        Linear algorithm maximum iterations.
1735
    l_gmres_restart_intvl: int, default=30
1736
        GMRES restart interval.
1737
    l_gmres_breakdown_tol: float, default=0.1
1738
        GMRES breakdown tolerance.
1739
    reset_phi0: bool, default=True
1740
        If true, reinitializes scalar fluxes to 1.0.
1741
    num_initial_power_iterations: int, default=0
1742
        Number of initial power iterations before the non-linear solve.
1743

1744
    Notes
1745
    -----
1746
    PETSc convergence failures and iteration limits are reported through the
1747
    solver status and log output, consistent with the other transport solvers.
1748
    Invalid residual states are reported to PETSc as function-domain
1749
    errors so SNES can backtrack or terminate with the appropriate reason.
1750
    )"
1751
  );
1752
  non_linear_k_eigen_solver.def(
966✔
1753
    "GetEigenvalue",
1754
    &NonLinearKEigenSolver::GetEigenvalue,
966✔
1755
    R"(
1756
    Return the current k-eigenvalue.
1757
    )"
1758
  );
1759
  non_linear_k_eigen_solver.def(
966✔
1760
    "ComputeBalanceTable",
1761
    [BalanceTableToDict](const NonLinearKEigenSolver& self)
966✔
1762
    {
1763
      return BalanceTableToDict(self.ComputeBalanceTable());
×
1764
    },
1765
    R"(
1766
    Compute and return the global balance table using the solver's normalization.
1767
    This is a collective operation and must be called on all ranks.
1768

1769
    Returns
1770
    -------
1771
    dict
1772
        Dictionary with the following entries:
1773

1774
        - ``absorption_rate``:
1775
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1776
          groups and the full domain.
1777
        - ``production_rate``:
1778
          Global volumetric production/source rate used by the solver,
1779
          approximately ``integral Q dV`` summed over groups and the full domain.
1780
        - ``inflow_rate``:
1781
          Global incoming boundary contribution integrated over incoming
1782
          angular flux on boundaries.
1783
        - ``outflow_rate``:
1784
          Global outgoing boundary contribution accumulated from face outflow
1785
          tallies.
1786
        - ``balance``:
1787
          Rate balance,
1788
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1789

1790
    Notes
1791
    -----
1792
    For k-eigenvalue balance reporting, this solver scales the production term by
1793
    ``1 / k_eff`` before forming both ``production_rate`` and ``balance``.
1794
    )"
1795
  );
1796
  // clang-format on
1797
}
966✔
1798

1799
// Wrap power iteration solvers
1800
void
1801
WrapPIteration(py::module& slv)
966✔
1802
{
1803
  const auto BalanceTableToDict = [](const BalanceTable& table)
978✔
1804
  {
1805
    py::dict values;
12✔
1806
    values["absorption_rate"] = table.absorption_rate;
12✔
1807
    values["production_rate"] = table.production_rate;
12✔
1808
    values["inflow_rate"] = table.inflow_rate;
12✔
1809
    values["outflow_rate"] = table.outflow_rate;
12✔
1810
    values["balance"] = table.balance;
12✔
1811
    if (table.initial_inventory.has_value())
12✔
1812
      values["initial_inventory"] = table.initial_inventory.value();
×
1813
    if (table.final_inventory.has_value())
12✔
1814
      values["final_inventory"] = table.final_inventory.value();
×
1815
    if (table.predicted_inventory_change.has_value())
12✔
1816
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1817
    if (table.actual_inventory_change.has_value())
12✔
1818
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1819
    if (table.inventory_residual.has_value())
12✔
1820
      values["inventory_residual"] = table.inventory_residual.value();
×
1821
    return values;
12✔
1822
  };
×
1823
  // clang-format off
1824
  // power iteration k-eigen solver
1825
  auto pi_k_eigen_solver = py::class_<PowerIterationKEigenSolver, std::shared_ptr<PowerIterationKEigenSolver>,
966✔
1826
                                      Solver>(
1827
    slv,
1828
    "PowerIterationKEigenSolver",
1829
    R"(
1830
    Power iteration k-eigenvalue solver.
1831

1832
    Wrapper of :cpp:class:`opensn::PowerIterationKEigenSolver`.
1833
    )"
1834
  );
966✔
1835
  pi_k_eigen_solver.def(
1,932✔
1836
    py::init(
966✔
1837
      [](py::kwargs& params)
398✔
1838
      {
1839
        return PowerIterationKEigenSolver::Create(kwargs_to_param_block(params));
398✔
1840
      }
1841
    ),
1842
    R"(
1843
    Construct a power iteration k-eigen solver.
1844

1845
    Parameters
1846
    ----------
1847
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1848
        Existing DiscreteOrdinatesProblem instance.
1849
    acceleration: pyopensn.solver.DiscreteOrdinatesKEigenAcceleration
1850
        Optional DiscreteOrdinatesKEigenAcceleration instance for acceleration.
1851
    max_iters: int, default = 1000
1852
        Maximum power iterations allowed.
1853
    k_tol: float, default = 1.0e-10
1854
        Tolerance on the k-eigenvalue.
1855
    reset_solution: bool, default=True
1856
        If true, initialize flux moments to 1.0.
1857
    reset_phi0: bool, default=True
1858
        If true, reinitializes scalar fluxes to 1.0.
1859

1860
    Notes
1861
    -----
1862
    If the problem was constructed with ``options={'read_restart_path': ...}``,
1863
    restart data is read during :meth:`Initialize`. If it was constructed with
1864
    ``options={'restart_writes_enabled': True, ...}``, timed restart dumps may
1865
    be written during the outer iteration loop and a final restart dump is
1866
    written when execution completes.
1867
    )"
1868
  );
1869
  pi_k_eigen_solver.def(
966✔
1870
    "GetEigenvalue",
1871
    &PowerIterationKEigenSolver::GetEigenvalue,
966✔
1872
    R"(
1873
    Return the current k-eigenvalue.
1874
    )"
1875
  );
1876
  pi_k_eigen_solver.def(
966✔
1877
    "GetNumPowerIterations",
1878
    &PowerIterationKEigenSolver::GetNumPowerIterations,
966✔
1879
    R"(
1880
    Return the number of completed power iterations.
1881
    )"
1882
  );
1883
  pi_k_eigen_solver.def(
966✔
1884
    "GetNumSweeps",
1885
    &PowerIterationKEigenSolver::GetNumSweeps,
966✔
1886
    R"(
1887
    Return the total number of transport sweeps applied by all WGS solvers.
1888
    )"
1889
  );
1890
  pi_k_eigen_solver.def(
966✔
1891
    "ComputeBalanceTable",
1892
    [BalanceTableToDict](const PowerIterationKEigenSolver& self)
966✔
1893
    {
1894
      return BalanceTableToDict(self.ComputeBalanceTable());
12✔
1895
    },
1896
    R"(
1897
    Compute and return the global balance table using the solver's normalization.
1898
    This is a collective operation and must be called on all ranks.
1899

1900
    Returns
1901
    -------
1902
    dict
1903
        Dictionary with the following entries:
1904

1905
        - ``absorption_rate``:
1906
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1907
          groups and the full domain.
1908
        - ``production_rate``:
1909
          Global volumetric production/source rate used by the solver,
1910
          approximately ``integral Q dV`` summed over groups and the full domain.
1911
        - ``inflow_rate``:
1912
          Global incoming boundary contribution integrated over incoming
1913
          angular flux on boundaries.
1914
        - ``outflow_rate``:
1915
          Global outgoing boundary contribution accumulated from face outflow
1916
          tallies.
1917
        - ``balance``:
1918
          Rate balance,
1919
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1920

1921
    Notes
1922
    -----
1923
    For k-eigenvalue balance reporting, this solver scales the production term by
1924
    ``1 / k_eff`` before forming both ``production_rate`` and ``balance``.
1925
    )"
1926
  );
1927
  // clang-format on
1928
}
966✔
1929

1930
// Wrap LBS solver
1931
void
1932
WrapDiscreteOrdinatesKEigenAcceleration(py::module& slv)
966✔
1933
{
1934
  // clang-format off
1935
  // discrete ordinates k-eigen acceleration base
1936
  auto acceleration = py::class_<DiscreteOrdinatesKEigenAcceleration,
966✔
1937
                                 std::shared_ptr<DiscreteOrdinatesKEigenAcceleration>>(
1938
    slv,
1939
    "DiscreteOrdinatesKEigenAcceleration",
1940
    R"(
1941
    Base class for discrete ordinates k-eigenvalue acceleration methods.
1942

1943
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesKEigenAcceleration`.
1944
    )"
1945
  );
966✔
1946
  // SCDSA acceleration
1947
  auto scdsa_acceleration = py::class_<SCDSAAcceleration,
966✔
1948
                                       std::shared_ptr<SCDSAAcceleration>,
1949
                                       DiscreteOrdinatesKEigenAcceleration>(
1950
    slv,
1951
    "SCDSAAcceleration",
1952
    R"(
1953
    Construct an SCDSA accelerator for the power iteration k-eigenvalue solver.
1954

1955
    Wrapper of :cpp:class:`opensn::SCDSAAcceleration`.
1956
    )"
1957
  );
966✔
1958
  scdsa_acceleration.def(
1,932✔
1959
    py::init(
966✔
1960
      [](py::kwargs& params)
8✔
1961
      {
1962
        return SCDSAAcceleration::Create(kwargs_to_param_block(params));
8✔
1963
      }
1964
    ),
1965
    R"(
1966
    SCDSA acceleration for the power iteration k-eigenvalue solver.
1967

1968
    Parameters
1969
    ----------
1970
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1971
        Existing DiscreteOrdinatesProblem instance.
1972
    l_abs_tol: float, default=1.0e-10
1973
        Absolute residual tolerance.
1974
    max_iters: int, default=100
1975
        Maximum allowable iterations.
1976
    verbose: bool, default=False
1977
        If true, enables verbose output.
1978
    petsc_options: str, default="ssss"
1979
        Additional PETSc options.
1980
    pi_max_its: int, default=50
1981
        Maximum allowable iterations for inner power iterations.
1982
    pi_k_tol: float, default=1.0e-10
1983
        k-eigenvalue tolerance for the inner power iterations.
1984
    sdm: str, default="pwld"
1985
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1986
            - 'pwld' : Piecewise Linear Discontinuous
1987
            - 'pwlc' : Piecewise Linear Continuous
1988
    )"
1989
  );
1990
  // SMM acceleration
1991
  auto smm_acceleration = py::class_<SMMAcceleration,
966✔
1992
                                     std::shared_ptr<SMMAcceleration>,
1993
                                     DiscreteOrdinatesKEigenAcceleration>(
1994
    slv,
1995
    "SMMAcceleration",
1996
    R"(
1997
    Construct an SMM accelerator for the power iteration k-eigenvalue solver.
1998

1999
    Wrapper of :cpp:class:`opensn::SMMAcceleration`.
2000
    )"
2001
  );
966✔
2002
  smm_acceleration.def(
1,932✔
2003
    py::init(
966✔
2004
      [](py::kwargs& params)
4✔
2005
      {
2006
        return SMMAcceleration::Create(kwargs_to_param_block(params));
4✔
2007
      }
2008
    ),
2009
    R"(
2010
    SMM acceleration for the power iteration k-eigenvalue solver.
2011

2012
    Warnings
2013
    --------
2014
       SMM acceleration is **experimental** and should be used with caution!
2015
       SMM acceleration only supports problems with isotropic scattering.
2016

2017
    Parameters
2018
    ----------
2019
    problem: pyopensn.solver.DiscreteOrdinatesProblem
2020
        Existing DiscreteOrdinatesProblem instance.
2021
    l_abs_tol: float, default=1.0e-10
2022
        Absolute residual tolerance.
2023
    max_iters: int, default=100
2024
        Maximum allowable iterations.
2025
    verbose: bool, default=False
2026
        If true, enables verbose output.
2027
    petsc_options: str, default="ssss"
2028
        Additional PETSc options.
2029
    pi_max_its: int, default=50
2030
        Maximum allowable iterations for inner power iterations.
2031
    pi_k_tol: float, default=1.0e-10
2032
        k-eigenvalue tolerance for the inner power iterations.
2033
    sdm: str, default="pwld"
2034
        Spatial discretization method to use for the diffusion solver. Valid choices are:
2035
            - 'pwld' : Piecewise Linear Discontinuous
2036
            - 'pwlc' : Piecewise Linear Continuous
2037
    )"
2038
  );
2039
  // CMFD acceleration
2040
  auto cmfd_acceleration = py::class_<CMFDAcceleration,
966✔
2041
                                      std::shared_ptr<CMFDAcceleration>,
2042
                                      DiscreteOrdinatesKEigenAcceleration>(
2043
    slv,
2044
    "CMFDAcceleration",
2045
    R"(
2046
    Construct a CMFD accelerator for the power iteration k-eigenvalue solver.
2047

2048
    Wrapper of :cpp:class:`opensn::CMFDAcceleration`.
2049
    )"
2050
  );
966✔
2051
  cmfd_acceleration.def(
1,932✔
2052
    py::init(
966✔
2053
      [](py::kwargs& params)
86✔
2054
      {
2055
        return CMFDAcceleration::Create(kwargs_to_param_block(params));
86✔
2056
      }
2057
    ),
2058
    R"(
2059
    CMFD acceleration for the power iteration k-eigenvalue solver.
2060

2061
    With CMFD, each power iteration performs a configured number of high-order WGS
2062
    transport update iterations, then applies a bounded low-order scalar-flux correction.
2063
    The defaults use one WGS update iteration, automatic current closure, fixed
2064
    correction relaxation, and a transport-current balance gate before outer power
2065
    iteration is allowed to converge.
2066

2067
    Most users should tune only the common controls: ``coarse_mesh``, ``current_closure``,
2068
    ``aggregation_size``, ``group_aggregation_size``, ``relaxation``,
2069
    ``update_wgs_max_its``, ``update_wgs_abs_tol``, and
2070
    ``balance_residual_tolerance``. Parameters marked "developer/debug" are exposed for
2071
    investigations and regression testing, but normally should be left at their defaults.
2072

2073
    Parameters
2074
    ----------
2075
    problem: pyopensn.solver.DiscreteOrdinatesProblem
2076
        Existing DiscreteOrdinatesProblem instance.
2077
    coarse_mesh: str, default="local_aggregation"
2078
        Common option. Coarse-mesh construction method. Valid choices are:
2079
            - 'identity' : one CMFD coarse cell per transport cell
2080
            - 'local_aggregation' : connected same-block fine cells aggregated locally
2081
            - 'global_aggregation' : connected same-block fine cells aggregated across MPI ranks
2082
        ``"local_aggregation"`` is the conservative default. ``"global_aggregation"`` can
2083
        reduce the coarse problem size on larger distributed meshes. Global aggregation is a
2084
        logical CMFD coarse-space construction: it does not repartition the transport mesh or
2085
        change fine-cell ownership. Each global coarse cell has one owning rank, while ranks
2086
        owning member fine cells keep membership records for restriction and prolongation.
2087
        Aggregates are connected by face adjacency, remain within one mesh block, and may be
2088
        smaller than ``aggregation_size`` near boundaries or disconnected regions. ``"identity"``
2089
        is primarily for debugging and method comparisons.
2090
    current_closure: str, default="auto"
2091
        Common option. CMFD face-current closure. Valid choices are:
2092
            - 'auto' : choose net, partial, or a blend from early coarse-balance behavior
2093
            - 'net' : match the signed transport current across each coarse face
2094
            - 'partial' : build face coupling from outgoing partial currents on both sides
2095
        ``"auto"`` is recommended for production use. A fixed closure is useful when
2096
        comparing methods, reproducing a benchmark setting, or diagnosing a case where
2097
        automatic selection is not robust.
2098
    aggregation_size: int, default=32
2099
        Common option. Target number of fine cells per aggregated coarse cell for
2100
        ``coarse_mesh="local_aggregation"`` or ``coarse_mesh="global_aggregation"``.
2101
        Larger values reduce CMFD matrix size and
2102
        setup/solve cost, but make the spatial correction less detailed. Smaller values
2103
        increase the coarse-system cost but may improve robustness.
2104
    group_aggregation_size: int, default=1
2105
        Common option. Number of transport energy groups per CMFD coarse group, not the
2106
        final number of coarse groups. A value of 1 preserves the full transport group
2107
        structure in the low-order system. To target ``N`` total coarse groups, use
2108
        ``(num_groups + N - 1) // N``.
2109
    relaxation: float, default=0.5
2110
        Common option. Relaxation factor applied to the CMFD scalar-flux correction.
2111
        This is the requested correction strength. The correction limiter may damp or
2112
        skip an individual correction if the requested update would produce an invalid
2113
        k-eigenvalue, non-finite flux, or excessive negative scalar flux.
2114
    update_wgs_max_its: int, default=1
2115
        Common option. Maximum WGS iterations used before each CMFD correction. The
2116
        default performs one transport update iteration per power iteration; larger
2117
        values make each transport update more accurate but more expensive.
2118
    update_wgs_abs_tol: float, default=1.0e-12
2119
        Common option. WGS absolute tolerance used before each CMFD correction. When
2120
        ``update_wgs_max_its=1``, this tolerance is normally not the stopping criterion
2121
        because only one WGS iteration is allowed. It matters when more WGS iterations
2122
        are allowed.
2123
    balance_residual_tolerance: float, default=1.0e-6
2124
        Common option. Restricted transport-current balance residual tolerance required
2125
        before CMFD permits outer power-iteration convergence. This prevents false
2126
        convergence when ``k_eff_change`` is small but the CMFD-restricted transport
2127
        balance is still inconsistent. This is a consistency guard, not an estimate of
2128
        the k-eigenvalue error. For large 3D cases it may need to be looser than the
2129
        outer ``k_tol``; choose it tight enough to prevent false convergence but not so
2130
        tight that it forces unnecessary asymptotic balance iterations.
2131
    l_abs_tol: float, default=1.0e-7
2132
        Developer/debug. Absolute residual tolerance for each CMFD coarse linear solve.
2133
        This affects the low-order linear solve, not the outer transport convergence.
2134
    max_iters: int, default=100
2135
        Developer/debug. Maximum iterations for each CMFD coarse linear solve. Increase
2136
        only if the coarse KSP solve is reaching its iteration limit. CMFD skips a
2137
        correction when any coarse linear solve fails to converge within this limit. If
2138
        the log reports ``correction = skipped (coarse_linear_solve_not_converged)``,
2139
        use a direct coarse solve for modest coarse systems, increase this limit for
2140
        iterative solves, or provide stronger ``petsc_options``.
2141
    verbose: bool, default=False
2142
        Developer/debug. If true, prints CMFD diagnostic and timing metrics. These are
2143
        useful for regression tests and performance studies but can be noisy in production.
2144
    petsc_options: str, default=""
2145
        Developer/debug. Additional PETSc options for the CMFD coarse solver. Used only
2146
        for solver experiments, especially with ``coarse_solver_policy="petsc_options"``.
2147
    pi_max_its: int, default=50
2148
        Developer/debug. Maximum inner power iterations for the CMFD coarse k solve.
2149
        Increase only if the coarse k solve is not converging enough to give useful
2150
        corrections.
2151
    pi_k_tol: float, default=1.0e-8
2152
        Developer/debug. k-eigenvalue tolerance for CMFD coarse power iterations. This
2153
        is separate from the outer ``PowerIterationKEigenSolver`` k tolerance.
2154
    correction_max_attempts: int, default=10
2155
        Developer/debug. Maximum CMFD correction damping attempts before skipping the
2156
        correction for the current transport update. Each failed attempt halves the
2157
        damping. If the log reports ``correction = skipped (negative_flux_guard)``,
2158
        first reduce ``relaxation`` or use finer spatial/energy aggregation; changing
2159
        this option is mainly for diagnostics.
2160
    correction_min_damping: float, default=1.0e-4
2161
        Developer/debug. Minimum CMFD correction damping factor considered during correction
2162
        limiting. If no admissible correction is found above this damping, CMFD skips
2163
        the correction for that transport update.
2164
    negative_flux_tolerance: float, default=1.0e-6
2165
        Developer/debug. Allowed scalar-flux undershoot for accepting a CMFD correction.
2166
        Corrections producing scalar flux below this guard are damped or skipped. Raising
2167
        this value can hide unstable corrections and should be accompanied by final-k and
2168
        scalar-flux checks.
2169
    inactive_iterations: int, default=0
2170
        Developer/debug. Number of initial power iterations before applying CMFD
2171
        corrections. Transport update controls are still used during these
2172
        iterations.
2173
    coarse_solver_policy: str, default="auto"
2174
        Developer/debug. Coarse solver policy. Valid choices are:
2175
            - 'auto' : PETSc preonly+LU below ``coarse_direct_solve_threshold``
2176
              global unknowns, GMRES+Jacobi otherwise
2177
            - 'direct' : PETSc preonly+LU
2178
            - 'iterative' : GMRES+Jacobi
2179
            - 'petsc_options' : allow ``petsc_options`` to override the PETSc KSP/PC setup
2180
        CMFD corrections from unconverged coarse linear solves are always skipped.
2181
        The skipped correction uses the unaccelerated transport update for that power
2182
        iteration; after repeated skipped corrections, CMFD returns the raw transport
2183
        k update so that power iteration can continue to move.
2184
    coarse_direct_solve_threshold: int, default=20000
2185
        Developer/debug. Maximum global CMFD unknown count for automatic direct coarse
2186
        solves when ``coarse_solver_policy="auto"``. Larger values make ``"auto"`` use
2187
        direct solves for larger coarse systems; choose values based on available host
2188
        memory and acceptable factorization cost.
2189
    )"
2190
  );
2191
  // clang-format on
2192
}
966✔
2193

2194
// Wrap the solver components of OpenSn
2195
void
2196
py_solver(py::module& pyopensn)
72✔
2197
{
2198
  py::module slv = pyopensn.def_submodule("solver", "Solver module.");
72✔
2199
  WrapProblem(slv);
72✔
2200
  WrapSolver(slv);
72✔
2201
  WrapLBS(slv);
72✔
2202
  WrapSteadyState(slv);
72✔
2203
  WrapTransient(slv);
72✔
2204
  WrapNLKEigen(slv);
72✔
2205
  WrapDiscreteOrdinatesKEigenAcceleration(slv);
72✔
2206
  WrapPIteration(slv);
72✔
2207
}
72✔
2208

2209
} // namespace opensn
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc