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

Open-Sn / opensn / 24227755950

09 Apr 2026 02:00PM UTC coverage: 75.028%. Remained the same
24227755950

push

github

web-flow
Merge pull request #1021 from wdhawkins/pr_template

Adding PR checklist to remind developers to update documentation.

21019 of 28015 relevant lines covered (75.03%)

66524754.07 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;
434✔
106
      for (unsigned int group = 0; group < self.GetNumGroups(); group++)
22,035✔
107
      {
108
        if (only_scalar_flux)
21,601✔
109
        {
110
          field_function_list_per_group.append(self.CreateScalarFluxFieldFunction(group, 0));
25,240✔
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;
434✔
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
661✔
160
  );
161
  lbs_problem.def(
1,322✔
162
    "CreateFieldFunction",
163
    static_cast<std::shared_ptr<FieldFunctionGridBased> (LBSProblem::*)(
1,322✔
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,322✔
199
    py::arg("xs_name"),
661✔
200
    py::arg("power_normalization_target") = -1.0
661✔
201
  );
202
  lbs_problem.def(
661✔
203
    "GetTime",
204
    &LBSProblem::GetTime,
661✔
205
    R"(
206
    Get the current simulation time in seconds.
207
    )"
208
  );
209
  lbs_problem.def(
661✔
210
    "GetTimeStep",
211
    &LBSProblem::GetTimeStep,
661✔
212
    R"(
213
    Get the current timestep size.
214
    )"
215
  );
216
  lbs_problem.def(
661✔
217
    "ComputeFissionRate",
218
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
661✔
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")
661✔
256
  );
257
  lbs_problem.def(
661✔
258
    "ComputeFissionProduction",
259
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
1,457✔
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")
661✔
297
  );
298
  lbs_problem.def(
661✔
299
    "GetPhiOldLocal",
300
    [](LBSProblem& self)
709✔
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(
661✔
314
    "GetPhiNewLocal",
315
    [](LBSProblem& self)
717✔
316
    {
317
      return convert_vector(self.GetPhiNewLocal());
56✔
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(
661✔
329
    "WriteFluxMoments",
330
    [](LBSProblem& self, const std::string& file_base)
693✔
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")
661✔
343
  );
344
  lbs_problem.def(
661✔
345
    "CreateAndWriteSourceMoments",
346
    [](LBSProblem& self, const std::string& file_base)
665✔
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")
661✔
360
  );
361
  lbs_problem.def(
661✔
362
    "ReadFluxMomentsAndMakeSourceMoments",
363
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
661✔
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,322✔
385
    py::arg("single_file_flag")
661✔
386
  );
387
  lbs_problem.def(
661✔
388
    "ReadSourceMoments",
389
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
665✔
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,322✔
406
    py::arg("single_file_flag")
661✔
407
  );
408
  lbs_problem.def(
661✔
409
    "ReadFluxMoments",
410
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
661✔
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,322✔
425
    py::arg("single_file_flag")
661✔
426
  );
427
  lbs_problem.def(
661✔
428
    "WriteAngularFluxes",
429
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
665✔
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")
661✔
442
  );
443
  lbs_problem.def(
661✔
444
    "ReadAngularFluxes",
445
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
665✔
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")
661✔
458
  );
459
  lbs_problem.def(
661✔
460
    "SetPointSources",
461
    [](LBSProblem& self, py::kwargs& params)
661✔
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(
661✔
492
    "SetVolumetricSources",
493
    [](LBSProblem& self, py::kwargs& params)
697✔
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(
661✔
524
    "SetXSMap",
525
    [](LBSProblem& self, py::kwargs& params)
833✔
526
    {
527
      BlockID2XSMap xs_map;
172✔
528
      for (auto [key, value] : params)
516✔
529
      {
530
        auto c_key = key.cast<std::string>();
172✔
531
        if (c_key == "xs_map")
172✔
532
        {
533
          auto xs_entries = value.cast<py::list>();
172✔
534
          for (auto entry : xs_entries)
516✔
535
          {
536
            InputParameters xs_entry_pars = LBSProblem::GetXSMapEntryBlock();
172✔
537
            xs_entry_pars.AssignParameters(pyobj_to_param_block("", entry.cast<py::dict>()));
172✔
538
            const auto& block_ids =
172✔
539
              xs_entry_pars.GetParam("block_ids").GetVectorValue<unsigned int>();
172✔
540
            auto xs = xs_entry_pars.GetSharedPtrParam<MultiGroupXS>("xs");
172✔
541
            for (const auto& block_id : block_ids)
344✔
542
              xs_map[block_id] = xs;
172✔
543
          }
172✔
544
        }
172✔
545
        else
546
          throw std::runtime_error("Invalid argument provided to SetXSMap.\n");
×
547
      }
172✔
548
      self.SetBlockID2XSMap(xs_map);
172✔
549
    },
172✔
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
    Forward/adjoint mode toggles via :meth:`LBSProblem.SetAdjoint` do not change this map.
565
    The ``MultiGroupXS`` objects themselves are mutable and shared by pointer. If the same
566
    ``MultiGroupXS`` handle is shared across multiple problems, toggling adjoint mode in one
567
    problem also changes the transport mode seen by the others.
568
    )"
569
  );
570
  lbs_problem.def(
661✔
571
    "ZeroPhi",
572
    [](LBSProblem& self)
661✔
573
    {
574
      self.ZeroPhi();
×
575
    },
576
    R"(
577
    Zero the scalar-flux vectors (``phi_old`` and ``phi_new``) in-place.
578
    )"
579
  );
580
  lbs_problem.def(
661✔
581
    "ZeroPsi",
582
    [](LBSProblem& self)
661✔
583
    {
584
      self.ZeroPsi();
×
585
    },
586
    R"(
587
    Zero the angular-flux storage (if present for this problem type).
588
    )"
589
  );
590
  lbs_problem.def(
661✔
591
    "SetAdjoint",
592
    [](LBSProblem& self, bool adjoint)
661✔
593
    {
594
      self.SetAdjoint(adjoint);
24✔
595
    },
596
    py::arg("adjoint") = true,
661✔
597
    R"(
598
    Set forward/adjoint transport mode.
599

600
    Parameters
601
    ----------
602
    adjoint: bool, default=True
603
        ``True`` enables adjoint mode and ``False`` enables forward mode.
604

605
    Notes
606
    -----
607
    This is one of two supported mode-setting paths in Python:
608
      1. ``options={'adjoint': ...}`` in the problem constructor.
609
      2. ``SetAdjoint(...)`` (this method).
610

611
    If this changes mode, OpenSn performs a full mode-transition reset:
612
      - Materials are reinitialized in the selected mode.
613
      - Point and volumetric sources are cleared.
614
      - Boundary conditions are cleared.
615
      - Scalar and angular flux vectors are zeroed.
616

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

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

623
    After a mode change, reapply the desired driving terms before solving, typically:
624
      - :meth:`LBSProblem.SetPointSources`
625
      - :meth:`LBSProblem.SetVolumetricSources`
626
      - :meth:`DiscreteOrdinatesProblem.SetBoundaryOptions`
627

628
    This routine is intentionally destructive with respect to source/boundary/flux state
629
    to avoid hidden coupling between forward and adjoint setups.
630
    )"
631
  );
632
  lbs_problem.def(
661✔
633
    "SetForward",
634
    &LBSProblem::SetForward,
661✔
635
    R"(
636
    Set forward transport mode.
637

638
    Equivalent to ``SetAdjoint(False)``.
639
    )"
640
  );
