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

Open-Sn / opensn / 23971600730

02 Apr 2026 07:54PM UTC coverage: 75.04%. Remained the same
23971600730

push

github

web-flow
Merge pull request #1010 from wdhawkins/field_function_refactor

Refactoring field function implementation

107 of 158 new or added lines in 3 files covered. (67.72%)

258 existing lines in 7 files now uncovered.

20988 of 27969 relevant lines covered (75.04%)

66023505.05 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/discrete_ordinates_compute.h"
15
#include "modules/linear_boltzmann_solvers/solvers/transient_solver.h"
16
#include "modules/linear_boltzmann_solvers/solvers/steady_state_solver.h"
17
#include "modules/linear_boltzmann_solvers/solvers/nl_keigen_solver.h"
18
#include "modules/linear_boltzmann_solvers/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/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)
661✔
38
{
39
  // clang-format off
40
  // problem base
41
  auto problem = py::class_<Problem, std::shared_ptr<Problem>>(
661✔
42
    slv,
43
    "Problem",
44
    R"(
45
    Base class for all problems.
46

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

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

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

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

98
    Wrapper of :cpp:class:`opensn::LBSProblem`.
99
    )"
100
  );
661✔
101
  lbs_problem.def(
661✔
102
    "GetScalarFluxFieldFunction",
103
    [](LBSProblem& self, bool only_scalar_flux)
661✔
104
    {
105
      py::list field_function_list_per_group;
682✔
106
      for (unsigned int group = 0; group < self.GetNumGroups(); group++)
23,787✔
107
      {
108
        if (only_scalar_flux)
23,105✔
109
        {
110
          field_function_list_per_group.append(self.CreateScalarFluxFieldFunction(group, 0));
28,248✔
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;
682✔
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. The solver does
145
    not retain or continuously update scalar-flux field functions between calls.
146

147
    Calling ``GetScalarFluxFieldFunction(only_scalar_flux=False)`` creates all requested
148
    moments from the current ``phi`` iterate at the time of the call.
149

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

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

172
    Notes
173
    -----
174
    The returned field function is created on demand from the current scalar-flux iterate.
175
    For ordinary XS names this computes ``sum_g xs[g] * phi_g`` at each node.
176

177
    If ``xs_name == "power"``, the same power-generation formula used elsewhere by the solver
178
    is applied on demand.
179

180
    If ``power_normalization_target > 0``, the returned field function is scaled using the power
181
    implied by the current scalar flux. This scaling affects only the returned field function;
182
    it does not mutate the solver's internal ``phi`` vectors.
183
    )",
184
    py::arg("name"),
1,322✔
185
    py::arg("xs_name"),
661✔
186
    py::arg("power_normalization_target") = -1.0
661✔
187
  );
188
  lbs_problem.def(
661✔
189
    "GetTime",
190
    &LBSProblem::GetTime,
661✔
191
    R"(
192
    Get the current simulation time in seconds.
193
    )"
194
  );
195
  lbs_problem.def(
661✔
196
    "GetTimeStep",
197
    &LBSProblem::GetTimeStep,
661✔
198
    R"(
199
    Get the current timestep size.
200
    )"
201
  );
202
  lbs_problem.def(
661✔
203
    "ComputeFissionRate",
204
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
661✔
205
    {
UNCOV
206
      const std::vector<double>* phi_ptr = nullptr;
×
UNCOV
207
      if (scalar_flux_iterate == "old")
×
208
      {
UNCOV
209
        phi_ptr = &self.GetPhiOldLocal();
×
210
      }
UNCOV
211
      else if (scalar_flux_iterate == "new")
×
212
      {
UNCOV
213
        phi_ptr = &self.GetPhiNewLocal();
×
214
      }
215
      else
216
      {
UNCOV
217
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
218
      }
UNCOV
219
      return ComputeFissionRate(self, *phi_ptr);
×
220
    },
221
    R"(
222
    Computes the total fission rate.
223

224
    Parameters
225
    ----------
226
    scalar_flux_iterate : {'old', 'new'}
227
        Specifies which scalar flux vector to use in the calculation.
228
            - 'old': Use the previous scalar flux iterate.
229
            - 'new': Use the current scalar flux iterate.
230

231
    Returns
232
    -------
233
    float
234
        The total fission rate.
235

236
    Raises
237
    ------
238
    ValueError
239
        If `scalar_flux_iterate` is not 'old' or 'new'.
240
    )",
241
    py::arg("scalar_flux_iterate")
661✔
242
  );
243
  lbs_problem.def(
661✔
244
    "ComputeFissionProduction",
245
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
1,457✔
246
    {
247
      const std::vector<double>* phi_ptr = nullptr;
796✔
248
      if (scalar_flux_iterate == "old")
796✔
249
      {
250
        phi_ptr = &self.GetPhiOldLocal();
44✔
251
      }
252
      else if (scalar_flux_iterate == "new")
752✔
253
      {
254
        phi_ptr = &self.GetPhiNewLocal();
752✔
255
      }
256
      else
257
      {
UNCOV
258
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
259
      }
260
      return ComputeFissionProduction(self, *phi_ptr);
796✔
261
    },
262
    R"(
263
    Computes the total fission production (nu*fission).
264

265
    Parameters
266
    ----------
267
    scalar_flux_iterate : {'old', 'new'}
268
        Specifies which scalar flux vector to use in the calculation.
269
            - 'old': Use the previous scalar flux iterate.
270
            - 'new': Use the current scalar flux iterate.
271

272
    Returns
273
    -------
274
    float
275
        The total fission production.
276

277
    Raises
278
    ------
279
    ValueError
280
        If `scalar_flux_iterate` is not 'old' or 'new'.
281
    )",
282
    py::arg("scalar_flux_iterate")
661✔
283
  );
284
  lbs_problem.def(
661✔
285
    "GetPhiOldLocal",
286
    [](LBSProblem& self)
709✔
287
    {
288
      return convert_vector(self.GetPhiOldLocal());
48✔
289
    },
290
    R"(
291
    Get the previous scalar flux iterate (local vector).
292

293
    Returns
294
    -------
295
    memoryview
296
        Memory view of the local old scalar flux vector.
297
    )"
298
  );
299
  lbs_problem.def(
661✔
300
    "GetPhiNewLocal",
301
    [](LBSProblem& self)
717✔
302
    {
303
      return convert_vector(self.GetPhiNewLocal());
56✔
304
    },
305
    R"(
306
    Get the current scalar flux iterate (local vector).
307

308
    Returns
309
    -------
310
    memoryview
311
        Memory view of the local new scalar flux vector.
312
    )"
313
  );
314
  lbs_problem.def(
661✔
315
    "WriteFluxMoments",
316
    [](LBSProblem& self, const std::string& file_base)
693✔
317
    {
318
      LBSSolverIO::WriteFluxMoments(self, file_base);
32✔
319
    },
320
    R"(
321
    Write flux moments to file.
322

323
    Parameters
324
    ----------
325
    file_base: str
326
        File basename.
327
    )",
328
    py::arg("file_base")
661✔
329
  );
330
  lbs_problem.def(
661✔
331
    "CreateAndWriteSourceMoments",
332
    [](LBSProblem& self, const std::string& file_base)
665✔
333
    {
334
      std::vector<double> source_moments = self.MakeSourceMomentsFromPhi();
4✔
335
      LBSSolverIO::WriteFluxMoments(self, file_base, source_moments);
4✔
336
    },
4✔
337
    R"(
338
    Write source moments from latest flux iterate to file.
339

340
    Parameters
341
    ----------
342
    file_base: str
343
        File basename.
344
    )",
345
    py::arg("file_base")
661✔
346
  );
347
  lbs_problem.def(
661✔
348
    "ReadFluxMomentsAndMakeSourceMoments",
349
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
661✔
350
    {
UNCOV
351
      auto ext_src_moments = self.GetExtSrcMomentsLocal();
×
UNCOV
352
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, ext_src_moments);
×
UNCOV
353
      self.SetExtSrcMomentsFrom(ext_src_moments);
×
UNCOV
354
      log.Log() << "Making source moments from flux file.";
×
UNCOV
355
      const auto temp_phi = self.GetPhiOldLocal();
×
UNCOV
356
      self.SetPhiOldFrom(self.GetExtSrcMomentsLocal());
×
UNCOV
357
      self.SetExtSrcMomentsFrom(self.MakeSourceMomentsFromPhi());
×
UNCOV
358
      self.SetPhiOldFrom(temp_phi);
×
UNCOV
359
    },
