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

Open-Sn / opensn / 25093042657

27 Apr 2026 05:34PM UTC coverage: 75.068%. Remained the same
25093042657

push

github

web-flow
Merge pull request #1033 from wdhawkins/iteration_logging

Improvements for iteration logging

364 of 485 new or added lines in 29 files covered. (75.05%)

478 existing lines in 26 files now uncovered.

21700 of 28907 relevant lines covered (75.07%)

65057883.8 hits per line

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

76.86
/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/runtime.h"
7
#include "framework/field_functions/field_function_grid_based.h"
8
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/acceleration/discrete_ordinates_keigen_acceleration.h"
9
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/acceleration/scdsa_acceleration.h"
10
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/acceleration/smm_acceleration.h"
11
#include "modules/linear_boltzmann_solvers/discrete_ordinates_curvilinear_problem/discrete_ordinates_curvilinear_problem.h"
12
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/discrete_ordinates_problem.h"
13
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/io/discrete_ordinates_problem_io.h"
14
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/solvers/transient_solver.h"
15
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/solvers/steady_state_solver.h"
16
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/solvers/nl_keigen_solver.h"
17
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/solvers/pi_keigen_solver.h"
18
#include "modules/linear_boltzmann_solvers/lbs_problem/io/lbs_problem_io.h"
19
#include "modules/linear_boltzmann_solvers/lbs_problem/lbs_problem.h"
20
#include "modules/linear_boltzmann_solvers/lbs_problem/compute/lbs_compute.h"
21
#include "modules/solver.h"
22
#include <pybind11/numpy.h>
23
#include <algorithm>
24
#include <cstddef>
25
#include <cstdint>
26
#include <map>
27
#include <memory>
28
#include <string>
29
#include <vector>
30

31
namespace opensn
32
{
33

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

46
    Wrapper of :cpp:class:`opensn::Problem`.
47
    )"
48
  );
670✔
49
  // clang-format on
50
}
670✔
51

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

244
    Returns
245
    -------
246
    float
247
        The total fission rate.
248

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

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

285
    Returns
286
    -------
287
    float
288
        The total fission production.
289

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

690
    Parameters
691
    ----------
692
    mesh : MeshContinuum
693
        The spatial mesh.
694
    num_groups : int
695
        The total number of energy groups.
696
    groupsets : List[Dict], default=[]
697
        A list of input parameter blocks, each block provides the iterative properties for a
698
        groupset. Each dictionary supports:
699
          - groups_from_to: List[int] (required)
700
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
701
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
702
              Handle to an angular quadrature.
703
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
704
              Angle aggregation method to use during sweeping.
705
          - angle_aggregation_num_subsets: int, default=1
706
              Number of angle subsets used for aggregation.
707
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
708
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
709
              Iterative method used for inner linear solves.
710
          - l_abs_tol: float, default=1.0e-6
711
              Inner linear solver absolute residual tolerance.
712
          - l_max_its: int, default=200
713
              Inner linear solver maximum iterations.
714
          - gmres_restart_interval: int, default=30
715
              GMRES restart interval, if GMRES is used.
716
          - allow_cycles: bool, default=True
717
              Whether cyclic dependencies are allowed in sweeps.
718
          - apply_wgdsa: bool, default=False
719
              Enable within-group DSA for this groupset.
720
          - wgdsa_l_abs_tol: float, default=1.0e-4
721
              WGDSA linear absolute tolerance.
722
          - wgdsa_l_max_its: int, default=30
723
              WGDSA maximum iterations.
724
          - wgdsa_verbose: bool, default=False
725
              Verbose WGDSA output.
726
          - wgdsa_petsc_options: str, default=''
727
              PETSc options string for the WGDSA solver.
728
          - apply_tgdsa: bool, default=False
729
              Enable two-grid DSA for this groupset.
730
          - tgdsa_l_abs_tol: float, default=1.0e-4
731
              TGDSA linear absolute tolerance.
732
          - tgdsa_l_max_its: int, default=30
733
              TGDSA maximum iterations.
734
          - tgdsa_verbose: bool, default=False
735
              Verbose TGDSA output.
736
          - tgdsa_petsc_options: str, default=''
737
              PETSc options string for the TGDSA solver.
738
    xs_map : List[Dict], default=[]
739
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
740
          - block_ids: List[int] (required)
741
              Mesh block IDs to associate with the cross section.
742
          - xs: pyopensn.xs.MultiGroupXS (required)
743
              Cross-section object to assign to the specified blocks.
744
    boundary_conditions: List[Dict], default=[]
745
        A list containing tables for each boundary specification. Each dictionary supports:
746
          - name: str (required)
747
              Boundary name that identifies the specific boundary.
748
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
749
              Boundary type specification.
750
          - group_strength: List[float], optional
751
              Required when ``type='isotropic'``. Isotropic strength per group.
752
          - function: AngularFluxFunction, optional
753
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
754
    point_sources: List[pyopensn.source.PointSource], default=[]
755
        A list of point sources.
756
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
757
        A list of volumetric sources.
758
    options : Dict, default={}
759
        A block of optional configuration parameters, including:
760
          - max_mpi_message_size: int, default=32768
761
          - restart_writes_enabled: bool, default=False
762
          - write_delayed_psi_to_restart: bool, default=True
763
          - read_restart_path: str, default=''
764
          - write_restart_path: str, default=''
765
          - write_restart_time_interval: int, default=0
766
            (must be 0 or >=30)
767
          - use_precursors: bool, default=True
768
            Enable delayed-neutron precursor treatment. This is treated as user intent and remains
769
            active across later ``SetXSMap`` calls, even if the current XS map temporarily contains
770
            no precursor-bearing material. When XS maps are swapped, existing precursor
771
            concentrations are remapped by cell and precursor-family index; new families start at
772
            zero and removed families are discarded. If any fissionable material in the active map
773
            has precursor data and ``use_precursors=True``, all fissionable materials in that map
774
            must have precursor data. Non-fissionable materials may have zero precursors.
775
          - use_source_moments: bool, default=False
776
          - save_angular_flux: bool, default=False
777
            Store angular flux state (`psi`) for transient mode, angular-flux
778
            field functions, and angular-flux I/O.
779
          - adjoint: bool, default=False
780
          - verbose_inner_iterations: bool, default=True
781
            Print inner iteration details, including WGS and AGS iterations.
782
          - verbose_outer_iterations: bool, default=True
783
            Print outer solver progress, including PI/NLKE iterations and transient steps.
784
          - max_ags_iterations: int, default=100