641
  lbs_problem.def(
661✔
642
    "IsAdjoint",
643
    &LBSProblem::IsAdjoint,
661✔
644
    R"(
645
    Return ``True`` if the problem is in adjoint mode, otherwise ``False``.
646
    )"
647
  );
648
  lbs_problem.def(
661✔
649
    "IsTimeDependent",
650
    &LBSProblem::IsTimeDependent,
661✔
651
    R"(
652
    Return ``True`` if the problem is in time-dependent mode, otherwise ``False``.
653
    )"
654
  );
655

656
  // discrete ordinate solver
657
  auto do_problem = py::class_<DiscreteOrdinatesProblem, std::shared_ptr<DiscreteOrdinatesProblem>,
661✔
658
                               LBSProblem>(
659
    slv,
660
    "DiscreteOrdinatesProblem",
661
    R"(
662
    Base class for discrete ordinates problems in Cartesian geometry.
663

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

668
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesProblem`.
669
    )"
670
  );
661✔
671
  do_problem.def(
1,322✔
672
    py::init(
661✔
673
      [](py::kwargs& params)
563✔
674
      {
675
        return DiscreteOrdinatesProblem::Create(kwargs_to_param_block(params));
563✔
676
      }
677
    ),
678
    R"(
679
    Construct a discrete ordinates problem with Cartesian geometry.
680

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

788
    Notes
789
    -----
790
    Switch problem from steady-state to time-dependent mode. This updates problem
791
    internals (sweep chunk mode and source-function) while preserving user boundary
792
    conditions and fixed sources.
793

794
    Requires ``options.save_angular_flux=True`` at problem creation.
795
    )"
796
  );
797
  do_problem.def(
661✔
798
    "SetSteadyStateMode",
799
    &DiscreteOrdinatesProblem::SetSteadyStateMode,
661✔
800
    R"(
801
    Set the problem to steady-state mode.
802

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

843
    Parameters
844
    ----------
845
    clear_boundary_conditions: bool, default=False
846
        If true, all current boundary conditions are deleted.
847
    boundary_conditions: List[Dict]
848
        A list of boundary condition dictionaries. Each dictionary supports:
849
          - name: str (required)
850
              Boundary name that identifies the specific boundary.
851
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
852
              Boundary type specification.
853
          - group_strength: List[float], optional
854
              Required when ``type='isotropic'``. Isotropic strength per group.
855
          - function: AngularFluxFunction, optional
856
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
857

858
    Notes
859
    -----
860
    Mode transitions via :meth:`LBSProblem.SetAdjoint` clear all boundary conditions.
861
    Reapply boundaries with this method before solving in the new mode.
862
    )"
863
  );
864
  do_problem.def(
661✔
865
    "GetPsi",
866
    [](DiscreteOrdinatesProblem& self)
669✔
867
    {
868
      const auto& psi = self.GetPsiNewLocal();
8✔
869
      py::list psi_list;
8✔
870
      for (const auto& vec : psi)
16✔
871
      {
872
        auto array = py::array_t<double>(static_cast<py::ssize_t>(vec.size()),
8✔
873
                                         vec.data(),
874
                                         py::cast(self));
8✔
875
        psi_list.append(array);
8✔
876
      }
8✔
877
      return psi_list;
8✔
878
    },
×
879
    R"(
880
    Return psi as a list of NumPy arrays (float64), using zero-copy views into the
881
    underlying data.
882
    )"
883
  );
884
  do_problem.def(
661✔
885
    "GetAngularFieldFunctionList",
886
    [](DiscreteOrdinatesProblem& self, py::list groups, py::list angles)
661✔
887
    {
888
      std::vector<unsigned int> group_ids;
×
889
      std::vector<size_t> angle_ids;
×
890
      group_ids.reserve(groups.size());
×
891
      angle_ids.reserve(angles.size());
×
892

893
      for (py::handle g : groups)
×
894
        group_ids.push_back(g.cast<unsigned int>());
×
895
      for (py::handle a : angles)
×
896
        angle_ids.push_back(a.cast<size_t>());
×
897

898
      auto ff_list = self.CreateAngularFluxFieldFunctionList(group_ids, angle_ids);
×
899
      py::list out;
×
900
      for (const auto& ff : ff_list)
×
901
        out.append(ff);
×
902
      return out;
×
903
    },
×
904
    R"(
905
    Create field functions for selected angular flux components.
906

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

911
    Example
912
    -------
913
    .. code-block:: python
914

915
       solver.Initialize()
916
       solver.Execute()
917
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
918

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

924
    1) Use ``TransientSolver.Execute()`` with a post-advance callback:
925

926
    .. code-block:: python
927

928
       solver = TransientSolver(problem=phys)
929
       solver.Initialize()
930
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
931

932
       def post_advance():
933
           for ff in ang_ff:
934
               ff.Update()
935
           FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
936

937
       solver.SetPostAdvanceCallback(post_advance)
938
       solver.Execute()
939

940
    2) Use a custom Python loop with ``TransientSolver.Advance()``:
941

942
    .. code-block:: python
943

944
       solver = TransientSolver(problem=phys)
945
       solver.Initialize()
946
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
947
       for _ in range(num_steps):
948
           solver.Advance()
949
           for ff in ang_ff:
950
               ff.Update()
951
           FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
952

953
    Parameters
954
    ----------
955
    groups : List[int]
956
        Global group indices to export.
957
    angles : List[int]
958
        Angle indices within the groupset quadrature for each group.
959

960
    Returns
961
    -------
962
    List[pyopensn.fieldfunc.FieldFunctionGridBased]
963
        Field functions for the requested ``(group, angle)`` pairs. Each returned field function
964
        is a snapshot, but supports ``CanUpdate()`` and ``Update()`` while its owning problem is
965
        alive.
966
    )",
967
    py::arg("groups"),
1,322✔
968
    py::arg("angles")
661✔
969
  );
970
  do_problem.def(
661✔
971
    "ComputeLeakage",
972
    [](DiscreteOrdinatesProblem& self, py::list bnd_names)
700✔
973
    {
974
      auto grid = self.GetGrid();
39✔
975
      // get the supported boundaries
976
      std::map<std::string, std::uint64_t> allowed_bd_names = grid->GetBoundaryNameMap();
39✔
977
      std::map<std::uint64_t, std::string> allowed_bd_ids = grid->GetBoundaryIDMap();
39✔
978
      const auto coord_sys = grid->GetCoordinateSystem();
39✔
979
      const auto mesh_type = grid->GetType();
39✔
980
      const auto dim = grid->GetDimension();
39✔
981
      // get the boundaries to parse, preserving user order
982
      std::vector<std::uint64_t> bndry_ids;
39✔
983
      if (bnd_names.size() > 1)
39✔
984
      {
985
        for (py::handle name : bnd_names)
184✔
986
        {
987
          auto sname = name.cast<std::string>();
112✔
988
          if (coord_sys == CoordinateSystemType::CYLINDRICAL && dim == 2)
112✔
989
          {
990
            if (sname == "xmin" || sname == "xmax" || sname == "ymin" || sname == "ymax")
24✔
991
              throw std::runtime_error("ComputeLeakage: Boundary name '" + sname +
×
992
                                       "' is invalid for cylindrical orthogonal meshes. "
993
                                       "Use rmin, rmax, zmin, zmax.");
×
994

995
            if (mesh_type == MeshType::ORTHOGONAL)
24✔
996
            {
997
              if (sname == "rmin") sname = "xmin";
24✔
998
              else if (sname == "rmax") sname = "xmax";
24✔
999
              else if (sname == "zmin") sname = "ymin";
16✔
1000
              else if (sname == "zmax") sname = "ymax";
8✔
1001
            }
1002
          }
1003
          bndry_ids.push_back(allowed_bd_names.at(sname));
112✔
1004
        }
112✔
1005
      }
1006
      else
1007
      {
1008
        bndry_ids = self.GetGrid()->GetUniqueBoundaryIDs();
9✔
1009
      }
1010
      // compute the leakage
1011
      std::map<std::uint64_t, std::vector<double>> leakage = ComputeLeakage(self, bndry_ids);
39✔
1012
      const bool rz_ortho = (coord_sys == CoordinateSystemType::CYLINDRICAL &&
78✔
1013
                             mesh_type == MeshType::ORTHOGONAL && dim == 2);
39✔
1014

1015
      auto to_rz_name = [](const std::string& name)
63✔
1016
      {
1017
        if (name == "xmin") return std::string("rmin");
24✔
1018
        if (name == "xmax") return std::string("rmax");
24✔
1019
        if (name == "ymin") return std::string("zmin");
16✔
1020
        if (name == "ymax") return std::string("zmax");
8✔
1021
        return name;
×
1022
      };
1023

1024
      // convert result to native Python
1025
      py::dict result;
39✔
1026
      for (const auto& bndry_id : bndry_ids)
157✔
1027
      {
1028
        const auto it = leakage.find(bndry_id);
118✔
1029
        if (it == leakage.end())
118✔
1030
          continue;
×
1031
        // construct numpy array and copy contents
1032
        const auto& grp_wise_leakage = it->second;
118✔
1033
        py::array_t<double> np_vector(py::ssize_t(grp_wise_leakage.size()));
118✔
1034
        auto buffer = np_vector.request();
118✔
1035
        auto *np_vector_data = static_cast<double*>(buffer.ptr);
118✔
1036
        std::copy(grp_wise_leakage.begin(), grp_wise_leakage.end(), np_vector_data);
118✔
1037
        std::string name = allowed_bd_ids.at(bndry_id);
118✔
1038
        if (rz_ortho)
118✔
1039
          name = to_rz_name(name);
24✔
1040
        result[py::str(name)] = std::move(np_vector);
236✔
1041
      }
118✔
1042

1043
      return result;
39✔
1044
    },
78✔
1045
    R"(
1046
    Compute leakage for the problem.
1047

1048
    Parameters
1049
    ----------
1050
    bnd_names : List[str]
1051
        A list of boundary names for which leakage should be computed.
1052

1053
    Returns
1054
    -------
1055
    Dict[str, numpy.ndarray]
1056
        A dictionary mapping boundary names to group-wise leakage vectors.
1057
        Each array contains the outgoing angular flux (per group) integrated over
1058
        the corresponding boundary surface.
1059

1060
    Raises
1061
    ------
1062
    RuntimeError
1063
        If `save_angular_flux` option was not enabled during problem setup.
1064

1065
    ValueError
1066
        If one or more boundary ids are not present on the current mesh.
1067
    )",
1068
    py::arg("bnd_names")
661✔
1069
  );
1070

1071
  // discrete ordinates curvilinear problem
1072
  auto do_curvilinear_problem = py::class_<DiscreteOrdinatesCurvilinearProblem,
661✔
1073
                                           std::shared_ptr<DiscreteOrdinatesCurvilinearProblem>,
1074
                                           DiscreteOrdinatesProblem>(
1075
    slv,
1076
    "DiscreteOrdinatesCurvilinearProblem",
1077
    R"(
1078
    Base class for discrete ordinates problems in curvilinear geometry.
1079

1080
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesCurvilinearProblem`.
1081
    )"