×
360
    R"(
361
    Read flux moments and compute corresponding source moments.
362

363
    Parameters
364
    ----------
365
    file_base: str
366
        File basename.
367
    single_file_flag: bool
368
        True if all flux moments are in a single file.
369
    )",
370
    py::arg("file_base"),
1,322✔
371
    py::arg("single_file_flag")
661✔
372
  );
373
  lbs_problem.def(
661✔
374
    "ReadSourceMoments",
375
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
665✔
376
    {
377
      auto ext_src_moments = self.GetExtSrcMomentsLocal();
4✔
378
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, ext_src_moments);
4✔
379
      self.SetExtSrcMomentsFrom(ext_src_moments);
4✔
380
    },
4✔
381
    R"(
382
    Read source moments from file.
383

384
    Parameters
385
    ----------
386
    file_base: str
387
        File basename.
388
    single_file_flag: bool
389
        True if all source moments are in a single file.
390
    )",
391
    py::arg("file_base"),
1,322✔
392
    py::arg("single_file_flag")
661✔
393
  );
394
  lbs_problem.def(
661✔
395
    "ReadFluxMoments",
396
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
661✔
397
    {
UNCOV
398
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag);
×
399
    },
400
    R"(
401
    Read flux moment data.
402

403
    Parameters
404
    ----------
405
    file_base: str
406
        File basename.
407
    single_file_flag: bool
408
        True if all flux moments are in a single file.
409
    )",
410
    py::arg("file_base"),
1,322✔
411
    py::arg("single_file_flag")
661✔
412
  );
413
  lbs_problem.def(
661✔
414
    "WriteAngularFluxes",
415
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
665✔
416
    {
417
      DiscreteOrdinatesProblemIO::WriteAngularFluxes(self, file_base);
4✔
418
    },
419
    R"(
420
    Write angular flux data to file.
421

422
    Parameters
423
    ----------
424
    file_base: str
425
        File basename.
426
    )",
427
    py::arg("file_base")
661✔
428
  );
429
  lbs_problem.def(
661✔
430
    "ReadAngularFluxes",
431
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
665✔
432
    {
433
      DiscreteOrdinatesProblemIO::ReadAngularFluxes(self, file_base);
4✔
434
    },
435
    R"(
436
    Read angular fluxes from file.
437

438
    Parameters
439
    ----------
440
    file_base: str
441
        File basename.
442
    )",
443
    py::arg("file_base")
661✔
444
  );
445
  lbs_problem.def(
661✔
446
    "SetPointSources",
447
    [](LBSProblem& self, py::kwargs& params)
661✔
448
    {
UNCOV
449
      for (auto [key, value] : params)
×
450
      {
UNCOV
451
        auto c_key = key.cast<std::string>();
×
UNCOV
452
        if (c_key == "clear_point_sources")
×
UNCOV
453
          self.ClearPointSources();
×
UNCOV
454
        else if (c_key == "point_sources")
×
455
        {
UNCOV
456
          auto sources = value.cast<py::list>();
×
UNCOV
457
          for (auto source : sources)
×
458
          {
UNCOV
459
            self.AddPointSource(source.cast<std::shared_ptr<PointSource>>());
×
460
          }
UNCOV
461
        }
×
462
        else
UNCOV
463
          throw std::runtime_error("Invalid argument provided to SetPointSources.\n");
×
UNCOV
464
      }
×
UNCOV
465
    },
×
466
    R"(
467
    Set or clear point sources.
468

469
    Parameters
470
    ----------
471
    clear_point_sources: bool, default=False
472
        If true, all current the point sources of the problem are deleted.
473
    point_sources: List[pyopensn.source.PointSource]
474
        List of new point sources to be added to the problem.
475
    )"
476
  );
477
  lbs_problem.def(
661✔
478
    "SetVolumetricSources",
479
    [](LBSProblem& self, py::kwargs& params)
697✔
480
    {
481
      for (auto [key, value] : params)
108✔
482
      {
483
        auto c_key = key.cast<std::string>();
36✔
484
        if (c_key == "clear_volumetric_sources")
36✔
485
          self.ClearVolumetricSources();
12✔
486
        else if (c_key == "volumetric_sources")
24✔
487
        {
488
          auto sources = value.cast<py::list>();
24✔
489
          for (auto source : sources)
72✔
490
          {
491
            self.AddVolumetricSource(source.cast<std::shared_ptr<VolumetricSource>>());
48✔
492
          }
493
        }
24✔
494
        else
UNCOV
495
          throw std::runtime_error("Invalid argument provided to SetVolumetricSources.\n");
×
496
      }
36✔
497
    },
36✔
498
    R"(
499
    Set or clear volumetric sources.
500

501
    Parameters
502
    ----------
503
    clear_volumetric_sources: bool, default=False
504
        If true, all current the volumetric sources of the problem are deleted.
505
    volumetric_sources: List[pyopensn.source.VolumetricSource]
506
        List of new volumetric sources to be added to the problem.
507
    )"
508
  );
509
  lbs_problem.def(
661✔
510
    "SetXSMap",
511
    [](LBSProblem& self, py::kwargs& params)
833✔
512
    {
513
      BlockID2XSMap xs_map;
172✔
514
      for (auto [key, value] : params)
516✔
515
      {
516
        auto c_key = key.cast<std::string>();
172✔
517
        if (c_key == "xs_map")
172✔
518
        {
519
          auto xs_entries = value.cast<py::list>();
172✔
520
          for (auto entry : xs_entries)
516✔
521
          {
522
            InputParameters xs_entry_pars = LBSProblem::GetXSMapEntryBlock();
172✔
523
            xs_entry_pars.AssignParameters(pyobj_to_param_block("", entry.cast<py::dict>()));
172✔
524
            const auto& block_ids =
172✔
525
              xs_entry_pars.GetParam("block_ids").GetVectorValue<unsigned int>();
172✔
526
            auto xs = xs_entry_pars.GetSharedPtrParam<MultiGroupXS>("xs");
172✔
527
            for (const auto& block_id : block_ids)
344✔
528
              xs_map[block_id] = xs;
172✔
529
          }
172✔
530
        }
172✔
531
        else
UNCOV
532
          throw std::runtime_error("Invalid argument provided to SetXSMap.\n");
×
533
      }
172✔
534
      self.SetBlockID2XSMap(xs_map);
172✔
535
    },
172✔
536
    R"(
537
    Replace the block-id to cross-section map.
538

539
    Parameters
540
    ----------
541
    xs_map: List[Dict]
542
        A list of block-id to cross-section mapping dictionaries. Each dictionary supports:
543
          - block_ids: List[int] (required)
544
              Mesh block ids to associate with the cross section.
545
          - xs: pyopensn.xs.MultiGroupXS (required)
546
              Cross section object.
547

548
    Notes
549
    -----
550
    Forward/adjoint mode toggles via :meth:`LBSProblem.SetAdjoint` do not change this map.
551
    The ``MultiGroupXS`` objects themselves are mutable and shared by pointer. If the same
552
    ``MultiGroupXS`` handle is shared across multiple problems, toggling adjoint mode in one
553
    problem also changes the transport mode seen by the others.
554
    )"
555
  );
556
  lbs_problem.def(
661✔
557
    "ZeroPhi",
558
    [](LBSProblem& self)
661✔
559
    {
UNCOV
560
      self.ZeroPhi();
×
561
    },
562
    R"(
563
    Zero the scalar-flux vectors (``phi_old`` and ``phi_new``) in-place.
564
    )"
565
  );
566
  lbs_problem.def(
661✔
567
    "ZeroPsi",
568
    [](LBSProblem& self)
661✔
569
    {
UNCOV
570
      self.ZeroPsi();
×
571
    },
572
    R"(
573
    Zero the angular-flux storage (if present for this problem type).
574
    )"
575
  );