785
          - ags_tolerance: float, default=1.0e-6
786
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
787
          - power_default_kappa: float, default=3.20435e-11
788
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
789
          - field_function_prefix: str, default=''
790
        These options are applied at problem creation.
791
    sweep_type : str, default="AAH"
792
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
793
    time_dependent : bool, default=False
794
        If true, the problem starts in time-dependent mode. Otherwise it starts in
795
        steady-state mode. Requires ``options.save_angular_flux=True``.
796
    use_gpus : bool, default=False
797
        A flag specifying whether GPU acceleration is used for the sweep. Currently, only ``AAH`` is
798
        supported.
799
    )"
800
  );
801
  do_problem.def(
670✔
802
    "SetTimeDependentMode",
803
    &DiscreteOrdinatesProblem::SetTimeDependentMode,
670✔
804
    R"(
805
    Set the problem to time-dependent mode.
806

807
    Notes
808
    -----
809
    Switch problem from steady-state to time-dependent mode. This updates problem
810
    internals (sweep chunk mode and source-function) while preserving user boundary
811
    conditions and fixed sources.
812

813
    Requires ``options.save_angular_flux=True`` at problem creation.
814
    )"
815
  );
816
  do_problem.def(
670✔
817
    "SetSteadyStateMode",
818
    &DiscreteOrdinatesProblem::SetSteadyStateMode,
670✔
819
    R"(
820
    Set the problem to steady-state mode.
821

822
    Notes
823
    -----
824
    Switch problem from time-dependent to steady-state mode. This updates problem
825
    internals (sweep chunk mode and source-function) while preserving user boundary
826
    conditions and fixed sources.
827
    )"
828
  );
829
  do_problem.def(
670✔
830
    "IsTimeDependent",
831
    &DiscreteOrdinatesProblem::IsTimeDependent,
670✔
832
    R"(
833
    Return ``True`` if the problem is currently in time-dependent mode.
834
    )"
835
  );
836
  do_problem.def(
670✔
837
    "SetBoundaryOptions",
838
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
674✔
839
    {
840
      for (auto [key, value] : params)
12✔
841
      {
842
        auto c_key = key.cast<std::string>();
4✔
843
        if (c_key == "clear_boundary_conditions")
4✔
UNCOV
844
          self.ClearBoundaries();
×
845
        else if (c_key == "boundary_conditions")
4✔
846
        {
847
          auto boundaries = value.cast<py::list>();
4✔
848
          for (auto boundary : boundaries)
24✔
849
          {
850
            InputParameters input = DiscreteOrdinatesProblem::GetBoundaryOptionsBlock();
16✔
851
            input.AssignParameters(pyobj_to_param_block("", boundary.cast<py::dict>()));
16✔
852
            self.SetBoundaryOptions(input);
16✔
853
          }
16✔
854
        }
4✔
855
        else
UNCOV
856
          throw std::runtime_error("Invalid argument provided to SetBoundaryOptions.\n");
×
857
      }
4✔
858
    },
4✔
859
    R"(
860
    Set or clear boundary conditions.
861

862
    Parameters
863
    ----------
864
    clear_boundary_conditions: bool, default=False
865
        If true, all current boundary conditions are deleted.
866
    boundary_conditions: List[Dict]
867
        A list of boundary condition dictionaries. Each dictionary supports:
868
          - name: str (required)
869
              Boundary name that identifies the specific boundary.
870
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
871
              Boundary type specification.
872
          - group_strength: List[float], optional
873
              Required when ``type='isotropic'``. Isotropic strength per group.
874
          - function: AngularFluxFunction, optional
875
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
876

877
    Notes
878
    -----
879
    Mode transitions via :meth:`LBSProblem.SetAdjoint` clear all boundary conditions.
880
    Reapply boundaries with this method before solving in the new mode.
881
    )"
882
  );
883
  do_problem.def(
670✔
884
    "ZeroPsi",
885
    [](DiscreteOrdinatesProblem& self)
670✔
886
    {
UNCOV
887
      self.ZeroPsi();
×
888
    },
889
    R"(
890
    Zero the angular-flux storage.
891
    )"
892
  );
893
  do_problem.def(
670✔
894
    "GetPsi",
895
    [](DiscreteOrdinatesProblem& self)
678✔
896
    {
897
      const auto& psi = self.GetPsiNewLocal();
8✔
898
      py::list psi_list;
8✔
899
      for (const auto& vec : psi)
16✔
900
      {
901
        auto array = py::array_t<double>(static_cast<py::ssize_t>(vec.size()),
8✔
902
                                         vec.data(),
903
                                         py::cast(self));
8✔
904
        psi_list.append(array);
8✔
905
      }
8✔
906
      return psi_list;
8✔
UNCOV
907
    },
×
908
    R"(
909
    Return psi as a list of NumPy arrays (float64), using zero-copy views into the
910
    underlying data.
911
    )"
912
  );
913
  do_problem.def(
670✔
914
    "GetAngularFieldFunctionList",
915
    [](DiscreteOrdinatesProblem& self, py::list groups, py::list angles)
670✔
916
    {
UNCOV
917
      std::vector<unsigned int> group_ids;
×
918
      std::vector<size_t> angle_ids;
×
919
      group_ids.reserve(groups.size());
×
920
      angle_ids.reserve(angles.size());
×
921

UNCOV
922
      for (py::handle g : groups)
×
923
        group_ids.push_back(g.cast<unsigned int>());
×
924
      for (py::handle a : angles)
×
925
        angle_ids.push_back(a.cast<size_t>());
×
926

UNCOV
927
      auto ff_list = self.CreateAngularFluxFieldFunctionList(group_ids, angle_ids);
×
928
      py::list out;
×
929
      for (const auto& ff : ff_list)
×
930
        out.append(ff);
×
931
      return out;
×
932
    },
×
933
    R"(
934
    Create field functions for selected angular flux components.
935

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

940
    Example
941
    -------
942
    .. code-block:: python
943

944
       solver.Initialize()
945
       solver.Execute()
946
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
947

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

953
    1) Use ``TransientSolver.Execute()`` with a post-advance callback:
954

955
    .. code-block:: python
956

957
       solver = TransientSolver(problem=phys)
958
       solver.Initialize()
959
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
960

961
       def post_advance():
962
           for ff in ang_ff:
963
               ff.Update()
964
           FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
965

966
       solver.SetPostAdvanceCallback(post_advance)
967
       solver.Execute()
968

969
    2) Use a custom Python loop with ``TransientSolver.Advance()``:
970

971
    .. code-block:: python
972

973
       solver = TransientSolver(problem=phys)
974
       solver.Initialize()
975
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
976
       for _ in range(num_steps):
977
           solver.Advance()
978
           for ff in ang_ff:
979
               ff.Update()
980
           FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
981

982
    Parameters
983
    ----------
984
    groups : List[int]
985
        Global group indices to export.
986
    angles : List[int]
987
        Angle indices within the groupset quadrature for each group.
988

989
    Returns
990
    -------
991
    List[pyopensn.fieldfunc.FieldFunctionGridBased]
992
        Field functions for the requested ``(group, angle)`` pairs. Each returned field function
993
        is a snapshot, but supports ``CanUpdate()`` and ``Update()`` while its owning problem is
994
        alive.
995
    )",
996
    py::arg("groups"),
1,340✔
997
    py::arg("angles")
670✔
998
  );
999
  do_problem.def(
670✔
1000
    "ComputeLeakage",
1001
    [](DiscreteOrdinatesProblem& self, py::list bnd_names)
709✔
1002
    {
1003
      auto grid = self.GetGrid();
39✔
1004
      // get the supported boundaries
1005
      std::map<std::string, std::uint64_t> allowed_bd_names = grid->GetBoundaryNameMap();
39✔
1006
      std::map<std::uint64_t, std::string> allowed_bd_ids = grid->GetBoundaryIDMap();
39✔
1007
      const auto coord_sys = grid->GetCoordinateSystem();
39✔
1008
      const auto mesh_type = grid->GetType();
39✔
1009
      const auto dim = grid->GetDimension();
39✔
1010
      // get the boundaries to parse, preserving user order
1011
      std::vector<std::uint64_t> bndry_ids;
39✔
1012
      if (bnd_names.size() > 1)
39✔
1013
      {
1014
        for (py::handle name : bnd_names)
184✔
1015
        {
1016
          auto sname = name.cast<std::string>();
112✔
1017
          if (coord_sys == CoordinateSystemType::CYLINDRICAL && dim == 2)
112✔
1018
          {
1019
            if (sname == "xmin" || sname == "xmax" || sname == "ymin" || sname == "ymax")
24✔
UNCOV
1020
              throw std::runtime_error("ComputeLeakage: Boundary name '" + sname +
×
1021
                                       "' is invalid for cylindrical orthogonal meshes. "
UNCOV
1022
                                       "Use rmin, rmax, zmin, zmax.");
×
1023

1024
            if (mesh_type == MeshType::ORTHOGONAL)
24✔
1025
            {
1026
              if (sname == "rmin") sname = "xmin";
24✔
1027
              else if (sname == "rmax") sname = "xmax";
24✔
1028
              else if (sname == "zmin") sname = "ymin";
16✔
1029
              else if (sname == "zmax") sname = "ymax";
8✔
1030
            }
1031
          }
1032
          bndry_ids.push_back(allowed_bd_names.at(sname));
112✔
1033
        }
112✔
1034
      }
1035
      else
1036
      {
1037
        bndry_ids = self.GetGrid()->GetUniqueBoundaryIDs();
9✔
1038
      }
1039
      // compute the leakage
1040
      std::map<std::uint64_t, std::vector<double>> leakage = ComputeLeakage(self, bndry_ids);
39✔
1041
      const bool rz_ortho = (coord_sys == CoordinateSystemType::CYLINDRICAL &&
78✔
1042
                             mesh_type == MeshType::ORTHOGONAL && dim == 2);
39✔
1043

1044
      auto to_rz_name = [](const std::string& name)
63✔
1045
      {
1046
        if (name == "xmin") return std::string("rmin");
24✔
1047
        if (name == "xmax") return std::string("rmax");
24✔
1048
        if (name == "ymin") return std::string("zmin");
16✔
1049
        if (name == "ymax") return std::string("zmax");
8✔
UNCOV
1050
        return name;
×
1051
      };
1052

1053
      // convert result to native Python
1054
      py::dict result;
39✔
1055
      for (const auto& bndry_id : bndry_ids)
157✔
1056
      {
1057
        const auto it = leakage.find(bndry_id);
118✔
1058
        if (it == leakage.end())
118✔
UNCOV
1059
          continue;
×
1060
        // construct numpy array and copy contents
1061
        const auto& grp_wise_leakage = it->second;
118✔
1062
        py::array_t<double> np_vector(py::ssize_t(grp_wise_leakage.size()));
118✔
1063
        auto buffer = np_vector.request();
118✔
1064
        auto *np_vector_data = static_cast<double*>(buffer.ptr);
118✔
1065
        std::copy(grp_wise_leakage.begin(), grp_wise_leakage.end(), np_vector_data);
118✔
1066
        std::string name = allowed_bd_ids.at(bndry_id);
118✔
1067
        if (rz_ortho)
118✔
1068
          name = to_rz_name(name);
24✔
1069
        result[py::str(name)] = std::move(np_vector);
236✔
1070
      }
118✔
1071

1072
      return result;
39✔
1073
    },
78✔
1074
    R"(
1075
    Compute leakage for the problem.
1076

1077
    Parameters
1078
    ----------
1079
    bnd_names : List[str]
1080
        A list of boundary names for which leakage should be computed.
1081

1082
    Returns
1083
    -------
1084
    Dict[str, numpy.ndarray]
1085
        A dictionary mapping boundary names to group-wise leakage vectors.
1086
        Each array contains the outgoing angular flux (per group) integrated over
1087
        the corresponding boundary surface.
1088

1089
    Raises
1090
    ------
1091
    RuntimeError
1092
        If `save_angular_flux` option was not enabled during problem setup.
1093

1094
    ValueError
1095
        If one or more boundary ids are not present on the current mesh.
1096
    )",
1097
    py::arg("bnd_names")
670✔
1098
  );
1099

1100
  // discrete ordinates curvilinear problem
1101
  auto do_curvilinear_problem = py::class_<DiscreteOrdinatesCurvilinearProblem,
670✔
1102
                                           std::shared_ptr<DiscreteOrdinatesCurvilinearProblem>,