1082
  );
661✔
1083
  do_curvilinear_problem.def(
1,322✔
1084
    py::init(
661✔
1085
      [](py::kwargs& params)
80✔
1086
      {
1087
        return DiscreteOrdinatesCurvilinearProblem::Create(kwargs_to_param_block(params));
80✔
1088
      }
1089
    ),
1090
    R"(
1091
    Construct a discrete ordinates problem for curvilinear geometry.
1092

1093
    Warnings
1094
    --------
1095
       DiscreteOrdinatesCurvilinearProblem is **experimental** and should be used with caution!
1096

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

1193
// Wrap steady-state solver
1194
void
1195
WrapSteadyState(py::module& slv)
661✔
1196
{
1197
  const auto balance_table_to_dict = [](const BalanceTable& table)
680✔
1198
  {
1199
    py::dict values;
19✔
1200
    values["absorption_rate"] = table.absorption_rate;
19✔
1201
    values["production_rate"] = table.production_rate;
19✔
1202
    values["inflow_rate"] = table.inflow_rate;
19✔
1203
    values["outflow_rate"] = table.outflow_rate;
19✔
1204
    values["balance"] = table.balance;
19✔
1205
    if (table.initial_inventory.has_value())
19✔
1206
      values["initial_inventory"] = table.initial_inventory.value();
×
1207
    if (table.final_inventory.has_value())
19✔
1208
      values["final_inventory"] = table.final_inventory.value();
×
1209
    if (table.predicted_inventory_change.has_value())
19✔
1210
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1211
    if (table.actual_inventory_change.has_value())
19✔
1212
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1213
    if (table.inventory_residual.has_value())
19✔
1214
      values["inventory_residual"] = table.inventory_residual.value();
×
1215
    return values;
19✔
1216
  };
×
1217

1218
  // clang-format off
1219
  // steady state solver
1220
  auto steady_state_solver = py::class_<SteadyStateSourceSolver, std::shared_ptr<SteadyStateSourceSolver>,
661✔
1221
                                        Solver>(
1222
    slv,
1223
    "SteadyStateSourceSolver",
1224
    R"(
1225
    Steady state solver.
1226

1227
    Wrapper of :cpp:class:`opensn::SteadyStateSourceSolver`.
1228
    )"
1229
  );
661✔
1230
  steady_state_solver.def(
1,322✔
1231
    py::init(
661✔
1232
      [](py::kwargs& params)
407✔
1233
      {
1234
        return SteadyStateSourceSolver::Create(kwargs_to_param_block(params));
407✔
1235
      }
1236
    ),
1237
    R"(
1238
    Construct a steady state solver.
1239

1240
    Parameters
1241
    ----------
1242
    pyopensn.solver.LBSProblem : LBSProblem
1243
        Existing LBSProblem instance.
1244
    )"
1245
  );