576
  lbs_problem.def(
661✔
577
    "SetAdjoint",
578
    [](LBSProblem& self, bool adjoint)
661✔
579
    {
580
      self.SetAdjoint(adjoint);
24✔
581
    },
582
    py::arg("adjoint") = true,
661✔
583
    R"(
584
    Set forward/adjoint transport mode.
585

586
    Parameters
587
    ----------
588
    adjoint: bool, default=True
589
        ``True`` enables adjoint mode and ``False`` enables forward mode.
590

591
    Notes
592
    -----
593
    This is one of two supported mode-setting paths in Python:
594
      1. ``options={'adjoint': ...}`` in the problem constructor.
595
      2. ``SetAdjoint(...)`` (this method).
596

597
    If this changes mode, OpenSn performs a full mode-transition reset:
598
      - Materials are reinitialized in the selected mode.
599
      - Point and volumetric sources are cleared.
600
      - Boundary conditions are cleared.
601
      - Scalar and angular flux vectors are zeroed.
602

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

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

609
    After a mode change, reapply the desired driving terms before solving, typically:
610
      - :meth:`LBSProblem.SetPointSources`
611
      - :meth:`LBSProblem.SetVolumetricSources`
612
      - :meth:`DiscreteOrdinatesProblem.SetBoundaryOptions`
613

614
    This routine is intentionally destructive with respect to source/boundary/flux state
615
    to avoid hidden coupling between forward and adjoint setups.
616
    )"
617
  );
618
  lbs_problem.def(
661✔
619
    "SetForward",
620
    &LBSProblem::SetForward,
661✔
621
    R"(
622
    Set forward transport mode.
623

624
    Equivalent to ``SetAdjoint(False)``.
625
    )"
626
  );
627
  lbs_problem.def(
661✔
628
    "IsAdjoint",
629
    &LBSProblem::IsAdjoint,
661✔
630
    R"(
631
    Return ``True`` if the problem is in adjoint mode, otherwise ``False``.
632
    )"
633
  );
634
  lbs_problem.def(
661✔
635
    "IsTimeDependent",
636
    &LBSProblem::IsTimeDependent,
661✔
637
    R"(
638
    Return ``True`` if the problem is in time-dependent mode, otherwise ``False``.
639
    )"
640
  );
641

642
  // discrete ordinate solver
643
  auto do_problem = py::class_<DiscreteOrdinatesProblem, std::shared_ptr<DiscreteOrdinatesProblem>,
