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

Open-Sn / opensn / 25478472129

07 May 2026 04:23AM UTC coverage: 75.685% (+0.3%) from 75.416%
25478472129

push

github

web-flow
Merge pull request #1057 from wdhawkins/c5g7_fixes

Updating tests.json for c5g7 tests and new iterative output.

22081 of 29175 relevant lines covered (75.68%)

65079358.06 hits per line

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

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

32
namespace opensn
33
{
34

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

820
    Notes
821
    -----
822
    Switch problem from steady-state to time-dependent mode. This updates problem
823
    internals (sweep chunk mode and source-function) while preserving user boundary
824
    conditions and fixed sources.
825

826
    Requires ``options.save_angular_flux=True`` at problem creation.
827
    )"
828
  );
829
  do_problem.def(
714✔
830
    "SetSteadyStateMode",
831
    &DiscreteOrdinatesProblem::SetSteadyStateMode,
714✔
832
    R"(
833
    Set the problem to steady-state mode.
834

835
    Notes
836
    -----
837
    Switch problem from time-dependent to steady-state mode. This updates problem
838
    internals (sweep chunk mode and source-function) while preserving user boundary
839
    conditions and fixed sources.
840
    )"
841
  );
842
  do_problem.def(
714✔
843
    "IsTimeDependent",
844
    &DiscreteOrdinatesProblem::IsTimeDependent,
714✔
845
    R"(
846
    Return ``True`` if the problem is currently in time-dependent mode.
847
    )"
848
  );
849
  do_problem.def(
714✔
850
    "SetBoundaryOptions",
851
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
894✔
852
    {
853
      bool clear_boundary_conditions = false;
180✔
854
      std::vector<InputParameters> boundary_params;
180✔
855
      for (auto [key, value] : params)
716✔
856
      {
857
        auto c_key = key.cast<std::string>();
356✔
858
        if (c_key == "clear_boundary_conditions")
356✔
859
          clear_boundary_conditions = value.cast<bool>();
176✔
860
        else if (c_key == "boundary_conditions")
180✔
861
        {
862
          auto boundaries = value.cast<py::list>();
180✔
863
          for (auto boundary : boundaries)
968✔
864
          {
865
            InputParameters input = DiscreteOrdinatesProblem::GetBoundaryOptionsBlock();
608✔
866
            input.AssignParameters(pyobj_to_param_block("", boundary.cast<py::dict>()));
608✔
867
            boundary_params.push_back(std::move(input));
608✔
868
          }
608✔
869
        }
180✔
870
        else
871
          throw std::runtime_error("Invalid argument provided to SetBoundaryOptions.\n");
×
872
      }
356✔
873
      if (clear_boundary_conditions or not boundary_params.empty())
180✔
874
        self.SetBoundaryOptions(boundary_params, clear_boundary_conditions);
180✔
875
    },
180✔
876
    R"(
877
    Set or clear boundary conditions.
878

879
    Parameters
880
    ----------
881
    clear_boundary_conditions: bool, default=False
882
        If true, all current boundary conditions are deleted.
883
    boundary_conditions: List[Dict]
884
        A list of boundary condition dictionaries. Each dictionary supports:
885
          - name: str (required)
886
              Boundary name that identifies the specific boundary.
887
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
888
              Boundary type specification.
889
          - group_strength: List[float], optional
890
              Required when ``type='isotropic'``. Isotropic strength per group.
891
          - start_time: float, optional
892
              Active start time for isotropic boundaries only. Defaults to -infinity.
893
          - end_time: float, optional
894
              Active end time for isotropic boundaries only. Defaults to infinity.
895
          - function: AngularFluxFunction, optional
896
              Required when ``type='arbitrary'`` unless ``time_function`` is supplied. Callable
897
              that returns incoming angular flux from group and direction.
898
          - time_function: AngularFluxTimeFunction, optional
899
              Required when ``type='arbitrary'`` unless ``function`` is supplied. Callable that
900
              returns incoming angular flux from group, direction, and time.
901
        Isotropic boundaries may use ``start_time``/``end_time`` for simple on/off behavior.
902
        Arbitrary boundaries must specify exactly one of ``function`` or ``time_function``; use
903
        ``time_function`` for time-dependent arbitrary inflow and handle any active window inside
904
        that callback.
905

906
    Notes
907
    -----
908
    Mode transitions via :meth:`LBSProblem.SetAdjoint` clear all boundary conditions.
909
    Reapply boundaries with this method before solving in the new mode.
910
    )"
911
  );
912
  do_problem.def(
714✔
913
    "ZeroPsi",
914
    [](DiscreteOrdinatesProblem& self)
714✔
915
    {
916
      self.ZeroPsi();
×
917
    },
918
    R"(
919
    Zero the angular-flux storage.
920
    )"
921
  );
922
  do_problem.def(
714✔
923
    "GetPsi",
924
    [](DiscreteOrdinatesProblem& self)
722✔
925
    {
926
      const auto& psi = self.GetPsiNewLocal();
8✔
927
      py::list psi_list;
8✔
928
      for (const auto& vec : psi)
16✔
929
      {
930
        auto array = py::array_t<double>(static_cast<py::ssize_t>(vec.size()));
8✔
931
        std::copy(vec.begin(), vec.end(), static_cast<double*>(array.mutable_data()));
8✔
932
        psi_list.append(array);
8✔
933
      }
8✔
934
      return psi_list;
8✔
935
    },
×
936
    R"(
937
    Return psi as a list of NumPy arrays (float64).
938

939
    The arrays are copies of the current angular-flux state. Mutating them does not
940
    mutate the problem.
941
    )"
942
  );