1246
  steady_state_solver.def(
661✔
1247
    "ComputeBalanceTable",
1248
    [balance_table_to_dict](const SteadyStateSourceSolver& self)
661✔
1249
    {
1250
      return balance_table_to_dict(self.ComputeBalanceTable());
19✔
1251
    },
1252
    R"(
1253
    Compute and return the global balance table using the solver's normalization.
1254
    This is a collective operation and must be called on all ranks.
1255

1256
    Returns
1257
    -------
1258
    dict
1259
        Dictionary with the following entries:
1260

1261
        - ``absorption_rate``:
1262
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1263
          groups and the full domain.
1264
        - ``production_rate``:
1265
          Global volumetric production/source rate used by the solver,
1266
          approximately ``integral Q dV`` summed over groups and the full domain.
1267
        - ``inflow_rate``:
1268
          Global incoming boundary contribution integrated over incoming
1269
          angular flux on boundaries.
1270
        - ``outflow_rate``:
1271
          Global outgoing boundary contribution accumulated from face outflow
1272
          tallies.
1273
        - ``balance``:
1274
          Rate balance,
1275
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1276

1277
    Notes
1278
    -----
1279
    This solver applies no extra normalization to the balance table.
1280
    )"
1281
  );
1282
  // clang-format on
1283
}
661✔
1284