661✔
644
                               LBSProblem>(
645
    slv,
646
    "DiscreteOrdinatesProblem",
647
    R"(
648
    Base class for discrete ordinates problems in Cartesian geometry.
649

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

654
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesProblem`.
655
    )"
656
  );
661✔
657
  do_problem.def(
1,322✔
658
    py::init(
661✔
659
      [](py::kwargs& params)
563✔
660
      {
661
        return DiscreteOrdinatesProblem::Create(kwargs_to_param_block(params));
563✔
662
      }
663
    ),
664
    R"(
665
    Construct a discrete ordinates problem with Cartesian geometry.
666

667
    Parameters
668
    ----------
669
    mesh : MeshContinuum
670
        The spatial mesh.
671
    num_groups : int
672
        The total number of energy groups.
673
    groupsets : List[Dict], default=[]
674
        A list of input parameter blocks, each block provides the iterative properties for a
675
        groupset. Each dictionary supports:
676
          - groups_from_to: List[int] (required)
677
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
678
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
679
              Handle to an angular quadrature.
680
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
681
              Angle aggregation method to use during sweeping.
682
          - angle_aggregation_num_subsets: int, default=1
683
              Number of angle subsets used for aggregation.
684
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
685
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
686
              Iterative method used for inner linear solves.
687
          - l_abs_tol: float, default=1.0e-6
688
              Inner linear solver absolute residual tolerance.
689
          - l_max_its: int, default=200
690
              Inner linear solver maximum iterations.
691
          - gmres_restart_interval: int, default=30
692
              GMRES restart interval, if GMRES is used.
693
          - allow_cycles: bool, default=True
694
              Whether cyclic dependencies are allowed in sweeps.
695
          - apply_wgdsa: bool, default=False
696
              Enable within-group DSA for this groupset.
697
          - wgdsa_l_abs_tol: float, default=1.0e-4
698
              WGDSA linear absolute tolerance.
699
          - wgdsa_l_max_its: int, default=30
700
              WGDSA maximum iterations.
701
          - wgdsa_verbose: bool, default=False
702
              Verbose WGDSA output.
703
          - wgdsa_petsc_options: str, default=''
704
              PETSc options string for the WGDSA solver.
705
          - apply_tgdsa: bool, default=False
706
              Enable two-grid DSA for this groupset.
707
          - tgdsa_l_abs_tol: float, default=1.0e-4
708
              TGDSA linear absolute tolerance.
709
          - tgdsa_l_max_its: int, default=30
710
              TGDSA maximum iterations.
711
          - tgdsa_verbose: bool, default=False
712
              Verbose TGDSA output.
713
          - tgdsa_petsc_options: str, default=''
714
              PETSc options string for the TGDSA solver.
715
    xs_map : List[Dict], default=[]
716
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
717
          - block_ids: List[int] (required)
718
              Mesh block IDs to associate with the cross section.
719
          - xs: pyopensn.xs.MultiGroupXS (required)
720
              Cross-section object to assign to the specified blocks.
721
    boundary_conditions: List[Dict], default=[]
722
        A list containing tables for each boundary specification. Each dictionary supports:
723
          - name: str (required)
724
              Boundary name that identifies the specific boundary.
725
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
726
              Boundary type specification.
727
          - group_strength: List[float], optional
728
              Required when ``type='isotropic'``. Isotropic strength per group.
729
          - function: AngularFluxFunction, optional
730
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
731
    point_sources: List[pyopensn.source.PointSource], default=[]
732
        A list of point sources.
733
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
734
        A list of volumetric sources.
735
    options : Dict, default={}
736
        A block of optional configuration parameters, including:
737
          - max_mpi_message_size: int, default=32768
738
          - restart_writes_enabled: bool, default=False
739
          - write_delayed_psi_to_restart: bool, default=True
740
          - read_restart_path: str, default=''
741
          - write_restart_path: str, default=''
742
          - write_restart_time_interval: int, default=0
743
            (must be 0 or >=30)
744
          - use_precursors: bool, default=False
745
          - use_source_moments: bool, default=False
746
          - save_angular_flux: bool, default=False
747
          - adjoint: bool, default=False
748
          - verbose_inner_iterations: bool, default=True
749
          - verbose_outer_iterations: bool, default=True
750
          - max_ags_iterations: int, default=100
751
          - ags_tolerance: float, default=1.0e-6
752
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
753
          - verbose_ags_iterations: bool, default=True
754
          - power_default_kappa: float, default=3.20435e-11
755
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
756
          - field_function_prefix: str, default=''
757
        These options are applied at problem creation.
758
    sweep_type : str, default="AAH"
759
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
760
    time_dependent : bool, default=False
761
        If true, the problem starts in time-dependent mode. Otherwise it starts in
762
        steady-state mode. Requires ``options.save_angular_flux=True``.
763
    use_gpus : bool, default=False
764
        A flag specifying whether GPU acceleration is used for the sweep. Currently, only ``AAH`` is
765
        supported.
766
    )"
767
  );
768
  do_problem.def(
661✔
769
    "SetTimeDependentMode",
770
    &DiscreteOrdinatesProblem::SetTimeDependentMode,
661✔
771
    R"(
772
    Set the problem to time-dependent mode.
773

774
    Notes
775
    -----
776
    Switch problem from steady-state to time-dependent mode. This updates problem
777
    internals (sweep chunk mode and source-function) while preserving user boundary
778
    conditions and fixed sources.
779

780
    Requires ``options.save_angular_flux=True`` at problem creation.
781
    )"
782
  );
783
  do_problem.def(
661✔
784
    "SetSteadyStateMode",
785
    &DiscreteOrdinatesProblem::SetSteadyStateMode,
661✔
786
    R"(
787
    Set the problem to steady-state mode.
788

789
    Notes
790
    -----
791
    Switch problem from time-dependent to steady-state mode. This updates problem
792
    internals (sweep chunk mode and source-function) while preserving user boundary
793
    conditions and fixed sources.
794
    )"
795
  );
796
  do_problem.def(
661✔
797
    "IsTimeDependent",
798
    &DiscreteOrdinatesProblem::IsTimeDependent,
661✔
799
    R"(
800
    Return ``True`` if the problem is currently in time-dependent mode.
801
    )"
802
  );
803
  do_problem.def(
661✔
804
    "SetBoundaryOptions",
805
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
665✔
806
    {
807
      for (auto [key, value] : params)
12✔
808
      {
809
        auto c_key = key.cast<std::string>();
4✔
810
        if (c_key == "clear_boundary_conditions")
4✔
UNCOV
811
          self.ClearBoundaries();
×
812
        else if (c_key == "boundary_conditions")
4✔
813
        {
814
          auto boundaries = value.cast<py::list>();
4✔
815
          for (auto boundary : boundaries)
24✔
816
          {
817
            InputParameters input = DiscreteOrdinatesProblem::GetBoundaryOptionsBlock();
16✔
818
            input.AssignParameters(pyobj_to_param_block("", boundary.cast<py::dict>()));
16✔
819
            self.SetBoundaryOptions(input);
16✔
820
          }
16✔
821
        }
4✔
822
        else
UNCOV
823
          throw std::runtime_error("Invalid argument provided to SetBoundaryOptions.\n");
×
824
      }
4✔
825
    },
4✔
826
    R"(
827
    Set or clear boundary conditions.
828

829
    Parameters
830
    ----------
831
    clear_boundary_conditions: bool, default=False
832
        If true, all current boundary conditions are deleted.
833
    boundary_conditions: List[Dict]
834
        A list of boundary condition dictionaries. Each dictionary supports:
835
          - name: str (required)
836
              Boundary name that identifies the specific boundary.
837
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
838
              Boundary type specification.
839
          - group_strength: List[float], optional
840
              Required when ``type='isotropic'``. Isotropic strength per group.
841
          - function: AngularFluxFunction, optional
842
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
843

844
    Notes
845
    -----
846
    Mode transitions via :meth:`LBSProblem.SetAdjoint` clear all boundary conditions.
847
    Reapply boundaries with this method before solving in the new mode.
848
    )"
849
  );
850
  do_problem.def(
661✔
851
    "GetPsi",
852
    [](DiscreteOrdinatesProblem& self)
669✔
853
    {
854
      const auto& psi = self.GetPsiNewLocal();
8✔
855
      py::list psi_list;
8✔
856
      for (const auto& vec : psi)
16✔
857
      {
858
        auto array = py::array_t<double>(static_cast<py::ssize_t>(vec.size()),
8✔
859
                                         vec.data(),
860
                                         py::cast(self));
8✔
861
        psi_list.append(array);
8✔
862
      }
8✔
863
      return psi_list;
8✔
UNCOV
864
    },
×
865
    R"(
866
    Return psi as a list of NumPy arrays (float64), using zero-copy views into the
867
    underlying data.
868
    )"
869
  );
870
  do_problem.def(
661✔
871
    "GetAngularFieldFunctionList",
872
    [](DiscreteOrdinatesProblem& self, py::list groups, py::list angles)
661✔
873
    {
UNCOV
874
      std::vector<unsigned int> group_ids;
×
UNCOV
875
      std::vector<size_t> angle_ids;
×
UNCOV
876
      group_ids.reserve(groups.size());
×
UNCOV
877
      angle_ids.reserve(angles.size());
×
878

UNCOV
879
      for (py::handle g : groups)
×
UNCOV
880
        group_ids.push_back(g.cast<unsigned int>());
×
UNCOV
881
      for (py::handle a : angles)
×
882
        angle_ids.push_back(a.cast<size_t>());
×
883

NEW
884
      auto ff_list = self.CreateAngularFluxFieldFunctionList(group_ids, angle_ids);
×
UNCOV
885
      py::list out;
×
UNCOV
886
      for (const auto& ff : ff_list)
×
UNCOV
887
        out.append(ff);
×
UNCOV
888
      return out;
×
UNCOV
889
    },
×
890
    R"(
891
    Create field functions for selected angular flux components.
892

893
    Note: You must enable angular flux storage (``save_angular_flux=True``) in
894
    the problem options, otherwise the returned field functions will remain zero.
895

896
    Example
897
    -------
898
    ```python
899
    solver.Initialize()
900
    solver.Execute()
901
    ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
902
    ```
903

904
    For transient/time-dependent runs, call this after each timestep. Two common
905
    patterns are:
906

907
    1) Use `TransientSolver.Execute()` with a post-advance callback:
908
    ```python
909
    solver = TransientSolver(problem=phys)
910
    solver.Initialize()
911

912
    def post_advance():
913
        ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
914
        FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
915

916
    solver.SetPostAdvanceCallback(post_advance)
917
    solver.Execute()
918
    ```
919

920
    2) Use a custom Python loop with `TransientSolver.Advance()`:
921
    ```python
922
    solver = TransientSolver(problem=phys)
923
    solver.Initialize()
924
    for _ in range(num_steps):
925
        solver.Advance()
926
        ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
927
        FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
928
    ```
929

930
    Parameters
931
    ----------
932
    groups : List[int]
933
        Global group indices to export.
934
    angles : List[int]
935
        Angle indices within the groupset quadrature for each group.
936

937
    Returns
938
    -------
939
    List[pyopensn.fieldfunc.FieldFunctionGridBased]
940
        Field-function snapshots for the requested (group, angle) pairs.
941
    )",
942
    py::arg("groups"),
1,322✔
943
    py::arg("angles")
661✔
944
  );
945
  do_problem.def(
661✔
946
    "ComputeLeakage",
947
    [](DiscreteOrdinatesProblem& self, py::list bnd_names)
700✔
948
    {
949
      auto grid = self.GetGrid();
39✔
950
      // get the supported boundaries
951
      std::map<std::string, std::uint64_t> allowed_bd_names = grid->GetBoundaryNameMap();
39✔
952
      std::map<std::uint64_t, std::string> allowed_bd_ids = grid->GetBoundaryIDMap();
39✔
953
      const auto coord_sys = grid->GetCoordinateSystem();
39✔
954
      const auto mesh_type = grid->GetType();
39✔
955
      const auto dim = grid->GetDimension();
39✔
956
      // get the boundaries to parse, preserving user order
957
      std::vector<std::uint64_t> bndry_ids;
39✔
958
      if (bnd_names.size() > 1)
39✔
959
      {
960
        for (py::handle name : bnd_names)
184✔
961
        {
962
          auto sname = name.cast<std::string>();
112✔
963
          if (coord_sys == CoordinateSystemType::CYLINDRICAL && dim == 2)
112✔
964
          {
965
            if (sname == "xmin" || sname == "xmax" || sname == "ymin" || sname == "ymax")
24✔
UNCOV
966
              throw std::runtime_error("ComputeLeakage: Boundary name '" + sname +
×
967
                                       "' is invalid for cylindrical orthogonal meshes. "
UNCOV
968
                                       "Use rmin, rmax, zmin, zmax.");
×
969

970
            if (mesh_type == MeshType::ORTHOGONAL)
24✔
971
            {
972
              if (sname == "rmin") sname = "xmin";
24✔
973
              else if (sname == "rmax") sname = "xmax";
24✔
974
              else if (sname == "zmin") sname = "ymin";
16✔
975
              else if (sname == "zmax") sname = "ymax";
8✔
976
            }
977
          }
978
          bndry_ids.push_back(allowed_bd_names.at(sname));
112✔
979
        }
112✔
980
      }
981
      else
982
      {
983
        bndry_ids = self.GetGrid()->GetUniqueBoundaryIDs();
9✔
984
      }
985
      // compute the leakage
986
      std::map<std::uint64_t, std::vector<double>> leakage = ComputeLeakage(self, bndry_ids);
39✔
987
      const bool rz_ortho = (coord_sys == CoordinateSystemType::CYLINDRICAL &&
78✔
988
                             mesh_type == MeshType::ORTHOGONAL && dim == 2);
39✔
989

990
      auto to_rz_name = [](const std::string& name)
63✔
991
      {
992
        if (name == "xmin") return std::string("rmin");
24✔
993
        if (name == "xmax") return std::string("rmax");
24✔
994
        if (name == "ymin") return std::string("zmin");
16✔
995
        if (name == "ymax") return std::string("zmax");
8✔
UNCOV
996
        return name;
×
997
      };
998

999
      // convert result to native Python
1000
      py::dict result;
39✔
1001
      for (const auto& bndry_id : bndry_ids)
157✔
1002
      {
1003
        const auto it = leakage.find(bndry_id);
118✔
1004
        if (it == leakage.end())
118✔
UNCOV
1005
          continue;
×
1006
        // construct numpy array and copy contents
1007
        const auto& grp_wise_leakage = it->second;
118✔
1008
        py::array_t<double> np_vector(py::ssize_t(grp_wise_leakage.size()));
118✔
1009
        auto buffer = np_vector.request();
118✔
1010
        auto *np_vector_data = static_cast<double*>(buffer.ptr);
118✔
1011
        std::copy(grp_wise_leakage.begin(), grp_wise_leakage.end(), np_vector_data);
118✔
1012
        std::string name = allowed_bd_ids.at(bndry_id);
118✔
1013
        if (rz_ortho)
118✔
1014
          name = to_rz_name(name);
24✔
1015
        result[py::str(name)] = std::move(np_vector);
236✔
1016
      }
118✔
1017

1018
      return result;
39✔
1019
    },
78✔
1020
    R"(
1021
    Compute leakage for the problem.
1022

1023
    Parameters
1024
    ----------
1025
    bnd_names : List[str]
1026
        A list of boundary names for which leakage should be computed.
1027

1028
    Returns
1029
    -------
1030
    Dict[str, numpy.ndarray]
1031
        A dictionary mapping boundary names to group-wise leakage vectors.
1032
        Each array contains the outgoing angular flux (per group) integrated over
1033
        the corresponding boundary surface.
1034

1035
    Raises
1036
    ------
1037
    RuntimeError
1038
        If `save_angular_flux` option was not enabled during problem setup.
1039

1040
    ValueError
1041
        If one or more boundary ids are not present on the current mesh.
1042
    )",
1043
    py::arg("bnd_names")
661✔
1044
  );
1045

1046
  // discrete ordinates curvilinear problem
1047
  auto do_curvilinear_problem = py::class_<DiscreteOrdinatesCurvilinearProblem,
661✔
1048
                                           std::shared_ptr<DiscreteOrdinatesCurvilinearProblem>,
1049
                                           DiscreteOrdinatesProblem>(
1050
    slv,
1051
    "DiscreteOrdinatesCurvilinearProblem",
1052
    R"(
1053
    Base class for discrete ordinates problems in curvilinear geometry.
1054

1055
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesCurvilinearProblem`.
1056
    )"