943
  do_problem.def(
714✔
944
    "GetAngularFieldFunctionList",
945
    [](DiscreteOrdinatesProblem& self, py::list groups, py::list angles)
714✔
946
    {
947
      std::vector<unsigned int> group_ids;
×
948
      std::vector<size_t> angle_ids;
×
949
      group_ids.reserve(groups.size());
×
950
      angle_ids.reserve(angles.size());
×
951

952
      for (py::handle g : groups)
×
953
        group_ids.push_back(g.cast<unsigned int>());
×
954
      for (py::handle a : angles)
×
955
        angle_ids.push_back(a.cast<size_t>());
×
956

957
      auto ff_list = self.CreateAngularFluxFieldFunctionList(group_ids, angle_ids);
×
958
      py::list out;
×
959
      for (const auto& ff : ff_list)
×
960
        out.append(ff);
×
961
      return out;
×
962
    },
×
963
    R"(
964
    Create field functions for selected angular flux components.
965

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

970
    Example
971
    -------
972
    .. code-block:: python
973

974
       solver.Initialize()
975
       solver.Execute()
976
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
977

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

983
    1) Use ``TransientSolver.Execute()`` with a post-advance callback:
984

985
    .. code-block:: python
986

987
       solver = TransientSolver(problem=phys)
988
       solver.Initialize()
989
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
990

991
       def post_advance():
992
           for ff in ang_ff:
993
               ff.Update()
994
           FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
995

996
       solver.SetPostAdvanceCallback(post_advance)
997
       solver.Execute()
998

999
    2) Use a custom Python loop with ``TransientSolver.Advance()``:
1000

1001
    .. code-block:: python
1002

1003
       solver = TransientSolver(problem=phys)
1004
       solver.Initialize()
1005
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
1006
       for _ in range(num_steps):
1007
           solver.Advance()
1008
           for ff in ang_ff:
1009
               ff.Update()
1010
           FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
1011

1012
    Parameters
1013
    ----------
1014
    groups : List[int]
1015
        Global group indices to export.
1016
    angles : List[int]
1017
        Angle indices within the groupset quadrature for each group.
1018

1019
    Returns
1020
    -------
1021
    List[pyopensn.fieldfunc.FieldFunctionGridBased]
1022
        Field functions for the requested ``(group, angle)`` pairs. Each returned field function
1023
        is a snapshot, but supports ``CanUpdate()`` and ``Update()`` while its owning problem is
1024
        alive.
1025
    )",
1026
    py::arg("groups"),
1,428✔
1027
    py::arg("angles")
714✔
1028
  );
1029
  do_problem.def(
714✔
1030
    "ComputeLeakage",
1031
    [](DiscreteOrdinatesProblem& self, py::list bnd_names)
1,137✔
1032
    {
1033
      auto grid = self.GetGrid();
423✔
1034
      // get the supported boundaries
1035
      std::map<std::string, std::uint64_t> allowed_bd_names = grid->GetBoundaryNameMap();
423✔
1036
      std::map<std::uint64_t, std::string> allowed_bd_ids = grid->GetBoundaryIDMap();
423✔
1037
      const auto coord_sys = grid->GetCoordinateSystem();
423✔
1038
      const auto mesh_type = grid->GetType();
423✔
1039
      const auto dim = grid->GetDimension();
423✔
1040
      // get the boundaries to parse, preserving user order
1041
      std::vector<std::uint64_t> bndry_ids;
423✔
1042
      if (bnd_names.size() > 1)
423✔
1043
      {
1044
        for (py::handle name : bnd_names)
2,488✔
1045
        {
1046
          auto sname = name.cast<std::string>();
1,648✔
1047
          if (coord_sys == CoordinateSystemType::CYLINDRICAL && dim == 2)
1,648✔
1048
          {
1049
            if (sname == "xmin" || sname == "xmax" || sname == "ymin" || sname == "ymax")
24✔
1050
              throw std::runtime_error("ComputeLeakage: Boundary name '" + sname +
×
1051
                                       "' is invalid for cylindrical orthogonal meshes. "
1052
                                       "Use rmin, rmax, zmin, zmax.");
×
1053

1054
            if (mesh_type == MeshType::ORTHOGONAL)
24✔
1055
            {
1056
              if (sname == "rmin") sname = "xmin";
24✔
1057
              else if (sname == "rmax") sname = "xmax";
24✔
1058
              else if (sname == "zmin") sname = "ymin";
16✔
1059
              else if (sname == "zmax") sname = "ymax";
8✔
1060
            }
1061
          }
1062
          bndry_ids.push_back(allowed_bd_names.at(sname));
1,648✔
1063
        }
1,648✔
1064
      }
1065
      else
1066
      {
1067
        bndry_ids = self.GetGrid()->GetUniqueBoundaryIDs();
9✔
1068
      }
1069
      // compute the leakage
1070
      std::map<std::uint64_t, std::vector<double>> leakage = ComputeLeakage(self, bndry_ids);
423✔
1071
      const bool rz_ortho = (coord_sys == CoordinateSystemType::CYLINDRICAL &&
846✔
1072
                             mesh_type == MeshType::ORTHOGONAL && dim == 2);
423✔
1073

1074
      auto to_rz_name = [](const std::string& name)
447✔
1075
      {
1076
        if (name == "xmin") return std::string("rmin");
24✔
1077
        if (name == "xmax") return std::string("rmax");
24✔
1078
        if (name == "ymin") return std::string("zmin");
16✔
1079
        if (name == "ymax") return std::string("zmax");
8✔
1080
        return name;
×
1081
      };
1082

1083
      // convert result to native Python
1084
      py::dict result;
423✔
1085
      for (const auto& bndry_id : bndry_ids)
2,077✔
1086
      {
1087
        const auto it = leakage.find(bndry_id);
1,654✔
1088
        if (it == leakage.end())
1,654✔
1089
          continue;
×
1090
        // construct numpy array and copy contents
1091
        const auto& grp_wise_leakage = it->second;
1,654✔
1092
        py::array_t<double> np_vector(py::ssize_t(grp_wise_leakage.size()));
1,654✔
1093
        auto buffer = np_vector.request();
1,654✔
1094
        auto *np_vector_data = static_cast<double*>(buffer.ptr);
1,654✔
1095
        std::copy(grp_wise_leakage.begin(), grp_wise_leakage.end(), np_vector_data);
1,654✔
1096
        std::string name = allowed_bd_ids.at(bndry_id);
1,654✔
1097
        if (rz_ortho)
1,654✔
1098
          name = to_rz_name(name);
24✔
1099
        result[py::str(name)] = std::move(np_vector);
3,308✔
1100
      }
1,654✔
1101

1102
      return result;
423✔
1103
    },
846✔
1104
    R"(
1105
    Compute leakage for the problem.
1106

1107
    Parameters
1108
    ----------
1109
    bnd_names : List[str]
1110
        A list of boundary names for which leakage should be computed.
1111

1112
    Returns
1113
    -------
1114
    Dict[str, numpy.ndarray]
1115
        A dictionary mapping boundary names to group-wise leakage vectors.
1116
        Each array contains the outgoing angular flux (per group) integrated over
1117
        the corresponding boundary surface.
1118

1119
    Raises
1120
    ------
1121
    RuntimeError
1122
        If `save_angular_flux` option was not enabled during problem setup.
1123

1124
    ValueError
1125
        If one or more boundary ids are not present on the current mesh.
1126
    )",