1285
// Wrap transient solver
1286
void
1287
WrapTransient(py::module& slv)
661✔
1288
{
1289
  const auto balance_table_to_dict = [](const BalanceTable& table)
661✔
1290
  {
1291
    py::dict values;
×
1292
    values["absorption_rate"] = table.absorption_rate;
×
1293
    values["production_rate"] = table.production_rate;
×
1294
    values["inflow_rate"] = table.inflow_rate;
×
1295
    values["outflow_rate"] = table.outflow_rate;
×
1296
    values["balance"] = table.balance;
×
1297
    if (table.initial_inventory.has_value())
×
1298
      values["initial_inventory"] = table.initial_inventory.value();
×
1299
    if (table.final_inventory.has_value())
×
1300
      values["final_inventory"] = table.final_inventory.value();
×
1301
    if (table.predicted_inventory_change.has_value())
×
1302
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1303
    if (table.actual_inventory_change.has_value())
×
1304
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1305
    if (table.inventory_residual.has_value())
×
1306
      values["inventory_residual"] = table.inventory_residual.value();
×
1307
    return values;
×
1308
  };
×
1309
  // clang-format off
1310
  auto transient_solver =
661✔
1311
    py::class_<TransientSolver, std::shared_ptr<TransientSolver>, Solver>(
1312
      slv,
1313
      "TransientSolver",
1314
      R"(
1315
      Transient solver.
1316

1317
      Wrapper of :cpp:class:`opensn::TransientSolver`.
1318
      )"
1319
    );
661✔
1320
  transient_solver.def(
1,322✔
1321
    py::init(
661✔
1322
      [](py::kwargs& params)
160✔
1323
      {
1324
        return TransientSolver::Create(kwargs_to_param_block(params));
160✔
1325
      }
1326
    ),
1327
    R"(
1328
    Construct a transient solver.
1329

1330
    Parameters
1331
    ----------
1332
    pyopensn.solver.DiscreteOrdinatesProblem : DiscreteOrdinatesProblem
1333
        Existing discrete ordinates problem instance.
1334
    dt : float, optional, default=2.0e-3
1335
        Time step size used during the simulation.
1336
    stop_time : float, optional, default=0.1
1337
        Simulation end time.
1338
    theta : float, optional, default=0.5
1339
        Time differencing scheme parameter.
1340
    initial_state : str, optional, default="existing"
1341
        Initial state for the transient solve. Allowed values: existing, zero.
1342
        In "zero" mode, scalar flux vectors are reset to zero.
1343
    verbose : bool, optional, default=True
1344
        Enable verbose logging.
1345

1346
    Notes
1347
    -----
1348
    The associated problem must have ``save_angular_flux=True`` enabled. This
1349
    is required for transient problems.
1350
    )"
1351
  );
1352
  transient_solver.def(
661✔
1353
    "SetTimeStep",
1354
    &TransientSolver::SetTimeStep,
661✔
1355
    R"(
1356
    Set the timestep size used by :meth:`Advance`.
1357

1358
    Parameters
1359
    ----------
1360
    dt : float
1361
        New timestep size.
1362
    )");
1363
  transient_solver.def(
661✔
1364
    "SetTheta",
1365
    &TransientSolver::SetTheta,
661✔
1366
    R"(
1367
    Set the theta parameter used by :meth:`Advance`.
1368

1369
    Parameters
1370
    ----------
1371
    theta : float
1372
        Theta value between 1.0e-16 and 1.
1373
    )");
1374
  transient_solver.def(
661✔
1375
    "Advance",
1376
    &TransientSolver::Advance,
661✔
1377
    R"(
1378
    Advance the solver by a single timestep.
1379

1380
    Notes
1381
    -----
1382
    You must call :meth:`Initialize` before calling :meth:`Advance` or
1383
    :meth:`Execute`.
1384
    )");
1385
  transient_solver.def(
661✔
1386
    "SetPreAdvanceCallback",
1387
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
661✔
1388
      &TransientSolver::SetPreAdvanceCallback),
1389
    R"(
1390
    Register a callback that runs before each advance within :meth:`Execute`.
1391

1392
    Parameters
1393
    ----------
1394
    callback : Optional[Callable[[], None]]
1395
        Function invoked before the solver advances a timestep. Pass None to clear.
1396
        If the callback modifies the timestep, the new value is used for the
1397
        upcoming step.
1398
    )");
1399
  transient_solver.def(
661✔
1400
    "SetPreAdvanceCallback",
1401
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
661✔
1402
      &TransientSolver::SetPreAdvanceCallback),
1403
    "Clear the PreAdvance callback by passing None.");
1404
  transient_solver.def(
661✔
1405
    "SetPostAdvanceCallback",
1406
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
661✔
1407
      &TransientSolver::SetPostAdvanceCallback),
1408
    R"(
1409
    Register a callback that runs after each advance within :meth:`Execute`.
1410

1411
    Parameters
1412
    ----------
1413
    callback : Optional[Callable[[], None]]
1414
        Function invoked after the solver advances a timestep. Pass None to clear.
1415
    )");
1416
  transient_solver.def(
661✔
1417
    "SetPostAdvanceCallback",
1418
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
661✔
1419
      &TransientSolver::SetPostAdvanceCallback),
1420
    "Clear the PostAdvance callback by passing None.");