1057
  );
661✔
1058
  do_curvilinear_problem.def(
1,322✔
1059
    py::init(
661✔
1060
      [](py::kwargs& params)
80✔
1061
      {
1062
        return DiscreteOrdinatesCurvilinearProblem::Create(kwargs_to_param_block(params));
80✔
1063
      }
1064
    ),
1065
    R"(
1066
    Construct a discrete ordinates problem for curvilinear geometry.
1067

1068
    Warnings
1069
    --------
1070
       DiscreteOrdinatesCurvilinearProblem is **experimental** and should be used with caution!
1071

1072
    Parameters
1073
    ----------
1074
    mesh : MeshContinuum
1075
        The spatial mesh.
1076
    coord_system : int
1077
        Coordinate system to use. Must be set to 2 (cylindrical coordinates).
1078
    num_groups : int
1079
        The total number of energy groups.
1080
    groupsets : list of dict
1081
        A list of input parameter blocks, each block provides the iterative properties for a
1082
        groupset. Each dictionary supports:
1083
          - groups_from_to: List[int] (required)
1084
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
1085
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
1086
              Handle to an angular quadrature.
1087
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
1088
              Angle aggregation method to use during sweeping.
1089
          - angle_aggregation_num_subsets: int, default=1
1090
              Number of angle subsets used for aggregation.
1091
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
1092
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
1093
              Iterative method used for inner linear solves.
1094
          - l_abs_tol: float, default=1.0e-6
1095
              Inner linear solver absolute residual tolerance.
1096
          - l_max_its: int, default=200
1097
              Inner linear solver maximum iterations.
1098
          - gmres_restart_interval: int, default=30
1099
              GMRES restart interval, if GMRES is used.
1100
          - allow_cycles: bool, default=True
1101
              Whether cyclic dependencies are allowed in sweeps.
1102
          - apply_wgdsa: bool, default=False
1103
              Enable within-group DSA for this groupset.
1104
          - wgdsa_l_abs_tol: float, default=1.0e-4
1105
              WGDSA linear absolute tolerance.
1106
          - wgdsa_l_max_its: int, default=30
1107
              WGDSA maximum iterations.
1108
          - wgdsa_verbose: bool, default=False
1109
              Verbose WGDSA output.
1110
          - wgdsa_petsc_options: str, default=''
1111
              PETSc options string for the WGDSA solver.
1112
          - apply_tgdsa: bool, default=False
1113
              Enable two-grid DSA for this groupset.
1114
          - tgdsa_l_abs_tol: float, default=1.0e-4
1115
              TGDSA linear absolute tolerance.
1116
          - tgdsa_l_max_its: int, default=30
1117
              TGDSA maximum iterations.
1118
          - tgdsa_verbose: bool, default=False
1119
              Verbose TGDSA output.
1120
          - tgdsa_petsc_options: str, default=''
1121
    xs_map : list of dict
1122
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
1123
          - block_ids: List[int] (required)
1124
              Mesh block IDs to associate with the cross section.
1125
          - xs: pyopensn.xs.MultiGroupXS (required)
1126
              Cross-section object to assign to the specified blocks.
1127
    boundary_conditions: List[Dict], default=[]
1128
        A list containing tables for each boundary specification. Each dictionary supports:
1129
          - name: str (required)
1130
              Boundary name that identifies the specific boundary.
1131
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
1132
              Boundary type specification.
1133
          - group_strength: List[float], optional
1134
              Required when ``type='isotropic'``. Isotropic strength per group.
1135
          - function: AngularFluxFunction, optional
1136
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
1137
    point_sources: List[pyopensn.source.PointSource], default=[]
1138
        A list of point sources.
1139
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
1140
        A list of volumetric sources.
1141
    options : dict, optional
1142
        A block of optional configuration parameters applied at problem creation, including:
1143
          - max_mpi_message_size: int, default=32768
1144
          - restart_writes_enabled: bool, default=False
1145
          - write_delayed_psi_to_restart: bool, default=True
1146
          - read_restart_path: str, default=''
1147
          - write_restart_path: str, default=''
1148
          - write_restart_time_interval: int, default=0
1149
          - use_precursors: bool, default=False
1150
          - use_source_moments: bool, default=False
1151
          - save_angular_flux: bool, default=False
1152
          - verbose_inner_iterations: bool, default=True
1153
          - verbose_outer_iterations: bool, default=True
1154
          - max_ags_iterations: int, default=100
1155
          - ags_tolerance: float, default=1.0e-6
1156
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
1157
          - verbose_ags_iterations: bool, default=True
1158
          - power_default_kappa: float, default=3.20435e-11
1159
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
1160
          - field_function_prefix: str, default=''
1161
    sweep_type : str, optional
1162
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
1163
        If ``time_dependent=True``, ``options.save_angular_flux=True`` is required.
1164
    )"
1165
  );
1166
}
661✔
1167