1127
    py::arg("bnd_names")
714✔
1128
  );
1129

1130
  // discrete ordinates curvilinear problem
1131
  auto do_curvilinear_problem = py::class_<DiscreteOrdinatesCurvilinearProblem,
714✔
1132
                                           std::shared_ptr<DiscreteOrdinatesCurvilinearProblem>,
1133
                                           DiscreteOrdinatesProblem>(
1134
    slv,
1135
    "DiscreteOrdinatesCurvilinearProblem",
1136
    R"(
1137
    Base class for discrete ordinates problems in curvilinear geometry.
1138

1139
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesCurvilinearProblem`.
1140
    )"
1141
  );
714✔
1142
  do_curvilinear_problem.def(
1,428✔
1143
    py::init(
714✔
1144
      [](py::kwargs& params)
80✔
1145
      {
1146
        return DiscreteOrdinatesCurvilinearProblem::Create(kwargs_to_param_block(params));
80✔
1147
      }
1148
    ),
1149
    R"(
1150
    Construct a discrete ordinates problem for curvilinear geometry.
1151

1152
    Warnings
1153
    --------
1154
       DiscreteOrdinatesCurvilinearProblem is **experimental** and should be used with caution!
1155

1156
    Parameters
1157
    ----------
1158
    mesh : MeshContinuum
1159
        The spatial mesh.
1160
    coord_system : int
1161
        Coordinate system to use. Must be set to 2 (cylindrical coordinates).
1162
    num_groups : int
1163
        The total number of energy groups.
1164
    groupsets : list of dict
1165
        A list of input parameter blocks, each block provides the iterative properties for a
1166
        groupset. Each dictionary supports:
1167
          - groups_from_to: List[int] (required)
1168
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
1169
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
1170
              Handle to an angular quadrature.
1171
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
1172
              Angle aggregation method to use during sweeping.
1173
          - angle_aggregation_num_subsets: int, default=1
1174
              Number of angle subsets used for aggregation.
1175
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
1176
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
1177
              Iterative method used for inner linear solves.
1178
          - l_abs_tol: float, default=1.0e-6
1179
              Inner linear solver absolute residual tolerance.
1180
          - l_max_its: int, default=200
1181
              Inner linear solver maximum iterations.
1182
          - gmres_restart_interval: int, default=30
1183
              GMRES restart interval, if GMRES is used.
1184
          - allow_cycles: bool, default=True
1185
              Whether cyclic dependencies are allowed in sweeps.
1186
          - apply_wgdsa: bool, default=False
1187
              Enable within-group DSA for this groupset.
1188
          - wgdsa_l_abs_tol: float, default=1.0e-4
1189
              WGDSA linear absolute tolerance.
1190
          - wgdsa_l_max_its: int, default=30
1191
              WGDSA maximum iterations.
1192
          - wgdsa_verbose: bool, default=False
1193
              Verbose WGDSA output.
1194
          - wgdsa_petsc_options: str, default=''
1195
              PETSc options string for the WGDSA solver.
1196
          - apply_tgdsa: bool, default=False
1197
              Enable two-grid DSA for this groupset.
1198
          - tgdsa_l_abs_tol: float, default=1.0e-4
1199
              TGDSA linear absolute tolerance.
1200
          - tgdsa_l_max_its: int, default=30
1201
              TGDSA maximum iterations.
1202
          - tgdsa_verbose: bool, default=False
1203
              Verbose TGDSA output.
1204
          - tgdsa_petsc_options: str, default=''
1205
    xs_map : list of dict
1206
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
1207
          - block_ids: List[int] (required)
1208
              Mesh block IDs to associate with the cross section.
1209
          - xs: pyopensn.xs.MultiGroupXS (required)
1210
              Cross-section object to assign to the specified blocks.
1211
    boundary_conditions: List[Dict], default=[]
1212
        A list containing tables for each boundary specification. Each dictionary supports:
1213
          - name: str (required)
1214
              Boundary name that identifies the specific boundary.
1215
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
1216
              Boundary type specification.
1217
          - group_strength: List[float], optional
1218
              Required when ``type='isotropic'``. Isotropic strength per group.
1219
          - start_time: float, optional
1220
              Active start time for isotropic boundaries only. Defaults to -infinity.
1221
          - end_time: float, optional
1222
              Active end time for isotropic boundaries only. Defaults to infinity.
1223
          - function: AngularFluxFunction, optional
1224
              Required when ``type='arbitrary'`` unless ``time_function`` is supplied. Callable
1225
              that returns incoming angular flux from group and direction.
1226
          - time_function: AngularFluxTimeFunction, optional
1227
              Required when ``type='arbitrary'`` unless ``function`` is supplied. Callable that
1228
              returns incoming angular flux from group, direction, and time.
1229
        Isotropic boundaries may use ``start_time``/``end_time`` for simple on/off behavior.
1230
        Arbitrary boundaries must specify exactly one of ``function`` or ``time_function``; use
1231
        ``time_function`` for time-dependent arbitrary inflow and handle any active window inside
1232
        that callback.
1233
    point_sources: List[pyopensn.source.PointSource], default=[]
1234
        A list of point sources.
1235
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
1236
        A list of volumetric sources.
1237
    options : dict, optional
1238
        A block of optional configuration parameters applied at problem creation, including:
1239
          - max_mpi_message_size: int, default=32768
1240
          - restart_writes_enabled: bool, default=False
1241
          - write_delayed_psi_to_restart: bool, default=True
1242
          - read_restart_path: str, default=''
1243
          - write_restart_path: str, default=''
1244
          - write_restart_time_interval: int, default=0
1245
          - use_precursors: bool, default=True
1246
            Enable delayed-neutron precursor treatment. This remains active across later
1247
            ``SetXSMap`` calls, even if the current map temporarily has no precursor-bearing
1248
            material. When XS maps change, precursor concentrations are remapped by cell and
1249
            precursor-family index; newly introduced families start at zero and removed families
1250
            are discarded.
1251
          - use_source_moments: bool, default=False
1252
          - save_angular_flux: bool, default=False
1253
            Store angular flux state (`psi`) for transient mode, angular-flux
1254
            field functions, and angular-flux I/O.
1255
          - verbose_inner_iterations: bool, default=True
1256
            Print inner iteration details, including WGS and AGS iterations.
1257
          - verbose_outer_iterations: bool, default=True
1258
            Print outer solver progress, including PI/NLKE iterations and transient steps.
1259
          - max_ags_iterations: int, default=100
1260
          - ags_tolerance: float, default=1.0e-6
1261
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
1262
          - power_default_kappa: float, default=3.20435e-11
1263
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
1264
          - field_function_prefix: str, default=''
1265
    sweep_type : str, optional
1266
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
1267
        If ``time_dependent=True``, ``options.save_angular_flux=True`` is required.
1268
    )"
1269
  );
1270
}
714✔
1271

1272
// Wrap steady-state solver
1273
void
1274
WrapSteadyState(py::module& slv)
714✔
1275
{
1276
  const auto balance_table_to_dict = [](const BalanceTable& table)
733✔
1277
  {
1278
    py::dict values;
19✔
1279
    values["absorption_rate"] = table.absorption_rate;
19✔
1280
    values["production_rate"] = table.production_rate;
19✔
1281
    values["inflow_rate"] = table.inflow_rate;
19✔
1282
    values["outflow_rate"] = table.outflow_rate;
19✔
1283
    values["balance"] = table.balance;
19✔
1284
    if (table.initial_inventory.has_value())
19✔
1285
      values["initial_inventory"] = table.initial_inventory.value();
×
1286
    if (table.final_inventory.has_value())
19✔
1287
      values["final_inventory"] = table.final_inventory.value();
×
1288
    if (table.predicted_inventory_change.has_value())
19✔
1289
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1290
    if (table.actual_inventory_change.has_value())
19✔
1291
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1292
    if (table.inventory_residual.has_value())
19✔
1293
      values["inventory_residual"] = table.inventory_residual.value();
×
1294
    return values;
19✔
1295
  };
×
1296

1297
  // clang-format off
1298
  // steady state solver
1299
  auto steady_state_solver = py::class_<SteadyStateSourceSolver, std::shared_ptr<SteadyStateSourceSolver>,
714✔
1300
                                        Solver>(
1301
    slv,
1302
    "SteadyStateSourceSolver",
1303
    R"(
1304
    Steady state solver.
1305

1306
    Wrapper of :cpp:class:`opensn::SteadyStateSourceSolver`.
1307
    )"
1308
  );
714✔
1309
  steady_state_solver.def(
1,428✔
1310
    py::init(
714✔
1311
      [](py::kwargs& params)
624✔
1312
      {
1313
        return SteadyStateSourceSolver::Create(kwargs_to_param_block(params));
624✔
1314
      }
1315
    ),
1316
    R"(
1317
    Construct a steady state solver.
1318

1319
    Parameters
1320
    ----------
1321
    problem : pyopensn.solver.DiscreteOrdinatesProblem
1322
        Existing discrete ordinates problem instance.
1323

1324
    Notes
1325
    -----
1326
    If the problem was constructed with ``options={'read_restart_path': ...}``,
1327
    restart data is read during :meth:`Initialize`. If it was constructed with
1328
    ``options={'restart_writes_enabled': True, ...}``, a restart dump is written
1329
    after :meth:`Execute` completes.
1330
    )"
1331
  );
1332
  steady_state_solver.def(
714✔
1333
    "ComputeBalanceTable",
1334
    [balance_table_to_dict](const SteadyStateSourceSolver& self)
714✔
1335
    {
1336
      return balance_table_to_dict(self.ComputeBalanceTable());
19✔
1337
    },
1338
    R"(
1339
    Compute and return the global balance table using the solver's normalization.
1340
    This is a collective operation and must be called on all ranks.
1341

1342
    Returns
1343
    -------
1344
    dict
1345
        Dictionary with the following entries:
1346

1347
        - ``absorption_rate``:
1348
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1349
          groups and the full domain.
1350
        - ``production_rate``:
1351
          Global volumetric production/source rate used by the solver,
1352
          approximately ``integral Q dV`` summed over groups and the full domain.
1353
        - ``inflow_rate``:
1354
          Global incoming boundary contribution integrated over incoming
1355
          angular flux on boundaries.
1356
        - ``outflow_rate``:
1357
          Global outgoing boundary contribution accumulated from face outflow
1358
          tallies.
1359
        - ``balance``:
1360
          Rate balance,
1361
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1362

1363
    Notes
1364
    -----
1365
    This solver applies no extra normalization to the balance table.
1366
    )"
1367
  );
1368
  // clang-format on
1369
}
714✔
1370

1371
// Wrap transient solver
1372
void
1373
WrapTransient(py::module& slv)
714✔
1374
{
1375
  const auto balance_table_to_dict = [](const BalanceTable& table)
714✔
1376
  {
1377
    py::dict values;
×
1378
    values["absorption_rate"] = table.absorption_rate;
×
1379
    values["production_rate"] = table.production_rate;
×
1380
    values["inflow_rate"] = table.inflow_rate;
×
1381
    values["outflow_rate"] = table.outflow_rate;
×
1382
    values["balance"] = table.balance;
×
1383
    if (table.initial_inventory.has_value())
×
1384
      values["initial_inventory"] = table.initial_inventory.value();
×
1385
    if (table.final_inventory.has_value())
×
1386
      values["final_inventory"] = table.final_inventory.value();
×
1387
    if (table.predicted_inventory_change.has_value())
×
1388
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1389
    if (table.actual_inventory_change.has_value())
×
1390
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1391
    if (table.inventory_residual.has_value())
×
1392
      values["inventory_residual"] = table.inventory_residual.value();
×
1393
    return values;
×
1394
  };
×
1395
  // clang-format off
1396
  auto transient_solver =
714✔
1397
    py::class_<TransientSolver, std::shared_ptr<TransientSolver>, Solver>(
1398
      slv,
1399
      "TransientSolver",
1400
      R"(
1401
      Transient solver.
1402

1403
      Wrapper of :cpp:class:`opensn::TransientSolver`.
1404
      )"
1405
    );
714✔
1406
  transient_solver.def(
1,428✔
1407
    py::init(
714✔
1408
      [](py::kwargs& params)
264✔
1409
      {
1410
        return TransientSolver::Create(kwargs_to_param_block(params));
264✔
1411
      }
1412
    ),
1413
    R"(
1414
    Construct a transient solver.
1415

1416
    Parameters
1417
    ----------
1418
    pyopensn.solver.DiscreteOrdinatesProblem : DiscreteOrdinatesProblem
1419
        Existing discrete ordinates problem instance.
1420
    dt : float, optional, default=2.0e-3
1421
        Time step size used during the simulation.
1422
    stop_time : float, optional, default=0.1
1423
        Simulation end time.
1424
    theta : float, optional, default=0.5
1425
        Time differencing scheme parameter.
1426
    initial_state : str, optional, default="existing"
1427
        Initial state for the transient solve. Allowed values: existing, zero.
1428
        In "zero" mode, scalar flux vectors are reset to zero.
1429
    verbose : bool, optional, default=True
1430
        Enable verbose logging.
1431

1432
    Notes
1433
    -----
1434
    The associated problem must have ``save_angular_flux=True`` enabled. This
1435
    is required for transient problems.
1436

1437
    If the problem was constructed with ``options={'read_restart_path': ...}``,
1438
    restart data is read during :meth:`Initialize`. If it was constructed with
1439
    ``options={'restart_writes_enabled': True, ...}``, timed restart dumps may
1440
    be written during :meth:`Execute` and a final restart dump is written when
1441
    execution completes.
1442
    )"
1443
  );
1444
  transient_solver.def(
714✔
1445
    "SetTimeStep",
1446
    &TransientSolver::SetTimeStep,
714✔
1447
    R"(
1448
    Set the timestep size used by :meth:`Advance`.
1449

1450
    Parameters
1451
    ----------
1452
    dt : float
1453
        New timestep size.
1454
    )");
1455
  transient_solver.def(
714✔
1456
    "SetTheta",
1457
    &TransientSolver::SetTheta,
714✔
1458
    R"(
1459
    Set the theta parameter used by :meth:`Advance`.
1460

1461
    Parameters
1462
    ----------
1463
    theta : float
1464
        Theta value between 1.0e-16 and 1.
1465
    )");
1466
  transient_solver.def(
714✔
1467
    "Advance",
1468
    &TransientSolver::Advance,
714✔
1469
    R"(
1470
    Advance the solver by a single timestep.
1471

1472
    Notes
1473
    -----
1474
    You must call :meth:`Initialize` before calling :meth:`Advance` or
1475
    :meth:`Execute`.
1476
    )");
1477
  transient_solver.def(
714✔
1478
    "SetPreAdvanceCallback",
1479
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
714✔
1480
      &TransientSolver::SetPreAdvanceCallback),
1481
    R"(
1482
    Register a callback that runs before each advance within :meth:`Execute`.
1483

1484
    Parameters
1485
    ----------
1486
    callback : Optional[Callable[[], None]]
1487
        Function invoked before the solver advances a timestep. Pass None to clear.
1488
        If the callback modifies the timestep, the new value is used for the
1489
        upcoming step.
1490
    )");
1491
  transient_solver.def(
714✔
1492
    "SetPreAdvanceCallback",
1493
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
714✔
1494
      &TransientSolver::SetPreAdvanceCallback),
1495
    "Clear the PreAdvance callback by passing None.");
1496
  transient_solver.def(
714✔
1497
    "SetPostAdvanceCallback",
1498
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
714✔
1499
      &TransientSolver::SetPostAdvanceCallback),
1500
    R"(
1501
    Register a callback that runs after each advance within :meth:`Execute`.
1502

1503
    Parameters
1504
    ----------
1505
    callback : Optional[Callable[[], None]]
1506
        Function invoked after the solver advances a timestep. Pass None to clear.
1507
    )");
1508
  transient_solver.def(
714✔
1509
    "SetPostAdvanceCallback",
1510
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
714✔
1511
      &TransientSolver::SetPostAdvanceCallback),
1512
    "Clear the PostAdvance callback by passing None.");
1513
  transient_solver.def(
714✔
1514
    "ComputeBalanceTable",
1515
    [balance_table_to_dict](const TransientSolver& self)
714✔
1516
    {
1517
      return balance_table_to_dict(self.ComputeBalanceTable());
×
1518
    },
1519
    R"(
1520
    Compute and return the global balance table using the solver's normalization.
1521
    This is a collective operation and must be called on all ranks.
1522

1523
    Returns
1524
    -------
1525
    dict
1526
        Dictionary with the following entries:
1527

1528
        - ``absorption_rate``:
1529
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1530
          groups and the full domain.
1531
        - ``production_rate``:
1532
          Global volumetric production/source rate used by the solver,
1533
          approximately ``integral Q dV`` summed over groups and the full domain.
1534
        - ``inflow_rate``:
1535
          Global incoming boundary contribution integrated over incoming
1536
          angular flux on boundaries.
1537
        - ``outflow_rate``:
1538
          Global outgoing boundary contribution accumulated from face outflow
1539
          tallies.
1540
        - ``balance``:
1541
          Rate balance,
1542
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1543
        - ``initial_inventory``:
1544
          Total particle inventory at the start of the timestep, computed as
1545
          ``integral (1 / v_g) * phi_old dV`` summed over groups and the full domain.
1546
        - ``final_inventory``:
1547
          Total particle inventory at the end of the timestep, computed as
1548
          ``integral (1 / v_g) * phi_new dV`` summed over groups and the full domain.
1549
        - ``predicted_inventory_change``:
1550
          Inventory change predicted by the current timestep balance, computed as
1551
          ``dt * balance``.
1552
        - ``actual_inventory_change``:
1553
          Measured change in total particle inventory over the timestep, computed as
1554
          ``final_inventory - initial_inventory``.
1555
        - ``inventory_residual``:
1556
          Mismatch between the measured and predicted timestep inventory
1557
          changes, computed as
1558
          ``actual_inventory_change - predicted_inventory_change``.
1559

1560
    Notes
1561
    -----
1562
    This solver applies no extra normalization to the balance table.
1563

1564
    The transient inventory terms currently use the end-of-step rate balance to
1565
    estimate the timestep inventory change.
1566
    )"
1567
  );
1568
  slv.attr("BackwardEuler") = 1.0;
714✔
1569
  slv.attr("CrankNicolson") = 0.5;
1,428✔
1570
  // clang-format on
1571
}
714✔
1572

1573
// Wrap non-linear k-eigen solver
1574
void
1575
WrapNLKEigen(py::module& slv)
714✔
1576
{
1577
  const auto balance_table_to_dict = [](const BalanceTable& table)
714✔
1578
  {
1579
    py::dict values;
×
1580
    values["absorption_rate"] = table.absorption_rate;
×
1581
    values["production_rate"] = table.production_rate;
×
1582
    values["inflow_rate"] = table.inflow_rate;
×
1583
    values["outflow_rate"] = table.outflow_rate;
×
1584
    values["balance"] = table.balance;
×
1585
    if (table.initial_inventory.has_value())
×
1586
      values["initial_inventory"] = table.initial_inventory.value();
×
1587
    if (table.final_inventory.has_value())
×
1588
      values["final_inventory"] = table.final_inventory.value();
×
1589
    if (table.predicted_inventory_change.has_value())
×
1590
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1591
    if (table.actual_inventory_change.has_value())
×
1592
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1593
    if (table.inventory_residual.has_value())
×
1594
      values["inventory_residual"] = table.inventory_residual.value();
×
1595
    return values;
×
1596
  };
×
1597
  // clang-format off
1598
  // non-linear k-eigen solver
1599
  auto non_linear_k_eigen_solver = py::class_<NonLinearKEigenSolver, std::shared_ptr<NonLinearKEigenSolver>,
714✔
1600
                                              Solver>(
1601
    slv,
1602
    "NonLinearKEigenSolver",
1603
    R"(
1604
    Non-linear k-eigenvalue solver.
1605

1606
    Wrapper of :cpp:class:`opensn::NonLinearKEigenSolver`.
1607
    )"
1608
  );
714✔
1609
  non_linear_k_eigen_solver.def(
1,428✔
1610
    py::init(
714✔
1611
      [](py::kwargs& params)
41✔
1612
      {
1613
        return NonLinearKEigenSolver::Create(kwargs_to_param_block(params));
41✔
1614
      }
1615
        ),
1616
    R"(
1617
    Construct a non-linear k-eigenvalue solver.
1618

1619
    Parameters
1620
    ----------
1621
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1622
        Existing discrete ordinates problem instance.
1623
    nl_abs_tol: float, default=1.0e-8
1624
        Non-linear absolute tolerance.
1625
    nl_rel_tol: float, default=1.0e-8
1626
        Non-linear relative tolerance.
1627
    nl_sol_tol: float, default=1.0e-50
1628
        Non-linear solution tolerance.
1629
    nl_max_its: int, default=50
1630
        Non-linear algorithm maximum iterations.
1631
    l_abs_tol: float, default=1.0e-8
1632
        Linear absolute tolerance.
1633
    l_rel_tol: float, default=1.0e-8
1634
        Linear relative tolerance.
1635
    l_div_tol: float, default=1.0e6
1636
        Linear divergence tolerance.
1637
    l_max_its: int, default=50
1638
        Linear algorithm maximum iterations.
1639
    l_gmres_restart_intvl: int, default=30
1640
        GMRES restart interval.
1641
    l_gmres_breakdown_tol: float, default=1.0e6
1642
        GMRES breakdown tolerance.
1643
    reset_phi0: bool, default=True
1644
        If true, reinitializes scalar fluxes to 1.0.
1645
    num_initial_power_iterations: int, default=0
1646
        Number of initial power iterations before the non-linear solve.
1647
    )"
1648
  );
1649
  non_linear_k_eigen_solver.def(
714✔
1650
    "GetEigenvalue",
1651
    &NonLinearKEigenSolver::GetEigenvalue,
714✔
1652
    R"(
1653
    Return the current k-eigenvalue.
1654
    )"
1655
  );
1656
  non_linear_k_eigen_solver.def(
714✔
1657
    "ComputeBalanceTable",
1658
    [balance_table_to_dict](const NonLinearKEigenSolver& self)
714✔
1659
    {
1660
      return balance_table_to_dict(self.ComputeBalanceTable());
×
1661
    },
1662
    R"(
1663
    Compute and return the global balance table using the solver's normalization.
1664
    This is a collective operation and must be called on all ranks.
1665

1666
    Returns
1667
    -------
1668
    dict
1669
        Dictionary with the following entries:
1670

1671
        - ``absorption_rate``:
1672
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1673
          groups and the full domain.
1674
        - ``production_rate``:
1675
          Global volumetric production/source rate used by the solver,
1676
          approximately ``integral Q dV`` summed over groups and the full domain.
1677
        - ``inflow_rate``:
1678
          Global incoming boundary contribution integrated over incoming
1679
          angular flux on boundaries.
1680
        - ``outflow_rate``:
1681
          Global outgoing boundary contribution accumulated from face outflow
1682
          tallies.
1683
        - ``balance``:
1684
          Rate balance,
1685
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1686

1687
    Notes
1688
    -----
1689
    For k-eigenvalue balance reporting, this solver scales the production term by
1690
    ``1 / k_eff`` before forming both ``production_rate`` and ``balance``.
1691
    )"
1692
  );
1693
  // clang-format on
1694
}
714✔
1695

1696
// Wrap power iteration solvers
1697
void
1698
WrapPIteration(py::module& slv)
714✔
1699
{
1700
  const auto balance_table_to_dict = [](const BalanceTable& table)
718✔
1701
  {
1702
    py::dict values;
4✔
1703
    values["absorption_rate"] = table.absorption_rate;
4✔
1704
    values["production_rate"] = table.production_rate;
4✔
1705
    values["inflow_rate"] = table.inflow_rate;
4✔
1706
    values["outflow_rate"] = table.outflow_rate;
4✔
1707
    values["balance"] = table.balance;
4✔
1708
    if (table.initial_inventory.has_value())
4✔
1709
      values["initial_inventory"] = table.initial_inventory.value();
×
1710
    if (table.final_inventory.has_value())
4✔
1711
      values["final_inventory"] = table.final_inventory.value();
×
1712
    if (table.predicted_inventory_change.has_value())
4✔
1713
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1714
    if (table.actual_inventory_change.has_value())
4✔
1715
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1716
    if (table.inventory_residual.has_value())
4✔
1717
      values["inventory_residual"] = table.inventory_residual.value();
×
1718
    return values;
4✔
1719
  };
×
1720
  // clang-format off
1721
  // power iteration k-eigen solver
1722
  auto pi_k_eigen_solver = py::class_<PowerIterationKEigenSolver, std::shared_ptr<PowerIterationKEigenSolver>,
714✔
1723
                                      Solver>(
1724
    slv,
1725
    "PowerIterationKEigenSolver",
1726
    R"(
1727
    Power iteration k-eigenvalue solver.
1728

1729
    Wrapper of :cpp:class:`opensn::PowerIterationKEigenSolver`.
1730
    )"
1731
  );
714✔
1732
  pi_k_eigen_solver.def(
1,428✔
1733
    py::init(
714✔
1734
      [](py::kwargs& params)
165✔
1735
      {
1736
        return PowerIterationKEigenSolver::Create(kwargs_to_param_block(params));
165✔
1737
      }
1738
    ),
1739
    R"(
1740
    Construct a power iteration k-eigen solver.
1741

1742
    Parameters
1743
    ----------
1744
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1745
        Existing DiscreteOrdinatesProblem instance.
1746
    acceleration: pyopensn.solver.DiscreteOrdinatesKEigenAcceleration
1747
        Optional DiscreteOrdinatesKEigenAcceleration instance for acceleration.
1748
    max_iters: int, default = 1000
1749
        Maximum power iterations allowed.
1750
    k_tol: float, default = 1.0e-10
1751
        Tolerance on the k-eigenvalue.
1752
    reset_solution: bool, default=True
1753
        If true, initialize flux moments to 1.0.
1754
    reset_phi0: bool, default=True
1755
        If true, reinitializes scalar fluxes to 1.0.
1756

1757
    Notes
1758
    -----
1759
    If the problem was constructed with ``options={'read_restart_path': ...}``,
1760
    restart data is read during :meth:`Initialize`. If it was constructed with
1761
    ``options={'restart_writes_enabled': True, ...}``, timed restart dumps may
1762
    be written during the outer iteration loop and a final restart dump is
1763
    written when execution completes.
1764
    )"
1765
  );
1766
  pi_k_eigen_solver.def(
714✔
1767
    "GetEigenvalue",
1768
    &PowerIterationKEigenSolver::GetEigenvalue,
714✔
1769
    R"(
1770
    Return the current k-eigenvalue.
1771
    )"
1772
  );
1773
  pi_k_eigen_solver.def(
714✔
1774
    "ComputeBalanceTable",
1775
    [balance_table_to_dict](const PowerIterationKEigenSolver& self)
714✔
1776
    {
1777
      return balance_table_to_dict(self.ComputeBalanceTable());
4✔
1778
    },
1779
    R"(
1780
    Compute and return the global balance table using the solver's normalization.
1781
    This is a collective operation and must be called on all ranks.
1782

1783
    Returns
1784
    -------
1785
    dict
1786
        Dictionary with the following entries:
1787

1788
        - ``absorption_rate``:
1789
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1790
          groups and the full domain.
1791
        - ``production_rate``:
1792
          Global volumetric production/source rate used by the solver,
1793
          approximately ``integral Q dV`` summed over groups and the full domain.
1794
        - ``inflow_rate``:
1795
          Global incoming boundary contribution integrated over incoming
1796
          angular flux on boundaries.
1797
        - ``outflow_rate``:
1798
          Global outgoing boundary contribution accumulated from face outflow
1799
          tallies.
1800
        - ``balance``:
1801
          Rate balance,
1802
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1803

1804
    Notes
1805
    -----
1806
    For k-eigenvalue balance reporting, this solver scales the production term by
1807
    ``1 / k_eff`` before forming both ``production_rate`` and ``balance``.
1808
    )"
1809
  );
1810
  // clang-format on
1811
}
714✔
1812

1813
// Wrap LBS solver
1814
void
1815
WrapDiscreteOrdinatesKEigenAcceleration(py::module& slv)
714✔
1816
{
1817
  // clang-format off
1818
  // discrete ordinates k-eigen acceleration base
1819
  auto acceleration = py::class_<DiscreteOrdinatesKEigenAcceleration,
714✔
1820
                                 std::shared_ptr<DiscreteOrdinatesKEigenAcceleration>>(
1821
    slv,
1822
    "DiscreteOrdinatesKEigenAcceleration",
1823
    R"(
1824
    Base class for discrete ordinates k-eigenvalue acceleration methods.
1825

1826
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesKEigenAcceleration`.
1827
    )"