1421
  transient_solver.def(
661✔
1422
    "ComputeBalanceTable",
1423
    [balance_table_to_dict](const TransientSolver& self)
661✔
1424
    {
1425
      return balance_table_to_dict(self.ComputeBalanceTable());
×
1426
    },
1427
    R"(
1428
    Compute and return the global balance table using the solver's normalization.
1429
    This is a collective operation and must be called on all ranks.
1430

1431
    Returns
1432
    -------
1433
    dict
1434
        Dictionary with the following entries:
1435

1436
        - ``absorption_rate``:
1437
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1438
          groups and the full domain.
1439
        - ``production_rate``:
1440
          Global volumetric production/source rate used by the solver,
1441
          approximately ``integral Q dV`` summed over groups and the full domain.
1442
        - ``inflow_rate``:
1443
          Global incoming boundary contribution integrated over incoming
1444
          angular flux on boundaries.
1445
        - ``outflow_rate``:
1446
          Global outgoing boundary contribution accumulated from face outflow
1447
          tallies.
1448
        - ``balance``:
1449
          Rate balance,
1450
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1451
        - ``initial_inventory``:
1452
          Total particle inventory at the start of the timestep, computed as
1453
          ``integral (1 / v_g) * phi_old dV`` summed over groups and the full domain.
1454
        - ``final_inventory``:
1455
          Total particle inventory at the end of the timestep, computed as
1456
          ``integral (1 / v_g) * phi_new dV`` summed over groups and the full domain.
1457
        - ``predicted_inventory_change``:
1458
          Inventory change predicted by the current timestep balance, computed as 
1459
          ``dt * balance``.
1460
        - ``actual_inventory_change``:
1461
          Measured change in total particle inventory over the timestep, computed as
1462
          ``final_inventory - initial_inventory``.
1463
        - ``inventory_residual``:
1464
          Mismatch between the measured and predicted timestep inventory
1465
          changes, computed as
1466
          ``actual_inventory_change - predicted_inventory_change``.
1467

1468
    Notes
1469
    -----
1470
    This solver applies no extra normalization to the balance table.
1471

1472
    The transient inventory terms currently use the end-of-step rate balance to
1473
    estimate the timestep inventory change.
1474
    )"
1475
  );
1476
  slv.attr("BackwardEuler") = 1.0;
661✔
1477
  slv.attr("CrankNicolson") = 0.5;
1,322✔
1478
  // clang-format on
1479
}
661✔
1480

1481
// Wrap non-linear k-eigen solver
1482
void
1483
WrapNLKEigen(py::module& slv)
661✔
1484
{
1485
  const auto balance_table_to_dict = [](const BalanceTable& table)
661✔
1486
  {
1487
    py::dict values;
×
1488
    values["absorption_rate"] = table.absorption_rate;
×
1489
    values["production_rate"] = table.production_rate;
×
1490
    values["inflow_rate"] = table.inflow_rate;
×
1491
    values["outflow_rate"] = table.outflow_rate;
×
1492
    values["balance"] = table.balance;
×
1493
    if (table.initial_inventory.has_value())
×
1494
      values["initial_inventory"] = table.initial_inventory.value();
×
1495
    if (table.final_inventory.has_value())
×
1496
      values["final_inventory"] = table.final_inventory.value();
×
1497
    if (table.predicted_inventory_change.has_value())
×
1498
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1499
    if (table.actual_inventory_change.has_value())
×
1500
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1501
    if (table.inventory_residual.has_value())
×
1502
      values["inventory_residual"] = table.inventory_residual.value();
×
1503
    return values;
×
1504
  };
×
1505
  // clang-format off
1506
  // non-linear k-eigen solver
1507
  auto non_linear_k_eigen_solver = py::class_<NonLinearKEigenSolver, std::shared_ptr<NonLinearKEigenSolver>,
661✔
1508
                                              Solver>(
1509
    slv,
1510
    "NonLinearKEigenSolver",
1511
    R"(
1512
    Non-linear k-eigenvalue solver.
1513

1514
    Wrapper of :cpp:class:`opensn::NonLinearKEigenSolver`.
1515
    )"
1516
  );
661✔
1517
  non_linear_k_eigen_solver.def(
1,322✔
1518
    py::init(
661✔
1519
      [](py::kwargs& params)
37✔
1520
      {
1521
        return NonLinearKEigenSolver::Create(kwargs_to_param_block(params));
37✔
1522
      }
1523
        ),
1524
    R"(
1525
    Construct a non-linear k-eigenvalue solver.
1526

1527
    Parameters
1528
    ----------
1529
    lbs_problem: pyopensn.solver.LBSProblem
1530
        Existing LBSProblem instance.
1531
    nl_abs_tol: float, default=1.0e-8
1532
        Non-linear absolute tolerance.
1533
    nl_rel_tol: float, default=1.0e-8
1534
        Non-linear relative tolerance.
1535
    nl_sol_tol: float, default=1.0e-50
1536
        Non-linear solution tolerance.
1537
    nl_max_its: int, default=50
1538
        Non-linear algorithm maximum iterations.
1539
    l_abs_tol: float, default=1.0e-8
1540
        Linear absolute tolerance.
1541
    l_rel_tol: float, default=1.0e-8
1542
        Linear relative tolerance.
1543
    l_div_tol: float, default=1.0e6
1544
        Linear divergence tolerance.
1545
    l_max_its: int, default=50
1546
        Linear algorithm maximum iterations.
1547
    l_gmres_restart_intvl: int, default=30
1548
        GMRES restart interval.
1549
    l_gmres_breakdown_tol: float, default=1.0e6
1550
        GMRES breakdown tolerance.
1551
    reset_phi0: bool, default=True
1552
        If true, reinitializes scalar fluxes to 1.0.
1553
    num_initial_power_iterations: int, default=0
1554
        Number of initial power iterations before the non-linear solve.
1555
    )"
1556
  );
1557
  non_linear_k_eigen_solver.def(
661✔
1558
    "GetEigenvalue",
1559
    &NonLinearKEigenSolver::GetEigenvalue,
661✔
1560
    R"(
1561
    Return the current k-eigenvalue.
1562
    )"