1168
// Wrap steady-state solver
1169
void
1170
WrapSteadyState(py::module& slv)
661✔
1171
{
1172
  const auto balance_table_to_dict = [](const BalanceTable& table)
680✔
1173
  {
1174
    py::dict values;
19✔
1175
    values["absorption_rate"] = table.absorption_rate;
19✔
1176
    values["production_rate"] = table.production_rate;
19✔
1177
    values["inflow_rate"] = table.inflow_rate;
19✔
1178
    values["outflow_rate"] = table.outflow_rate;
19✔
1179
    values["balance"] = table.balance;
19✔
1180
    if (table.initial_inventory.has_value())
19✔
UNCOV
1181
      values["initial_inventory"] = table.initial_inventory.value();
×
1182
    if (table.final_inventory.has_value())
19✔
UNCOV
1183
      values["final_inventory"] = table.final_inventory.value();
×
1184
    if (table.predicted_inventory_change.has_value())
19✔
UNCOV
1185
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1186
    if (table.actual_inventory_change.has_value())
19✔
UNCOV
1187
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1188
    if (table.inventory_residual.has_value())
19✔
UNCOV
1189
      values["inventory_residual"] = table.inventory_residual.value();
×
1190
    return values;
19✔
UNCOV
1191
  };
×
1192

1193
  // clang-format off
1194
  // steady state solver
1195
  auto steady_state_solver = py::class_<SteadyStateSourceSolver, std::shared_ptr<SteadyStateSourceSolver>,
661✔
1196
                                        Solver>(
1197
    slv,
1198
    "SteadyStateSourceSolver",
1199
    R"(
1200
    Steady state solver.
1201

1202
    Wrapper of :cpp:class:`opensn::SteadyStateSourceSolver`.
1203
    )"
1204
  );
661✔
1205
  steady_state_solver.def(
1,322✔
1206
    py::init(
661✔
1207
      [](py::kwargs& params)
407✔
1208
      {
1209
        return SteadyStateSourceSolver::Create(kwargs_to_param_block(params));
407✔
1210
      }
1211
    ),
1212
    R"(
1213
    Construct a steady state solver.
1214

1215
    Parameters
1216
    ----------
1217
    pyopensn.solver.LBSProblem : LBSProblem
1218
        Existing LBSProblem instance.
1219
    )"
1220
  );
1221
  steady_state_solver.def(
661✔
1222
    "ComputeBalanceTable",
1223
    [balance_table_to_dict](const SteadyStateSourceSolver& self)
661✔
1224
    {
1225
      return balance_table_to_dict(self.ComputeBalanceTable());
19✔
1226
    },
1227
    R"(
1228
    Compute and return the global balance table using the solver's normalization.
1229
    This is a collective operation and must be called on all ranks.
1230

1231
    Returns
1232
    -------
1233
    dict
1234
        Dictionary with the following entries:
1235

1236
        - ``absorption_rate``:
1237
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1238
          groups and the full domain.
1239
        - ``production_rate``:
1240
          Global volumetric production/source rate used by the solver,
1241
          approximately ``integral Q dV`` summed over groups and the full domain.
1242
        - ``inflow_rate``:
1243
          Global incoming boundary contribution integrated over incoming
1244
          angular flux on boundaries.
1245
        - ``outflow_rate``:
1246
          Global outgoing boundary contribution accumulated from face outflow
1247
          tallies.
1248
        - ``balance``:
1249
          Rate balance,
1250
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1251

1252
    Notes
1253
    -----
1254
    This solver applies no extra normalization to the balance table.
1255
    )"
1256
  );
1257
  // clang-format on
1258
}
661✔
1259

1260
// Wrap transient solver
1261
void
1262
WrapTransient(py::module& slv)
661✔
1263
{
1264
  const auto balance_table_to_dict = [](const BalanceTable& table)
661✔
1265
  {
UNCOV
1266
    py::dict values;
×
UNCOV
1267
    values["absorption_rate"] = table.absorption_rate;
×
UNCOV
1268
    values["production_rate"] = table.production_rate;
×
UNCOV
1269
    values["inflow_rate"] = table.inflow_rate;
×
UNCOV
1270
    values["outflow_rate"] = table.outflow_rate;
×
UNCOV
1271
    values["balance"] = table.balance;
×
UNCOV
1272
    if (table.initial_inventory.has_value())
×
UNCOV
1273
      values["initial_inventory"] = table.initial_inventory.value();
×
UNCOV
1274
    if (table.final_inventory.has_value())
×
UNCOV
1275
      values["final_inventory"] = table.final_inventory.value();
×
UNCOV
1276
    if (table.predicted_inventory_change.has_value())
×
UNCOV
1277
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
UNCOV
1278
    if (table.actual_inventory_change.has_value())
×
UNCOV
1279
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
UNCOV
1280
    if (table.inventory_residual.has_value())
×
UNCOV
1281
      values["inventory_residual"] = table.inventory_residual.value();
×
1282
    return values;
×
1283
  };
×
1284
  // clang-format off
1285
  auto transient_solver =
661✔
1286
    py::class_<TransientSolver, std::shared_ptr<TransientSolver>, Solver>(
1287
      slv,
1288
      "TransientSolver",
1289
      R"(
1290
      Transient solver.
1291

1292
      Wrapper of :cpp:class:`opensn::TransientSolver`.
1293
      )"
1294
    );
661✔
1295
  transient_solver.def(
1,322✔
1296
    py::init(
661✔
1297
      [](py::kwargs& params)
160✔
1298
      {
1299
        return TransientSolver::Create(kwargs_to_param_block(params));
160✔
1300
      }
1301
    ),
1302
    R"(
1303
    Construct a transient solver.
1304

1305
    Parameters
1306
    ----------
1307
    pyopensn.solver.DiscreteOrdinatesProblem : DiscreteOrdinatesProblem
1308
        Existing discrete ordinates problem instance.
1309
    dt : float, optional, default=2.0e-3
1310
        Time step size used during the simulation.
1311
    stop_time : float, optional, default=0.1
1312
        Simulation end time.
1313
    theta : float, optional, default=0.5
1314
        Time differencing scheme parameter.
1315
    initial_state : str, optional, default="existing"
1316
        Initial state for the transient solve. Allowed values: existing, zero.
1317
        In "zero" mode, scalar flux vectors are reset to zero.
1318
    verbose : bool, optional, default=True
1319
        Enable verbose logging.
1320
    )"
1321
  );
1322
  transient_solver.def(
661✔
1323
    "SetTimeStep",
1324
    &TransientSolver::SetTimeStep,
661✔
1325
    R"(
1326
    Set the timestep size used by :meth:`Advance`.
1327

1328
    Parameters
1329
    ----------
1330
    dt : float
1331
        New timestep size.
1332
    )");
1333
  transient_solver.def(
661✔
1334
    "SetTheta",
1335
    &TransientSolver::SetTheta,
661✔
1336
    R"(
1337
    Set the theta parameter used by :meth:`Advance`.
1338

1339
    Parameters
1340
    ----------
1341
    theta : float
1342
        Theta value between 1.0e-16 and 1.
1343
    )");
1344
  transient_solver.def(
661✔
1345
    "Advance",
1346
    &TransientSolver::Advance,
661✔
1347
    R"(
1348
    Advance the solver by a single timestep.
1349

1350
    Notes
1351
    -----
1352
    You must call :meth:`Initialize` before calling :meth:`Advance` or
1353
    :meth:`Execute`.
1354
    )");
1355
  transient_solver.def(
661✔
1356
    "SetPreAdvanceCallback",
1357
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
661✔
1358
      &TransientSolver::SetPreAdvanceCallback),
1359
    R"(
1360
    Register a callback that runs before each advance within :meth:`Execute`.
1361

1362
    Parameters
1363
    ----------
1364
    callback : Optional[Callable[[], None]]
1365
        Function invoked before the solver advances a timestep. Pass None to clear.
1366
        If the callback modifies the timestep, the new value is used for the
1367
        upcoming step.
1368
    )");
1369
  transient_solver.def(
661✔
1370
    "SetPreAdvanceCallback",
1371
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
661✔
1372
      &TransientSolver::SetPreAdvanceCallback),
1373
    "Clear the PreAdvance callback by passing None.");
1374
  transient_solver.def(
661✔
1375
    "SetPostAdvanceCallback",
1376
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
661✔
1377
      &TransientSolver::SetPostAdvanceCallback),
1378
    R"(
1379
    Register a callback that runs after each advance within :meth:`Execute`.
1380

1381
    Parameters
1382
    ----------
1383
    callback : Optional[Callable[[], None]]
1384
        Function invoked after the solver advances a timestep. Pass None to clear.
1385
    )");