1103
                                           DiscreteOrdinatesProblem>(
1104
    slv,
1105
    "DiscreteOrdinatesCurvilinearProblem",
1106
    R"(
1107
    Base class for discrete ordinates problems in curvilinear geometry.
1108

1109
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesCurvilinearProblem`.
1110
    )"
1111
  );
670✔
1112
  do_curvilinear_problem.def(
1,340✔
1113
    py::init(
670✔
1114
      [](py::kwargs& params)
80✔
1115
      {
1116
        return DiscreteOrdinatesCurvilinearProblem::Create(kwargs_to_param_block(params));
80✔
1117
      }
1118
    ),
1119
    R"(
1120
    Construct a discrete ordinates problem for curvilinear geometry.
1121

1122
    Warnings
1123
    --------
1124
       DiscreteOrdinatesCurvilinearProblem is **experimental** and should be used with caution!
1125

1126
    Parameters
1127
    ----------
1128
    mesh : MeshContinuum
1129
        The spatial mesh.
1130
    coord_system : int
1131
        Coordinate system to use. Must be set to 2 (cylindrical coordinates).
1132
    num_groups : int
1133
        The total number of energy groups.
1134
    groupsets : list of dict
1135
        A list of input parameter blocks, each block provides the iterative properties for a
1136
        groupset. Each dictionary supports:
1137
          - groups_from_to: List[int] (required)
1138
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
1139
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
1140
              Handle to an angular quadrature.
1141
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
1142
              Angle aggregation method to use during sweeping.
1143
          - angle_aggregation_num_subsets: int, default=1
1144
              Number of angle subsets used for aggregation.
1145
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
1146
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
1147
              Iterative method used for inner linear solves.
1148
          - l_abs_tol: float, default=1.0e-6
1149
              Inner linear solver absolute residual tolerance.
1150
          - l_max_its: int, default=200
1151
              Inner linear solver maximum iterations.
1152
          - gmres_restart_interval: int, default=30
1153
              GMRES restart interval, if GMRES is used.
1154
          - allow_cycles: bool, default=True
1155
              Whether cyclic dependencies are allowed in sweeps.
1156
          - apply_wgdsa: bool, default=False
1157
              Enable within-group DSA for this groupset.
1158
          - wgdsa_l_abs_tol: float, default=1.0e-4
1159
              WGDSA linear absolute tolerance.
1160
          - wgdsa_l_max_its: int, default=30
1161
              WGDSA maximum iterations.
1162
          - wgdsa_verbose: bool, default=False
1163
              Verbose WGDSA output.
1164
          - wgdsa_petsc_options: str, default=''
1165
              PETSc options string for the WGDSA solver.
1166
          - apply_tgdsa: bool, default=False
1167
              Enable two-grid DSA for this groupset.
1168
          - tgdsa_l_abs_tol: float, default=1.0e-4
1169
              TGDSA linear absolute tolerance.
1170
          - tgdsa_l_max_its: int, default=30
1171
              TGDSA maximum iterations.
1172
          - tgdsa_verbose: bool, default=False
1173
              Verbose TGDSA output.
1174
          - tgdsa_petsc_options: str, default=''
1175
    xs_map : list of dict
1176
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
1177
          - block_ids: List[int] (required)
1178
              Mesh block IDs to associate with the cross section.
1179
          - xs: pyopensn.xs.MultiGroupXS (required)
1180
              Cross-section object to assign to the specified blocks.
1181
    boundary_conditions: List[Dict], default=[]
1182
        A list containing tables for each boundary specification. Each dictionary supports:
1183
          - name: str (required)
1184
              Boundary name that identifies the specific boundary.
1185
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
1186
              Boundary type specification.
1187
          - group_strength: List[float], optional
1188
              Required when ``type='isotropic'``. Isotropic strength per group.
1189
          - function: AngularFluxFunction, optional
1190
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
1191
    point_sources: List[pyopensn.source.PointSource], default=[]
1192
        A list of point sources.
1193
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
1194
        A list of volumetric sources.
1195
    options : dict, optional
1196
        A block of optional configuration parameters applied at problem creation, including:
1197
          - max_mpi_message_size: int, default=32768
1198
          - restart_writes_enabled: bool, default=False
1199
          - write_delayed_psi_to_restart: bool, default=True
1200
          - read_restart_path: str, default=''
1201
          - write_restart_path: str, default=''
1202
          - write_restart_time_interval: int, default=0
1203
          - use_precursors: bool, default=True
1204
            Enable delayed-neutron precursor treatment. This remains active across later
1205
            ``SetXSMap`` calls, even if the current map temporarily has no precursor-bearing
1206
            material. When XS maps change, precursor concentrations are remapped by cell and
1207
            precursor-family index; newly introduced families start at zero and removed families
1208
            are discarded.
1209
          - use_source_moments: bool, default=False
1210
          - save_angular_flux: bool, default=False
1211
            Store angular flux state (`psi`) for transient mode, angular-flux
1212
            field functions, and angular-flux I/O.
1213
          - verbose_inner_iterations: bool, default=True
1214
            Print inner iteration details, including WGS and AGS iterations.
1215
          - verbose_outer_iterations: bool, default=True
1216
            Print outer solver progress, including PI/NLKE iterations and transient steps.
1217
          - max_ags_iterations: int, default=100
1218
          - ags_tolerance: float, default=1.0e-6
1219
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
1220
          - power_default_kappa: float, default=3.20435e-11
1221
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
1222
          - field_function_prefix: str, default=''
1223
    sweep_type : str, optional
1224
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
1225
        If ``time_dependent=True``, ``options.save_angular_flux=True`` is required.
1226
    )"
1227
  );
1228
}
670✔
1229

1230
// Wrap steady-state solver
1231
void
1232
WrapSteadyState(py::module& slv)
670✔
1233
{
1234
  const auto balance_table_to_dict = [](const BalanceTable& table)
689✔
1235
  {
1236
    py::dict values;
19✔
1237
    values["absorption_rate"] = table.absorption_rate;
19✔
1238
    values["production_rate"] = table.production_rate;
19✔
1239
    values["inflow_rate"] = table.inflow_rate;
19✔
1240
    values["outflow_rate"] = table.outflow_rate;
19✔
1241
    values["balance"] = table.balance;
19✔
1242
    if (table.initial_inventory.has_value())
19✔
UNCOV
1243
      values["initial_inventory"] = table.initial_inventory.value();
×
1244
    if (table.final_inventory.has_value())
19✔
1245
      values["final_inventory"] = table.final_inventory.value();
×
1246
    if (table.predicted_inventory_change.has_value())
19✔
1247
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1248
    if (table.actual_inventory_change.has_value())
19✔
1249
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1250
    if (table.inventory_residual.has_value())
19✔
1251
      values["inventory_residual"] = table.inventory_residual.value();
×
1252
    return values;
19✔
1253
  };
×
1254

1255
  // clang-format off
1256
  // steady state solver
1257
  auto steady_state_solver = py::class_<SteadyStateSourceSolver, std::shared_ptr<SteadyStateSourceSolver>,
670✔
1258
                                        Solver>(
1259
    slv,
1260
    "SteadyStateSourceSolver",
1261
    R"(
1262
    Steady state solver.
1263

1264
    Wrapper of :cpp:class:`opensn::SteadyStateSourceSolver`.
1265
    )"
1266
  );
670✔
1267
  steady_state_solver.def(
1,340✔
1268
    py::init(
670✔
1269
      [](py::kwargs& params)
412✔
1270
      {
1271
        return SteadyStateSourceSolver::Create(kwargs_to_param_block(params));
412✔
1272
      }
1273
    ),
1274
    R"(
1275
    Construct a steady state solver.
1276

1277
    Parameters
1278
    ----------
1279
    problem : pyopensn.solver.LBSProblem
1280
        Existing LBSProblem instance.
1281

1282
    Notes
1283
    -----
1284
    If ``problem.options.read_restart_path`` is set, restart data is read
1285
    during :meth:`Initialize`. If ``problem.options.restart_writes_enabled`` is
1286
    true, a restart dump is written after :meth:`Execute` completes.
1287
    problem : pyopensn.solver.DiscreteOrdinatesProblem
1288
        Existing discrete ordinates problem instance.
1289
    )"
1290
  );
1291
  steady_state_solver.def(
670✔
1292
    "ComputeBalanceTable",
1293
    [balance_table_to_dict](const SteadyStateSourceSolver& self)
670✔
1294
    {
1295
      return balance_table_to_dict(self.ComputeBalanceTable());
19✔
1296
    },
1297
    R"(
1298
    Compute and return the global balance table using the solver's normalization.
1299
    This is a collective operation and must be called on all ranks.
1300

1301
    Returns
1302
    -------
1303
    dict
1304
        Dictionary with the following entries:
1305

1306
        - ``absorption_rate``:
1307
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1308
          groups and the full domain.
1309
        - ``production_rate``:
1310
          Global volumetric production/source rate used by the solver,
1311
          approximately ``integral Q dV`` summed over groups and the full domain.
1312
        - ``inflow_rate``:
1313
          Global incoming boundary contribution integrated over incoming
1314
          angular flux on boundaries.
1315
        - ``outflow_rate``:
1316
          Global outgoing boundary contribution accumulated from face outflow
1317
          tallies.
1318
        - ``balance``:
1319
          Rate balance,
1320
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1321

1322
    Notes
1323
    -----
1324
    This solver applies no extra normalization to the balance table.
1325
    )"
1326
  );
1327
  // clang-format on
1328
}
670✔
1329

1330
// Wrap transient solver
1331
void
1332
WrapTransient(py::module& slv)
670✔
1333
{
1334
  const auto balance_table_to_dict = [](const BalanceTable& table)
670✔
1335
  {
UNCOV
1336
    py::dict values;
×
UNCOV
1337
    values["absorption_rate"] = table.absorption_rate;
×
1338
    values["production_rate"] = table.production_rate;
×
1339
    values["inflow_rate"] = table.inflow_rate;
×
1340
    values["outflow_rate"] = table.outflow_rate;
×
1341
    values["balance"] = table.balance;
×
1342
    if (table.initial_inventory.has_value())
×
1343
      values["initial_inventory"] = table.initial_inventory.value();
×
1344
    if (table.final_inventory.has_value())
×
1345
      values["final_inventory"] = table.final_inventory.value();
×
1346
    if (table.predicted_inventory_change.has_value())
×
1347
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1348
    if (table.actual_inventory_change.has_value())
×
1349
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1350
    if (table.inventory_residual.has_value())
×
1351
      values["inventory_residual"] = table.inventory_residual.value();
×
1352
    return values;
×
1353
  };
×
1354
  // clang-format off
1355
  auto transient_solver =
670✔
1356
    py::class_<TransientSolver, std::shared_ptr<TransientSolver>, Solver>(
1357
      slv,
1358
      "TransientSolver",
1359
      R"(
1360
      Transient solver.
1361

1362
      Wrapper of :cpp:class:`opensn::TransientSolver`.
1363
      )"
1364
    );
670✔
1365
  transient_solver.def(
1,340✔
1366
    py::init(
670✔
1367
      [](py::kwargs& params)
180✔
1368
      {
1369
        return TransientSolver::Create(kwargs_to_param_block(params));
180✔
1370
      }
1371
    ),
1372
    R"(
1373
    Construct a transient solver.
1374

1375
    Parameters
1376
    ----------
1377
    pyopensn.solver.DiscreteOrdinatesProblem : DiscreteOrdinatesProblem
1378
        Existing discrete ordinates problem instance.
1379
    dt : float, optional, default=2.0e-3
1380
        Time step size used during the simulation.
1381
    stop_time : float, optional, default=0.1
1382
        Simulation end time.
1383
    theta : float, optional, default=0.5
1384
        Time differencing scheme parameter.
1385
    initial_state : str, optional, default="existing"
1386
        Initial state for the transient solve. Allowed values: existing, zero.
1387
        In "zero" mode, scalar flux vectors are reset to zero.
1388
    verbose : bool, optional, default=True
1389
        Enable verbose logging.
1390

1391
    Notes
1392
    -----
1393
    The associated problem must have ``save_angular_flux=True`` enabled. This
1394
    is required for transient problems.
1395

1396
    If ``problem.options.read_restart_path`` is set, restart data is read
1397
    during :meth:`Initialize`. If ``problem.options.restart_writes_enabled`` is
1398
    true, timed restart dumps may be written during :meth:`Execute` and a final
1399
    restart dump is written when execution completes.
1400
    )"
1401
  );
1402
  transient_solver.def(
670✔
1403
    "SetTimeStep",
1404
    &TransientSolver::SetTimeStep,
670✔
1405
    R"(
1406
    Set the timestep size used by :meth:`Advance`.
1407

1408
    Parameters
1409
    ----------
1410
    dt : float
1411
        New timestep size.
1412
    )");
1413
  transient_solver.def(
670✔
1414
    "SetTheta",
1415
    &TransientSolver::SetTheta,
670✔
1416
    R"(
1417
    Set the theta parameter used by :meth:`Advance`.
1418

1419
    Parameters
1420
    ----------
1421
    theta : float
1422
        Theta value between 1.0e-16 and 1.
1423
    )");
1424
  transient_solver.def(
670✔
1425
    "Advance",
1426
    &TransientSolver::Advance,
670✔
1427
    R"(
1428
    Advance the solver by a single timestep.
1429

1430
    Notes
1431
    -----
1432
    You must call :meth:`Initialize` before calling :meth:`Advance` or
1433
    :meth:`Execute`.
1434
    )");
1435
  transient_solver.def(
670✔
1436
    "SetPreAdvanceCallback",
1437
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
670✔
1438
      &TransientSolver::SetPreAdvanceCallback),
1439
    R"(
1440
    Register a callback that runs before each advance within :meth:`Execute`.
1441

1442
    Parameters
1443
    ----------
1444
    callback : Optional[Callable[[], None]]
1445
        Function invoked before the solver advances a timestep. Pass None to clear.
1446
        If the callback modifies the timestep, the new value is used for the
1447
        upcoming step.
1448
    )");
1449
  transient_solver.def(
670✔
1450
    "SetPreAdvanceCallback",
1451
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
670✔
1452
      &TransientSolver::SetPreAdvanceCallback),
1453
    "Clear the PreAdvance callback by passing None.");
1454
  transient_solver.def(
670✔
1455
    "SetPostAdvanceCallback",
1456
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
670✔
1457
      &TransientSolver::SetPostAdvanceCallback),
1458
    R"(
1459
    Register a callback that runs after each advance within :meth:`Execute`.
1460

1461
    Parameters
1462
    ----------
1463
    callback : Optional[Callable[[], None]]
1464
        Function invoked after the solver advances a timestep. Pass None to clear.
1465
    )");
1466
  transient_solver.def(
670✔
1467
    "SetPostAdvanceCallback",
1468
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
670✔
1469
      &TransientSolver::SetPostAdvanceCallback),
1470
    "Clear the PostAdvance callback by passing None.");
1471
  transient_solver.def(
670✔
1472
    "ComputeBalanceTable",
1473
    [balance_table_to_dict](const TransientSolver& self)
670✔
1474
    {
UNCOV
1475
      return balance_table_to_dict(self.ComputeBalanceTable());
×
1476
    },
1477
    R"(
1478
    Compute and return the global balance table using the solver's normalization.
1479
    This is a collective operation and must be called on all ranks.
1480

1481
    Returns
1482
    -------
1483
    dict
1484
        Dictionary with the following entries:
1485

1486
        - ``absorption_rate``:
1487
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1488
          groups and the full domain.
1489
        - ``production_rate``:
1490
          Global volumetric production/source rate used by the solver,
1491
          approximately ``integral Q dV`` summed over groups and the full domain.
1492
        - ``inflow_rate``:
1493
          Global incoming boundary contribution integrated over incoming
1494
          angular flux on boundaries.
1495
        - ``outflow_rate``:
1496
          Global outgoing boundary contribution accumulated from face outflow
1497
          tallies.
1498
        - ``balance``:
1499
          Rate balance,
1500
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1501
        - ``initial_inventory``:
1502
          Total particle inventory at the start of the timestep, computed as
1503
          ``integral (1 / v_g) * phi_old dV`` summed over groups and the full domain.
1504
        - ``final_inventory``:
1505
          Total particle inventory at the end of the timestep, computed as
1506
          ``integral (1 / v_g) * phi_new dV`` summed over groups and the full domain.
1507
        - ``predicted_inventory_change``:
1508
          Inventory change predicted by the current timestep balance, computed as 
1509
          ``dt * balance``.
1510
        - ``actual_inventory_change``:
1511
          Measured change in total particle inventory over the timestep, computed as
1512
          ``final_inventory - initial_inventory``.
1513
        - ``inventory_residual``:
1514
          Mismatch between the measured and predicted timestep inventory
1515
          changes, computed as
1516
          ``actual_inventory_change - predicted_inventory_change``.
1517

1518
    Notes
1519
    -----
1520
    This solver applies no extra normalization to the balance table.
1521

1522
    The transient inventory terms currently use the end-of-step rate balance to
1523
    estimate the timestep inventory change.
1524
    )"
1525
  );
1526
  slv.attr("BackwardEuler") = 1.0;
670✔
1527
  slv.attr("CrankNicolson") = 0.5;
1,340✔
1528
  // clang-format on
1529
}
670✔
1530

1531
// Wrap non-linear k-eigen solver
1532
void
1533
WrapNLKEigen(py::module& slv)
670✔
1534
{
1535
  const auto balance_table_to_dict = [](const BalanceTable& table)
670✔
1536
  {
UNCOV
1537
    py::dict values;
×
UNCOV
1538
    values["absorption_rate"] = table.absorption_rate;
×
1539
    values["production_rate"] = table.production_rate;
×
1540
    values["inflow_rate"] = table.inflow_rate;
×
1541
    values["outflow_rate"] = table.outflow_rate;
×
1542
    values["balance"] = table.balance;
×
1543
    if (table.initial_inventory.has_value())
×
1544
      values["initial_inventory"] = table.initial_inventory.value();
×
1545
    if (table.final_inventory.has_value())
×
1546
      values["final_inventory"] = table.final_inventory.value();
×
1547
    if (table.predicted_inventory_change.has_value())
×
1548
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1549
    if (table.actual_inventory_change.has_value())
×
1550
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1551
    if (table.inventory_residual.has_value())
×
1552
      values["inventory_residual"] = table.inventory_residual.value();
×
1553
    return values;
×
1554
  };
×
1555
  // clang-format off
1556
  // non-linear k-eigen solver
1557
  auto non_linear_k_eigen_solver = py::class_<NonLinearKEigenSolver, std::shared_ptr<NonLinearKEigenSolver>,
670✔
1558
                                              Solver>(
1559
    slv,
1560
    "NonLinearKEigenSolver",
1561
    R"(
1562
    Non-linear k-eigenvalue solver.
1563

1564
    Wrapper of :cpp:class:`opensn::NonLinearKEigenSolver`.
1565
    )"
1566
  );
670✔
1567
  non_linear_k_eigen_solver.def(
1,340✔
1568
    py::init(
670✔
1569
      [](py::kwargs& params)
37✔
1570
      {
1571
        return NonLinearKEigenSolver::Create(kwargs_to_param_block(params));
37✔
1572
      }
1573
        ),
1574
    R"(
1575
    Construct a non-linear k-eigenvalue solver.
1576

1577
    Parameters
1578
    ----------
1579
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1580
        Existing discrete ordinates problem instance.
1581
    nl_abs_tol: float, default=1.0e-8
1582
        Non-linear absolute tolerance.
1583
    nl_rel_tol: float, default=1.0e-8
1584
        Non-linear relative tolerance.
1585
    nl_sol_tol: float, default=1.0e-50
1586
        Non-linear solution tolerance.
1587
    nl_max_its: int, default=50
1588
        Non-linear algorithm maximum iterations.
1589
    l_abs_tol: float, default=1.0e-8
1590
        Linear absolute tolerance.
1591
    l_rel_tol: float, default=1.0e-8
1592
        Linear relative tolerance.
1593
    l_div_tol: float, default=1.0e6
1594
        Linear divergence tolerance.
1595
    l_max_its: int, default=50
1596
        Linear algorithm maximum iterations.
1597
    l_gmres_restart_intvl: int, default=30
1598
        GMRES restart interval.
1599
    l_gmres_breakdown_tol: float, default=1.0e6
1600
        GMRES breakdown tolerance.
1601
    reset_phi0: bool, default=True
1602
        If true, reinitializes scalar fluxes to 1.0.
1603
    num_initial_power_iterations: int, default=0
1604
        Number of initial power iterations before the non-linear solve.
1605
    )"
1606
  );
1607
  non_linear_k_eigen_solver.def(
670✔
1608
    "GetEigenvalue",
1609
    &NonLinearKEigenSolver::GetEigenvalue,
670✔
1610
    R"(
1611
    Return the current k-eigenvalue.
1612
    )"
1613
  );
1614
  non_linear_k_eigen_solver.def(
670✔
1615
    "ComputeBalanceTable",
1616
    [balance_table_to_dict](const NonLinearKEigenSolver& self)
670✔
1617
    {
UNCOV
1618
      return balance_table_to_dict(self.ComputeBalanceTable());
×
1619
    },
1620
    R"(
1621
    Compute and return the global balance table using the solver's normalization.
1622
    This is a collective operation and must be called on all ranks.
1623

1624
    Returns
1625
    -------
1626
    dict
1627
        Dictionary with the following entries:
1628

1629
        - ``absorption_rate``:
1630
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1631
          groups and the full domain.
1632
        - ``production_rate``:
1633
          Global volumetric production/source rate used by the solver,
1634
          approximately ``integral Q dV`` summed over groups and the full domain.
1635
        - ``inflow_rate``:
1636
          Global incoming boundary contribution integrated over incoming
1637
          angular flux on boundaries.
1638
        - ``outflow_rate``:
1639
          Global outgoing boundary contribution accumulated from face outflow
1640
          tallies.
1641
        - ``balance``:
1642
          Rate balance,
1643
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1644

1645
    Notes
1646
    -----
1647
    For k-eigenvalue balance reporting, this solver scales the production term by
1648
    ``1 / k_eff`` before forming both ``production_rate`` and ``balance``.
1649
    )"
1650
  );
1651
  // clang-format on
1652
}
670✔
1653

1654
// Wrap power iteration solvers
1655
void
1656
WrapPIteration(py::module& slv)
670✔
1657
{
1658
  const auto balance_table_to_dict = [](const BalanceTable& table)
674✔
1659
  {
1660
    py::dict values;
4✔
1661
    values["absorption_rate"] = table.absorption_rate;
4✔
1662
    values["production_rate"] = table.production_rate;
4✔
1663
    values["inflow_rate"] = table.inflow_rate;
4✔
1664
    values["outflow_rate"] = table.outflow_rate;
4✔
1665
    values["balance"] = table.balance;
4✔
1666
    if (table.initial_inventory.has_value())
4✔
UNCOV
1667
      values["initial_inventory"] = table.initial_inventory.value();
×
1668
    if (table.final_inventory.has_value())
4✔
1669
      values["final_inventory"] = table.final_inventory.value();
×
1670
    if (table.predicted_inventory_change.has_value())
4✔
1671
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1672
    if (table.actual_inventory_change.has_value())
4✔
1673
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1674
    if (table.inventory_residual.has_value())
4✔
1675
      values["inventory_residual"] = table.inventory_residual.value();
×
1676
    return values;
4✔
1677
  };
×
1678
  // clang-format off
1679
  // power iteration k-eigen solver
1680
  auto pi_k_eigen_solver = py::class_<PowerIterationKEigenSolver, std::shared_ptr<PowerIterationKEigenSolver>,
670✔
1681
                                      Solver>(
1682
    slv,
1683
    "PowerIterationKEigenSolver",
1684
    R"(
1685
    Power iteration k-eigenvalue solver.
1686

1687
    Wrapper of :cpp:class:`opensn::PowerIterationKEigenSolver`.
1688
    )"
1689
  );
670✔
1690
  pi_k_eigen_solver.def(
1,340✔
1691
    py::init(
670✔
1692
      [](py::kwargs& params)
161✔
1693
      {
1694
        return PowerIterationKEigenSolver::Create(kwargs_to_param_block(params));
161✔
1695
      }
1696
    ),
1697
    R"(
1698
    Construct a power iteration k-eigen solver.
1699

1700
    Parameters
1701
    ----------
1702
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1703
        Existing DiscreteOrdinatesProblem instance.
1704
    acceleration: pyopensn.solver.DiscreteOrdinatesKEigenAcceleration
1705
        Optional DiscreteOrdinatesKEigenAcceleration instance for acceleration.
1706
    max_iters: int, default = 1000
1707
        Maximum power iterations allowed.
1708
    k_tol: float, default = 1.0e-10
1709
        Tolerance on the k-eigenvalue.
1710
    reset_solution: bool, default=True
1711
        If true, initialize flux moments to 1.0.
1712
    reset_phi0: bool, default=True
1713
        If true, reinitializes scalar fluxes to 1.0.
1714

1715
    Notes
1716
    -----
1717
    If ``problem.options.read_restart_path`` is set, restart data is read
1718
    during :meth:`Initialize`. If ``problem.options.restart_writes_enabled`` is
1719
    true, timed restart dumps may be written during the outer iteration loop
1720
    and a final restart dump is written when execution completes.
1721
    )"
1722
  );
1723
  pi_k_eigen_solver.def(
670✔
1724
    "GetEigenvalue",
1725
    &PowerIterationKEigenSolver::GetEigenvalue,
670✔
1726
    R"(
1727
    Return the current k-eigenvalue.
1728
    )"
1729
  );
1730
  pi_k_eigen_solver.def(
670✔
1731
    "ComputeBalanceTable",
1732
    [balance_table_to_dict](const PowerIterationKEigenSolver& self)
670✔
1733
    {
1734
      return balance_table_to_dict(self.ComputeBalanceTable());
4✔
1735
    },
1736
    R"(
1737
    Compute and return the global balance table using the solver's normalization.
1738
    This is a collective operation and must be called on all ranks.
1739

1740
    Returns
1741
    -------
1742
    dict
1743
        Dictionary with the following entries:
1744

1745
        - ``absorption_rate``:
1746
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1747
          groups and the full domain.
1748
        - ``production_rate``:
1749
          Global volumetric production/source rate used by the solver,
1750
          approximately ``integral Q dV`` summed over groups and the full domain.
1751
        - ``inflow_rate``:
1752
          Global incoming boundary contribution integrated over incoming
1753
          angular flux on boundaries.
1754
        - ``outflow_rate``:
1755
          Global outgoing boundary contribution accumulated from face outflow
1756
          tallies.
1757
        - ``balance``:
1758
          Rate balance,
1759
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1760

1761
    Notes
1762
    -----
1763
    For k-eigenvalue balance reporting, this solver scales the production term by
1764
    ``1 / k_eff`` before forming both ``production_rate`` and ``balance``.
1765
    )"
1766
  );
1767
  // clang-format on
1768
}
670✔
1769

1770
// Wrap LBS solver
1771
void
1772
WrapDiscreteOrdinatesKEigenAcceleration(py::module& slv)
670✔
1773
{
1774
  // clang-format off
1775
  // discrete ordinates k-eigen acceleration base
1776
  auto acceleration = py::class_<DiscreteOrdinatesKEigenAcceleration,
670✔
1777
                                 std::shared_ptr<DiscreteOrdinatesKEigenAcceleration>>(
1778
    slv,
1779
    "DiscreteOrdinatesKEigenAcceleration",
1780
    R"(
1781
    Base class for discrete ordinates k-eigenvalue acceleration methods.
1782

1783
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesKEigenAcceleration`.
1784
    )"