1563
  );
1564
  non_linear_k_eigen_solver.def(
661✔
1565
    "ComputeBalanceTable",
1566
    [balance_table_to_dict](const NonLinearKEigenSolver& self)
661✔
1567
    {
1568
      return balance_table_to_dict(self.ComputeBalanceTable());
×
1569
    },
1570
    R"(
1571
    Compute and return the global balance table using the solver's normalization.
1572
    This is a collective operation and must be called on all ranks.
1573

1574
    Returns
1575
    -------
1576
    dict
1577
        Dictionary with the following entries:
1578

1579
        - ``absorption_rate``:
1580
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1581
          groups and the full domain.
1582
        - ``production_rate``:
1583
          Global volumetric production/source rate used by the solver,
1584
          approximately ``integral Q dV`` summed over groups and the full domain.
1585
        - ``inflow_rate``:
1586
          Global incoming boundary contribution integrated over incoming
1587
          angular flux on boundaries.
1588
        - ``outflow_rate``:
1589
          Global outgoing boundary contribution accumulated from face outflow
1590
          tallies.
1591
        - ``balance``:
1592
          Rate balance,
1593
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1594

1595
    Notes
1596
    -----
1597
    For k-eigenvalue balance reporting, this solver scales the production term by
1598
    ``1 / k_eff`` before forming both ``production_rate`` and ``balance``.
1599
    )"
1600
  );
1601
  // clang-format on
1602
}
661✔
1603

1604
// Wrap power iteration solvers
1605
void
1606
WrapPIteration(py::module& slv)
661✔
1607
{
1608
  const auto balance_table_to_dict = [](const BalanceTable& table)
665✔
1609
  {
1610
    py::dict values;
4✔
1611
    values["absorption_rate"] = table.absorption_rate;
4✔
1612
    values["production_rate"] = table.production_rate;
4✔
1613
    values["inflow_rate"] = table.inflow_rate;
4✔
1614
    values["outflow_rate"] = table.outflow_rate;
4✔
1615
    values["balance"] = table.balance;
4✔
1616
    if (table.initial_inventory.has_value())
4✔
1617
      values["initial_inventory"] = table.initial_inventory.value();
×
1618
    if (table.final_inventory.has_value())
4✔
1619
      values["final_inventory"] = table.final_inventory.value();
×
1620
    if (table.predicted_inventory_change.has_value())
4✔
1621
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1622
    if (table.actual_inventory_change.has_value())
4✔
1623
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1624
    if (table.inventory_residual.has_value())
4✔
1625
      values["inventory_residual"] = table.inventory_residual.value();
×
1626
    return values;
4✔
1627
  };
×
1628
  // clang-format off
1629
  // power iteration k-eigen solver
1630
  auto pi_k_eigen_solver = py::class_<PowerIterationKEigenSolver, std::shared_ptr<PowerIterationKEigenSolver>,
661✔
1631
                                      Solver>(
1632
    slv,
1633
    "PowerIterationKEigenSolver",
1634
    R"(
1635
    Power iteration k-eigenvalue solver.
1636

1637
    Wrapper of :cpp:class:`opensn::PowerIterationKEigenSolver`.
1638
    )"
1639
  );
661✔
1640
  pi_k_eigen_solver.def(
1,322✔
1641
    py::init(
661✔
1642
      [](py::kwargs& params)
153✔
1643
      {
1644
        return PowerIterationKEigenSolver::Create(kwargs_to_param_block(params));
153✔
1645
      }
1646
    ),
1647
    R"(
1648
    Construct a power iteration k-eigen solver.
1649

1650
    Parameters
1651
    ----------
1652
    problem: pyopensn.solver.LBSProblem
1653
        Existing DiscreteOrdinatesProblem instance.
1654
    acceleration: pyopensn.solver.DiscreteOrdinatesKEigenAcceleration
1655
        Optional DiscreteOrdinatesKEigenAcceleration instance for acceleration.
1656
    max_iters: int, default = 1000
1657
        Maximum power iterations allowed.
1658
    k_tol: float, default = 1.0e-10
1659
        Tolerance on the k-eigenvalue.
1660
    reset_solution: bool, default=True
1661
        If true, initialize flux moments to 1.0.
1662
    reset_phi0: bool, default=True
1663
        If true, reinitializes scalar fluxes to 1.0.
1664
    )"
1665
  );
1666
  pi_k_eigen_solver.def(
661✔
1667
    "GetEigenvalue",
1668
    &PowerIterationKEigenSolver::GetEigenvalue,
661✔
1669
    R"(
1670
    Return the current k-eigenvalue.
1671
    )"
1672
  );
1673
  pi_k_eigen_solver.def(
661✔
1674
    "ComputeBalanceTable",
1675
    [balance_table_to_dict](const PowerIterationKEigenSolver& self)
661✔
1676
    {
1677
      return balance_table_to_dict(self.ComputeBalanceTable());
4✔
1678
    },
1679
    R"(
1680
    Compute and return the global balance table using the solver's normalization.
1681
    This is a collective operation and must be called on all ranks.
1682

1683
    Returns
1684
    -------
1685
    dict
1686
        Dictionary with the following entries:
1687

1688
        - ``absorption_rate``:
1689
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1690
          groups and the full domain.
1691
        - ``production_rate``:
1692
          Global volumetric production/source rate used by the solver,
1693
          approximately ``integral Q dV`` summed over groups and the full domain.
1694
        - ``inflow_rate``:
1695
          Global incoming boundary contribution integrated over incoming
1696
          angular flux on boundaries.
1697
        - ``outflow_rate``:
1698
          Global outgoing boundary contribution accumulated from face outflow
1699
          tallies.
1700
        - ``balance``:
1701
          Rate balance,
1702
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1703

1704
    Notes
1705
    -----
1706
    For k-eigenvalue balance reporting, this solver scales the production term by
1707
    ``1 / k_eff`` before forming both ``production_rate`` and ``balance``.
1708
    )"