1386
  transient_solver.def(
661✔
1387
    "SetPostAdvanceCallback",
1388
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
661✔
1389
      &TransientSolver::SetPostAdvanceCallback),
1390
    "Clear the PostAdvance callback by passing None.");
1391
  transient_solver.def(
661✔
1392
    "ComputeBalanceTable",
1393
    [balance_table_to_dict](const TransientSolver& self)
661✔
1394
    {
UNCOV
1395
      return balance_table_to_dict(self.ComputeBalanceTable());
×
1396
    },
1397
    R"(
1398
    Compute and return the global balance table using the solver's normalization.
1399
    This is a collective operation and must be called on all ranks.
1400

1401
    Returns
1402
    -------
1403
    dict
1404
        Dictionary with the following entries:
1405

1406
        - ``absorption_rate``:
1407
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1408
          groups and the full domain.
1409
        - ``production_rate``:
1410
          Global volumetric production/source rate used by the solver,
1411
          approximately ``integral Q dV`` summed over groups and the full domain.
1412
        - ``inflow_rate``:
1413
          Global incoming boundary contribution integrated over incoming
1414
          angular flux on boundaries.
1415
        - ``outflow_rate``:
1416
          Global outgoing boundary contribution accumulated from face outflow
1417
          tallies.
1418
        - ``balance``:
1419
          Rate balance,
1420
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1421
        - ``initial_inventory``:
1422
          Total particle inventory at the start of the timestep, computed as
1423
          ``integral (1 / v_g) * phi_old dV`` summed over groups and the full domain.
1424
        - ``final_inventory``:
1425
          Total particle inventory at the end of the timestep, computed as
1426
          ``integral (1 / v_g) * phi_new dV`` summed over groups and the full domain.
1427
        - ``predicted_inventory_change``:
1428
          Inventory change predicted by the current timestep balance, computed as 
1429
          ``dt * balance``.
1430
        - ``actual_inventory_change``:
1431
          Measured change in total particle inventory over the timestep, computed as
1432
          ``final_inventory - initial_inventory``.
1433
        - ``inventory_residual``:
1434
          Mismatch between the measured and predicted timestep inventory
1435
          changes, computed as
1436
          ``actual_inventory_change - predicted_inventory_change``.
1437

1438
    Notes
1439
    -----
1440
    This solver applies no extra normalization to the balance table.
1441

1442
    The transient inventory terms currently use the end-of-step rate balance to
1443
    estimate the timestep inventory change.
1444
    )"
1445
  );
1446
  slv.attr("BackwardEuler") = 1.0;
661✔
1447
  slv.attr("CrankNicolson") = 0.5;
1,322✔
1448
  // clang-format on
1449
}
661✔
1450

1451
// Wrap non-linear k-eigen solver
1452
void
1453
WrapNLKEigen(py::module& slv)
661✔
1454
{
1455
  const auto balance_table_to_dict = [](const BalanceTable& table)
661✔
1456
  {
UNCOV
1457
    py::dict values;
×
UNCOV
1458
    values["absorption_rate"] = table.absorption_rate;
×
UNCOV
1459
    values["production_rate"] = table.production_rate;
×
UNCOV
1460
    values["inflow_rate"] = table.inflow_rate;
×
UNCOV
1461
    values["outflow_rate"] = table.outflow_rate;
×
UNCOV
1462
    values["balance"] = table.balance;
×
UNCOV
1463
    if (table.initial_inventory.has_value())
×
UNCOV
1464
      values["initial_inventory"] = table.initial_inventory.value();
×
UNCOV
1465
    if (table.final_inventory.has_value())
×
UNCOV
1466
      values["final_inventory"] = table.final_inventory.value();
×
UNCOV
1467
    if (table.predicted_inventory_change.has_value())
×
UNCOV
1468
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
UNCOV
1469
    if (table.actual_inventory_change.has_value())
×
UNCOV
1470
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
UNCOV
1471
    if (table.inventory_residual.has_value())
×
UNCOV
1472
      values["inventory_residual"] = table.inventory_residual.value();
×
1473
    return values;
×
1474
  };
×
1475
  // clang-format off
1476
  // non-linear k-eigen solver
1477
  auto non_linear_k_eigen_solver = py::class_<NonLinearKEigenSolver, std::shared_ptr<NonLinearKEigenSolver>,
661✔
1478
                                              Solver>(
1479
    slv,
1480
    "NonLinearKEigenSolver",
1481
    R"(
1482
    Non-linear k-eigenvalue solver.
1483

1484
    Wrapper of :cpp:class:`opensn::NonLinearKEigenSolver`.
1485
    )"
1486
  );
661✔
1487
  non_linear_k_eigen_solver.def(
1,322✔
1488
    py::init(
661✔
1489
      [](py::kwargs& params)
37✔
1490
      {
1491
        return NonLinearKEigenSolver::Create(kwargs_to_param_block(params));
37✔
1492
      }
1493
        ),
1494
    R"(
1495
    Construct a non-linear k-eigenvalue solver.
1496

1497
    Parameters
1498
    ----------
1499
    lbs_problem: pyopensn.solver.LBSProblem
1500
        Existing LBSProblem instance.
1501
    nl_abs_tol: float, default=1.0e-8
1502
        Non-linear absolute tolerance.
1503
    nl_rel_tol: float, default=1.0e-8
1504
        Non-linear relative tolerance.
1505
    nl_sol_tol: float, default=1.0e-50
1506
        Non-linear solution tolerance.
1507
    nl_max_its: int, default=50
1508
        Non-linear algorithm maximum iterations.
1509
    l_abs_tol: float, default=1.0e-8
1510
        Linear absolute tolerance.
1511
    l_rel_tol: float, default=1.0e-8
1512
        Linear relative tolerance.
1513
    l_div_tol: float, default=1.0e6
1514
        Linear divergence tolerance.
1515
    l_max_its: int, default=50
1516
        Linear algorithm maximum iterations.
1517
    l_gmres_restart_intvl: int, default=30
1518
        GMRES restart interval.
1519
    l_gmres_breakdown_tol: float, default=1.0e6
1520
        GMRES breakdown tolerance.
1521
    reset_phi0: bool, default=True
1522
        If true, reinitializes scalar fluxes to 1.0.
1523
    num_initial_power_iterations: int, default=0
1524
        Number of initial power iterations before the non-linear solve.
1525
    )"
1526
  );
1527
  non_linear_k_eigen_solver.def(
661✔
1528
    "GetEigenvalue",
1529
    &NonLinearKEigenSolver::GetEigenvalue,
661✔
1530
    R"(
1531
    Return the current k-eigenvalue.
1532
    )"
1533
  );
1534
  non_linear_k_eigen_solver.def(
661✔
1535
    "ComputeBalanceTable",
1536
    [balance_table_to_dict](const NonLinearKEigenSolver& self)
661✔
1537
    {
UNCOV
1538
      return balance_table_to_dict(self.ComputeBalanceTable());
×
1539
    },
1540
    R"(
1541
    Compute and return the global balance table using the solver's normalization.
1542
    This is a collective operation and must be called on all ranks.
1543

1544
    Returns
1545
    -------
1546
    dict
1547
        Dictionary with the following entries:
1548

1549
        - ``absorption_rate``:
1550
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1551
          groups and the full domain.
1552
        - ``production_rate``:
1553
          Global volumetric production/source rate used by the solver,
1554
          approximately ``integral Q dV`` summed over groups and the full domain.
1555
        - ``inflow_rate``:
1556
          Global incoming boundary contribution integrated over incoming
1557
          angular flux on boundaries.
1558
        - ``outflow_rate``:
1559
          Global outgoing boundary contribution accumulated from face outflow
1560
          tallies.
1561
        - ``balance``:
1562
          Rate balance,
1563
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1564

1565
    Notes
1566
    -----
1567
    For k-eigenvalue balance reporting, this solver scales the production term by
1568
    ``1 / k_eff`` before forming both ``production_rate`` and ``balance``.
1569
    )"
1570
  );
1571
  // clang-format on
1572
}
661✔
1573

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

1607
    Wrapper of :cpp:class:`opensn::PowerIterationKEigenSolver`.
1608
    )"
1609
  );