1785
  );
670✔
1786
  // SCDSA acceleration
1787
  auto scdsa_acceleration = py::class_<SCDSAAcceleration,
670✔
1788
                                       std::shared_ptr<SCDSAAcceleration>,
1789
                                       DiscreteOrdinatesKEigenAcceleration>(
1790
    slv,
1791
    "SCDSAAcceleration",
1792
    R"(
1793
    Construct an SCDSA accelerator for the power iteration k-eigenvalue solver.
1794

1795
    Wrapper of :cpp:class:`opensn::SCDSAAcceleration`.
1796
    )"
1797
  );
670✔
1798
  scdsa_acceleration.def(
1,340✔
1799
    py::init(
670✔
1800
      [](py::kwargs& params)
8✔
1801
      {
1802
        return SCDSAAcceleration::Create(kwargs_to_param_block(params));
8✔
1803
      }
1804
    ),
1805
    R"(
1806
    SCDSA acceleration for the power iteration k-eigenvalue solver.
1807

1808
    Parameters
1809
    ----------
1810
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1811
        Existing DiscreteOrdinatesProblem instance.
1812
    l_abs_tol: float, defauilt=1.0e-10
1813
        Absolute residual tolerance.
1814
    max_iters: int, default=100
1815
        Maximum allowable iterations.
1816
    verbose: bool, default=False
1817
        If true, enables verbose output.
1818
    petsc_options: str, default="ssss"
1819
        Additional PETSc options.
1820
    pi_max_its: int, default=50
1821
        Maximum allowable iterations for inner power iterations.
1822
    pi_k_tol: float, default=1.0e-10
1823
        k-eigenvalue tolerance for the inner power iterations.
1824
    sdm: str, default="pwld"
1825
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1826
            - 'pwld' : Piecewise Linear Discontinuous
1827
            - 'pwlc' : Piecewise Linear Continuous
1828
    )"