1709
  );
1710
  // clang-format on
1711
}
661✔
1712

1713
// Wrap LBS solver
1714
void
1715
WrapDiscreteOrdinatesKEigenAcceleration(py::module& slv)
661✔
1716
{
1717
  // clang-format off
1718
  // discrete ordinates k-eigen acceleration base
1719
  auto acceleration = py::class_<DiscreteOrdinatesKEigenAcceleration,
661✔
1720
                                 std::shared_ptr<DiscreteOrdinatesKEigenAcceleration>>(
1721
    slv,
1722
    "DiscreteOrdinatesKEigenAcceleration",
1723
    R"(
1724
    Base class for discrete ordinates k-eigenvalue acceleration methods.
1725

1726
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesKEigenAcceleration`.
1727
    )"
1728
  );
661✔
1729
  // SCDSA acceleration
1730
  auto scdsa_acceleration = py::class_<SCDSAAcceleration,
661✔
1731
                                       std::shared_ptr<SCDSAAcceleration>,
1732
                                       DiscreteOrdinatesKEigenAcceleration>(
1733
    slv,
1734
    "SCDSAAcceleration",
1735
    R"(
1736
    Construct an SCDSA accelerator for the power iteration k-eigenvalue solver.
1737

1738
    Wrapper of :cpp:class:`opensn::SCDSAAcceleration`.
1739
    )"
1740
  );
661✔
1741
  scdsa_acceleration.def(
1,322✔
1742
    py::init(
661✔
1743
      [](py::kwargs& params)
8✔
1744
      {
1745
        return SCDSAAcceleration::Create(kwargs_to_param_block(params));
8✔
1746
      }
1747
    ),
1748
    R"(
1749
    SCDSA acceleration for the power iteration k-eigenvalue solver.
1750

1751
    Parameters
1752
    ----------
1753
    problem: pyopensn.solver.LBSProblem
1754
        Existing DiscreteOrdinatesProblem instance.
1755
    l_abs_tol: float, defauilt=1.0e-10
1756
        Absolute residual tolerance.
1757
    max_iters: int, default=100
1758
        Maximum allowable iterations.
1759
    verbose: bool, default=False
1760
        If true, enables verbose output.
1761
    petsc_options: str, default="ssss"
1762
        Additional PETSc options.
1763
    pi_max_its: int, default=50
1764
        Maximum allowable iterations for inner power iterations.
1765
    pi_k_tol: float, default=1.0e-10
1766
        k-eigenvalue tolerance for the inner power iterations.
1767
    sdm: str, default="pwld"
1768
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1769
            - 'pwld' : Piecewise Linear Discontinuous
1770
            - 'pwlc' : Piecewise Linear Continuous
1771
    )"
1772
  );
1773
  // SMM acceleration
1774
  auto smm_acceleration = py::class_<SMMAcceleration,
661✔
1775
                                     std::shared_ptr<SMMAcceleration>,
1776
                                     DiscreteOrdinatesKEigenAcceleration>(
1777
    slv,
1778
    "SMMAcceleration",
1779
    R"(
1780
    Construct an SMM accelerator for the power iteration k-eigenvalue solver.
1781

1782
    Wrapper of :cpp:class:`opensn::SMMAcceleration`.
1783
    )"
1784
  );
661✔
1785
  smm_acceleration.def(
1,322✔
1786
    py::init(
661✔
1787
      [](py::kwargs& params)
4✔
1788
      {
1789
        return SMMAcceleration::Create(kwargs_to_param_block(params));
4✔
1790
      }
1791
    ),
1792
    R"(
1793
    SMM acceleration for the power iteration k-eigenvalue solver.
1794

1795
    Warnings
1796
    --------
1797
       SMM acceleration is **experimental** and should be used with caution!
1798
       SMM accleration only supports problems with isotropic scattering.
1799

1800
    Parameters
1801
    ----------
1802
    problem: pyopensn.solver.LBSProblem
1803
        Existing DiscreteOrdinatesProblem instance.
1804
    l_abs_tol: float, defauilt=1.0e-10
1805
        Absolute residual tolerance.
1806
    max_iters: int, default=100
1807
        Maximum allowable iterations.
1808
    verbose: bool, default=False
1809
        If true, enables verbose output.
1810
    petsc_options: str, default="ssss"
1811
        Additional PETSc options.
1812
    pi_max_its: int, default=50
1813
        Maximum allowable iterations for inner power iterations.
1814
    pi_k_tol: float, default=1.0e-10
1815
        k-eigenvalue tolerance for the inner power iterations.
1816
    sdm: str, default="pwld"
1817
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1818
            - 'pwld' : Piecewise Linear Discontinuous
1819
            - 'pwlc' : Piecewise Linear Continuous
1820
    )"
1821
  );
1822
  // clang-format on
1823
}
661✔
1824

1825
// Wrap the solver components of OpenSn
1826
void
1827
py_solver(py::module& pyopensn)
72✔
1828
{
1829
  py::module slv = pyopensn.def_submodule("solver", "Solver module.");
72✔
1830
  WrapProblem(slv);
72✔
1831
  WrapSolver(slv);
72✔
1832
  WrapLBS(slv);
72✔
1833
  WrapSteadyState(slv);
72✔
1834
  WrapTransient(slv);
72✔
1835
  WrapNLKEigen(slv);
72✔
1836
  WrapDiscreteOrdinatesKEigenAcceleration(slv);
72✔
1837
  WrapPIteration(slv);
72✔
1838
}
72✔
1839

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