661✔
1610
  pi_k_eigen_solver.def(
1,322✔
1611
    py::init(
661✔
1612
      [](py::kwargs& params)
153✔
1613
      {
1614
        return PowerIterationKEigenSolver::Create(kwargs_to_param_block(params));
153✔
1615
      }
1616
    ),
1617
    R"(
1618
    Construct a power iteration k-eigen solver.
1619

1620
    Parameters
1621
    ----------
1622
    problem: pyopensn.solver.LBSProblem
1623
        Existing DiscreteOrdinatesProblem instance.
1624
    acceleration: pyopensn.solver.DiscreteOrdinatesKEigenAcceleration
1625
        Optional DiscreteOrdinatesKEigenAcceleration instance for acceleration.
1626
    max_iters: int, default = 1000
1627
        Maximum power iterations allowed.
1628
    k_tol: float, default = 1.0e-10
1629
        Tolerance on the k-eigenvalue.
1630
    reset_solution: bool, default=True
1631
        If true, initialize flux moments to 1.0.
1632
    reset_phi0: bool, default=True
1633
        If true, reinitializes scalar fluxes to 1.0.
1634
    )"
1635
  );
1636
  pi_k_eigen_solver.def(
661✔
1637
    "GetEigenvalue",
1638
    &PowerIterationKEigenSolver::GetEigenvalue,
661✔
1639
    R"(
1640
    Return the current k-eigenvalue.
1641
    )"
1642
  );
1643
  pi_k_eigen_solver.def(
661✔
1644
    "ComputeBalanceTable",
1645
    [balance_table_to_dict](const PowerIterationKEigenSolver& self)
661✔
1646
    {
1647
      return balance_table_to_dict(self.ComputeBalanceTable());
4✔
1648
    },
1649
    R"(
1650
    Compute and return the global balance table using the solver's normalization.
1651
    This is a collective operation and must be called on all ranks.
1652

1653
    Returns
1654
    -------
1655
    dict
1656
        Dictionary with the following entries:
1657

1658
        - ``absorption_rate``:
1659
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1660
          groups and the full domain.
1661
        - ``production_rate``:
1662
          Global volumetric production/source rate used by the solver,
1663
          approximately ``integral Q dV`` summed over groups and the full domain.
1664
        - ``inflow_rate``:
1665
          Global incoming boundary contribution integrated over incoming
1666
          angular flux on boundaries.
1667
        - ``outflow_rate``:
1668
          Global outgoing boundary contribution accumulated from face outflow
1669
          tallies.
1670
        - ``balance``:
1671
          Rate balance,
1672
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1673

1674
    Notes
1675
    -----
1676
    For k-eigenvalue balance reporting, this solver scales the production term by
1677
    ``1 / k_eff`` before forming both ``production_rate`` and ``balance``.
1678
    )"
1679
  );
1680
  // clang-format on
1681
}
661✔
1682

1683
// Wrap LBS solver
1684
void
1685
WrapDiscreteOrdinatesKEigenAcceleration(py::module& slv)
661✔
1686
{
1687
  // clang-format off
1688
  // discrete ordinates k-eigen acceleration base
1689
  auto acceleration = py::class_<DiscreteOrdinatesKEigenAcceleration,
661✔
1690
                                 std::shared_ptr<DiscreteOrdinatesKEigenAcceleration>>(
1691
    slv,
1692
    "DiscreteOrdinatesKEigenAcceleration",
1693
    R"(
1694
    Base class for discrete ordinates k-eigenvalue acceleration methods.
1695

1696
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesKEigenAcceleration`.
1697
    )"
1698
  );
661✔
1699
  // SCDSA acceleration
1700
  auto scdsa_acceleration = py::class_<SCDSAAcceleration,
661✔
1701
                                       std::shared_ptr<SCDSAAcceleration>,
1702
                                       DiscreteOrdinatesKEigenAcceleration>(
1703
    slv,
1704
    "SCDSAAcceleration",
1705
    R"(
1706
    Construct an SCDSA accelerator for the power iteration k-eigenvalue solver.
1707

1708
    Wrapper of :cpp:class:`opensn::SCDSAAcceleration`.
1709
    )"
1710
  );
661✔
1711
  scdsa_acceleration.def(
1,322✔
1712
    py::init(
661✔
1713
      [](py::kwargs& params)
8✔
1714
      {
1715
        return SCDSAAcceleration::Create(kwargs_to_param_block(params));
8✔
1716
      }
1717
    ),
1718
    R"(
1719
    SCDSA acceleration for the power iteration k-eigenvalue solver.
1720

1721
    Parameters
1722
    ----------
1723
    problem: pyopensn.solver.LBSProblem
1724
        Existing DiscreteOrdinatesProblem instance.
1725
    l_abs_tol: float, defauilt=1.0e-10
1726
        Absolute residual tolerance.
1727
    max_iters: int, default=100
1728
        Maximum allowable iterations.
1729
    verbose: bool, default=False
1730
        If true, enables verbose output.
1731
    petsc_options: str, default="ssss"
1732
        Additional PETSc options.
1733
    pi_max_its: int, default=50
1734
        Maximum allowable iterations for inner power iterations.
1735
    pi_k_tol: float, default=1.0e-10
1736
        k-eigenvalue tolerance for the inner power iterations.
1737
    sdm: str, default="pwld"
1738
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1739
            - 'pwld' : Piecewise Linear Discontinuous
1740
            - 'pwlc' : Piecewise Linear Continuous
1741
    )"
1742
  );
1743
  // SMM acceleration
1744
  auto smm_acceleration = py::class_<SMMAcceleration,
661✔
1745
                                     std::shared_ptr<SMMAcceleration>,
1746
                                     DiscreteOrdinatesKEigenAcceleration>(
1747
    slv,
1748
    "SMMAcceleration",
1749
    R"(
1750
    Construct an SMM accelerator for the power iteration k-eigenvalue solver.
1751

1752
    Wrapper of :cpp:class:`opensn::SMMAcceleration`.
1753
    )"
1754
  );
661✔
1755
  smm_acceleration.def(
1,322✔
1756
    py::init(
661✔
1757
      [](py::kwargs& params)
4✔
1758
      {
1759
        return SMMAcceleration::Create(kwargs_to_param_block(params));
4✔
1760
      }
1761
    ),
1762
    R"(
1763
    SMM acceleration for the power iteration k-eigenvalue solver.
1764

1765
    Warnings
1766
    --------
1767
       SMM acceleration is **experimental** and should be used with caution!
1768
       SMM accleration only supports problems with isotropic scattering.
1769

1770
    Parameters
1771
    ----------
1772
    problem: pyopensn.solver.LBSProblem
1773
        Existing DiscreteOrdinatesProblem instance.
1774
    l_abs_tol: float, defauilt=1.0e-10
1775
        Absolute residual tolerance.
1776
    max_iters: int, default=100
1777
        Maximum allowable iterations.
1778
    verbose: bool, default=False
1779
        If true, enables verbose output.
1780
    petsc_options: str, default="ssss"
1781
        Additional PETSc options.
1782
    pi_max_its: int, default=50
1783
        Maximum allowable iterations for inner power iterations.
1784
    pi_k_tol: float, default=1.0e-10
1785
        k-eigenvalue tolerance for the inner power iterations.
1786
    sdm: str, default="pwld"
1787
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1788
            - 'pwld' : Piecewise Linear Discontinuous
1789
            - 'pwlc' : Piecewise Linear Continuous
1790
    )"
1791
  );
1792
  // clang-format on
1793
}
661✔
1794

1795
// Wrap the solver components of OpenSn
1796
void
1797
py_solver(py::module& pyopensn)
72✔
1798
{
1799
  py::module slv = pyopensn.def_submodule("solver", "Solver module.");
72✔
1800
  WrapProblem(slv);
72✔
1801
  WrapSolver(slv);
72✔
1802
  WrapLBS(slv);
72✔
1803
  WrapSteadyState(slv);
72✔
1804
  WrapTransient(slv);
72✔
1805
  WrapNLKEigen(slv);
72✔
1806
  WrapDiscreteOrdinatesKEigenAcceleration(slv);
72✔
1807
  WrapPIteration(slv);
72✔
1808
}
72✔
1809

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