1828
  );
714✔
1829
  // SCDSA acceleration
1830
  auto scdsa_acceleration = py::class_<SCDSAAcceleration,
714✔
1831
                                       std::shared_ptr<SCDSAAcceleration>,
1832
                                       DiscreteOrdinatesKEigenAcceleration>(
1833
    slv,
1834
    "SCDSAAcceleration",
1835
    R"(
1836
    Construct an SCDSA accelerator for the power iteration k-eigenvalue solver.
1837

1838
    Wrapper of :cpp:class:`opensn::SCDSAAcceleration`.
1839
    )"
1840
  );
714✔
1841
  scdsa_acceleration.def(
1,428✔
1842
    py::init(
714✔
1843
      [](py::kwargs& params)
8✔
1844
      {
1845
        return SCDSAAcceleration::Create(kwargs_to_param_block(params));
8✔
1846
      }
1847
    ),
1848
    R"(
1849
    SCDSA acceleration for the power iteration k-eigenvalue solver.
1850

1851
    Parameters
1852
    ----------
1853
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1854
        Existing DiscreteOrdinatesProblem instance.
1855
    l_abs_tol: float, defauilt=1.0e-10
1856
        Absolute residual tolerance.
1857
    max_iters: int, default=100
1858
        Maximum allowable iterations.
1859
    verbose: bool, default=False
1860
        If true, enables verbose output.
1861
    petsc_options: str, default="ssss"
1862
        Additional PETSc options.
1863
    pi_max_its: int, default=50
1864
        Maximum allowable iterations for inner power iterations.
1865
    pi_k_tol: float, default=1.0e-10
1866
        k-eigenvalue tolerance for the inner power iterations.
1867
    sdm: str, default="pwld"
1868
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1869
            - 'pwld' : Piecewise Linear Discontinuous
1870
            - 'pwlc' : Piecewise Linear Continuous
1871
    )"