1829
  );
1830
  // SMM acceleration
1831
  auto smm_acceleration = py::class_<SMMAcceleration,
670✔
1832
                                     std::shared_ptr<SMMAcceleration>,
1833
                                     DiscreteOrdinatesKEigenAcceleration>(
1834
    slv,
1835
    "SMMAcceleration",
1836
    R"(
1837
    Construct an SMM accelerator for the power iteration k-eigenvalue solver.
1838

1839
    Wrapper of :cpp:class:`opensn::SMMAcceleration`.
1840
    )"
1841
  );
670✔
1842
  smm_acceleration.def(
1,340✔
1843
    py::init(
670✔
1844
      [](py::kwargs& params)
4✔
1845
      {
1846
        return SMMAcceleration::Create(kwargs_to_param_block(params));
4✔
1847
      }
1848
    ),
1849
    R"(
1850
    SMM acceleration for the power iteration k-eigenvalue solver.
1851

1852
    Warnings
1853
    --------
1854
       SMM acceleration is **experimental** and should be used with caution!
1855
       SMM accleration only supports problems with isotropic scattering.
1856

1857
    Parameters
1858
    ----------
1859
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1860
        Existing DiscreteOrdinatesProblem instance.
1861
    l_abs_tol: float, defauilt=1.0e-10
1862
        Absolute residual tolerance.
1863
    max_iters: int, default=100
1864
        Maximum allowable iterations.
1865
    verbose: bool, default=False
1866
        If true, enables verbose output.
1867
    petsc_options: str, default="ssss"
1868
        Additional PETSc options.
1869
    pi_max_its: int, default=50
1870
        Maximum allowable iterations for inner power iterations.
1871
    pi_k_tol: float, default=1.0e-10
1872
        k-eigenvalue tolerance for the inner power iterations.
1873
    sdm: str, default="pwld"
1874
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1875
            - 'pwld' : Piecewise Linear Discontinuous
1876
            - 'pwlc' : Piecewise Linear Continuous
1877
    )"
1878
  );
1879
  // clang-format on
1880
}
670✔
1881

1882
// Wrap the solver components of OpenSn
1883
void
1884
py_solver(py::module& pyopensn)
72✔
1885
{
1886
  py::module slv = pyopensn.def_submodule("solver", "Solver module.");
72✔
1887
  WrapProblem(slv);
72✔
1888
  WrapSolver(slv);
72✔
1889
  WrapLBS(slv);
72✔
1890
  WrapSteadyState(slv);
72✔
1891
  WrapTransient(slv);
72✔
1892
  WrapNLKEigen(slv);
72✔
1893
  WrapDiscreteOrdinatesKEigenAcceleration(slv);
72✔
1894
  WrapPIteration(slv);
72✔
1895
}
72✔
1896

1897
} // 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