1872
  );
1873
  // SMM acceleration
1874
  auto smm_acceleration = py::class_<SMMAcceleration,
714✔
1875
                                     std::shared_ptr<SMMAcceleration>,
1876
                                     DiscreteOrdinatesKEigenAcceleration>(
1877
    slv,
1878
    "SMMAcceleration",
1879
    R"(
1880
    Construct an SMM accelerator for the power iteration k-eigenvalue solver.
1881

1882
    Wrapper of :cpp:class:`opensn::SMMAcceleration`.
1883
    )"
1884
  );
714✔
1885
  smm_acceleration.def(
1,428✔
1886
    py::init(
714✔
1887
      [](py::kwargs& params)
4✔
1888
      {
1889
        return SMMAcceleration::Create(kwargs_to_param_block(params));
4✔
1890
      }
1891
    ),
1892
    R"(
1893
    SMM acceleration for the power iteration k-eigenvalue solver.
1894

1895
    Warnings
1896
    --------
1897
       SMM acceleration is **experimental** and should be used with caution!
1898
       SMM accleration only supports problems with isotropic scattering.
1899

1900
    Parameters
1901
    ----------
1902
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1903
        Existing DiscreteOrdinatesProblem instance.
1904
    l_abs_tol: float, defauilt=1.0e-10
1905
        Absolute residual tolerance.
1906
    max_iters: int, default=100
1907
        Maximum allowable iterations.
1908
    verbose: bool, default=False
1909
        If true, enables verbose output.
1910
    petsc_options: str, default="ssss"
1911
        Additional PETSc options.
1912
    pi_max_its: int, default=50
1913
        Maximum allowable iterations for inner power iterations.
1914
    pi_k_tol: float, default=1.0e-10
1915
        k-eigenvalue tolerance for the inner power iterations.
1916
    sdm: str, default="pwld"
1917
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1918
            - 'pwld' : Piecewise Linear Discontinuous
1919
            - 'pwlc' : Piecewise Linear Continuous
1920
    )"
1921
  );
1922
  // clang-format on
1923
}
714✔
1924

1925
// Wrap the solver components of OpenSn
1926
void
1927
py_solver(py::module& pyopensn)
72✔
1928
{
1929
  py::module slv = pyopensn.def_submodule("solver", "Solver module.");
72✔
1930
  WrapProblem(slv);
72✔
1931
  WrapSolver(slv);
72✔
1932
  WrapLBS(slv);
72✔
1933
  WrapSteadyState(slv);
72✔
1934
  WrapTransient(slv);
72✔
1935
  WrapNLKEigen(slv);
72✔
1936
  WrapDiscreteOrdinatesKEigenAcceleration(slv);
72✔
1937
  WrapPIteration(slv);
72✔
1938
}
72✔
1939

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