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

Open-Sn / opensn / 22654533986

04 Mar 2026 12:48AM UTC coverage: 74.268% (+0.08%) from 74.192%
22654533986

push

github

web-flow
Merge pull request #960 from andrsd/fv-removal

Removing finite-volume spatial discretization

0 of 2 new or added lines in 1 file covered. (0.0%)

183 existing lines in 9 files now uncovered.

19993 of 26920 relevant lines covered (74.27%)

67230185.92 hits per line

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

84.31
/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)
672✔
38
{
39
  // clang-format off
40
  // problem base
41
  auto problem = py::class_<Problem, std::shared_ptr<Problem>>(
672✔
42
    slv,
43
    "Problem",
44
    R"(
45
    Base class for all problems.
46

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

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

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

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

98
    Wrapper of :cpp:class:`opensn::LBSProblem`.
99
    )"
100
  );
672✔
101
  lbs_problem.def(
672✔
102
    "GetScalarFluxFieldFunction",
103
    [](LBSProblem& self, bool only_scalar_flux)
672✔
104
    {
105
      py::list field_function_list_per_group;
384✔
106
      for (unsigned int group = 0; group < self.GetNumGroups(); group++)
18,449✔
107
      {
108
        if (only_scalar_flux)
18,065✔
109
        {
110
          field_function_list_per_group.append(self.GetScalarFluxFieldFunction(group, 0));
23,218✔
111
        }
112
        else
113
        {
114
          py::list field_function_list_per_moment;
6,456✔
115
          for (unsigned int moment = 0; moment < self.GetNumMoments(); moment++)
23,832✔
116
          {
117
            field_function_list_per_moment.append(self.GetScalarFluxFieldFunction(group, moment));
34,752✔
118
          }
119
          field_function_list_per_group.append(field_function_list_per_moment);
6,456✔
120
        }
6,456✔
121
      }
122
      return field_function_list_per_group;
384✔
UNCOV
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
    In the nested form (``only_scalar_flux=False``), the moment index varies fastest
145
    within each group (inner index = moment, outer index = group).
146
    )",
147
    py::arg("only_scalar_flux") = true
672✔
148
  );
149
  lbs_problem.def(
672✔
150
    "GetPowerFieldFunction",
151
    &LBSProblem::GetPowerFieldFunction,
672✔
152
    R"(
153
    Returns the power generation field function, if enabled.
154
    )"
155
  );
156
  lbs_problem.def(
672✔
157
    "GetTime",
158
    &LBSProblem::GetTime,
672✔
159
    R"(
160
    Get the current simulation time in seconds.
161
    )"
162
  );
163
  lbs_problem.def(
672✔
164
    "GetTimeStep",
165
    &LBSProblem::GetTimeStep,
672✔
166
    R"(
167
    Get the current timestep size.
168
    )"
169
  );
170
  lbs_problem.def(
672✔
171
    "SetOptions",
172
    [](LBSProblem& self, py::kwargs& params)
672✔
173
    {
UNCOV
174
      InputParameters input = LBSProblem::GetOptionsBlock();
×
UNCOV
175
      input.AssignParameters(kwargs_to_param_block(params));
×
UNCOV
176
      self.SetOptions(input);
×
UNCOV
177
    },
×
178
    R"(
179
    Set problem options from a large list of parameters.
180

181
    Parameters
182
    ----------
183
    max_mpi_message_size: int default=32768
184
        The maximum MPI message size used during sweeps.
185
    restart_writes_enabled: bool, default=False
186
        Flag that controls writing of restart dumps.
187
        Requires ``write_restart_path`` to be set to a non-empty value.
188
    write_delayed_psi_to_restart: bool, default=True
189
        Flag that controls writing of delayed angular fluxes to restarts.
190
    read_restart_path: str, default=''
191
        Full path for reading restart dumps including file basename.
192
    write_restart_path: str, default=''
193
        Full path for writing restart dumps including file basename.
194
    write_restart_time_interval: int, default=0
195
        Time interval in seconds at which restart data is to be written.
196
        Must be ``0`` (disabled) or at least ``30`` seconds.
197
    use_precursors: bool, default=False
198
        Flag for using delayed neutron precursors.
199
    use_source_moments: bool, default=False
200
        Flag for ignoring fixed sources and selectively using source moments obtained elsewhere.
201
    save_angular_flux: bool, default=False
202
        Flag indicating whether angular fluxes are to be stored or not.
203
    adjoint: bool, default=False
204
        Toggle adjoint transport mode.
205
    verbose_inner_iterations: bool, default=True
206
        Flag to control verbosity of inner iterations.
207
    verbose_outer_iterations: bool, default=True
208
        Flag to control verbosity of across-groupset iterations.
209
    max_ags_iterations: int, default=100
210
        Maximum number of across-groupset iterations. ``0`` is allowed.
211
    ags_tolerance: float, default=1.0e-6
212
        Across-groupset iterations tolerance.
213
    ags_convergence_check: {'l2', 'pointwise'}, default='l2'
214
        Type of convergence check for AGS iterations.
215
    verbose_ags_iterations: bool, default=True
216
        Flag to control verbosity of across-groupset iterations.
217
    power_field_function_on: bool, default=False
218
        Flag to control the creation of the power generation field function. If set to ``True``, a
219
        field function will be created with the general name ``<solver_name>_power_generation``.
220
    power_default_kappa: float, default=3.20435e-11
221
        Default ``kappa`` value (Energy released per fission) to use for power generation when cross
222
        sections do not have ``kappa`` values. Default corresponds to 200 MeV per fission.
223
    power_normalization: float, default=-1.0
224
        Power normalization factor to use. Supply a negative or zero number to turn this off.
225
    field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
226
        Prefix option on field function names. If unset, flux field functions will be exported as
227
        ``phi_gXXX_mYYY``, where ``XXX`` is the zero-padded 3-digit group number and ``YYY`` is the
228
        zero-padded 3-digit moment.
229
    field_function_prefix: str, default=''
230
        Prefix to use on all field functions. By default, this is empty. If specified, flux moments
231
        are exported as ``prefix_phi_gXXX_mYYY``.
232

233
    Notes
234
    -----
235
    This call is additive: only options explicitly supplied are updated.
236

237
    This is one of three supported mode-setting paths in Python:
238
      1. ``options={'adjoint': ...}`` in the problem constructor.
239
      2. ``SetOptions(adjoint=...)`` (this method).
240
      3. ``SetAdjoint(...)``.
241

242
    If ``adjoint`` is explicitly supplied in this call and differs from the current mode, OpenSn
243
    performs the same mode-transition behavior as :meth:`LBSProblem.SetAdjoint`. If ``adjoint``
244
    is omitted, the existing mode is unchanged.
245

246
    Options requirements:
247
    - ``restart_writes_enabled=True`` requires non-empty ``write_restart_path``.
248
    - ``write_restart_time_interval>0`` requires ``restart_writes_enabled=True``.
249
    - ``write_restart_time_interval`` must be ``0`` or ``>=30``.
250
    - non-empty ``field_function_prefix`` requires ``field_function_prefix_option='prefix'``.
251
    )"
252
  );
253
  lbs_problem.def(
672✔
254
    "ComputeFissionRate",
255
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
672✔
256
    {
UNCOV
257
      const std::vector<double>* phi_ptr = nullptr;
×
UNCOV
258
      if (scalar_flux_iterate == "old")
×
259
      {
UNCOV
260
        phi_ptr = &self.GetPhiOldLocal();
×
261
      }
UNCOV
262
      else if (scalar_flux_iterate == "new")
×
263
      {
UNCOV
264
        phi_ptr = &self.GetPhiNewLocal();
×
265
      }
266
      else
267
      {
UNCOV
268
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
269
      }
UNCOV
270
      return ComputeFissionRate(self, *phi_ptr);
×
271
    },
272
    R"(
273
    Computes the total fission rate.
274

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

282
    Returns
283
    -------
284
    float
285
        The total fission rate.
286

287
    Raises
288
    ------
289
    ValueError
290
        If `scalar_flux_iterate` is not 'old' or 'new'.
291
    )",
292
    py::arg("scalar_flux_iterate")
672✔
293
  );
294
  lbs_problem.def(
672✔
295
    "ComputeFissionProduction",
296
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
1,416✔
297
    {
298
      const std::vector<double>* phi_ptr = nullptr;
744✔
299
      if (scalar_flux_iterate == "old")
744✔
300
      {
301
        phi_ptr = &self.GetPhiOldLocal();
44✔
302
      }
303
      else if (scalar_flux_iterate == "new")
700✔
304
      {
305
        phi_ptr = &self.GetPhiNewLocal();
700✔
306
      }
307
      else
308
      {
UNCOV
309
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
310
      }
311
      return ComputeFissionProduction(self, *phi_ptr);
744✔
312
    },
313
    R"(
314
    Computes the total fission production (nu*fission).
315

316
    Parameters
317
    ----------
318
    scalar_flux_iterate : {'old', 'new'}
319
        Specifies which scalar flux vector to use in the calculation.
320
            - 'old': Use the previous scalar flux iterate.
321
            - 'new': Use the current scalar flux iterate.
322

323
    Returns
324
    -------
325
    float
326
        The total fission production.
327

328
    Raises
329
    ------
330
    ValueError
331
        If `scalar_flux_iterate` is not 'old' or 'new'.
332
    )",
333
    py::arg("scalar_flux_iterate")
672✔
334
  );
335
  lbs_problem.def(
672✔
336
    "GetPhiOldLocal",
337
    [](LBSProblem& self)
720✔
338
    {
339
      return convert_vector(self.GetPhiOldLocal());
48✔
340
    },
341
    R"(
342
    Get the previous scalar flux iterate (local vector).
343

344
    Returns
345
    -------
346
    memoryview
347
        Memory view of the local old scalar flux vector.
348
    )"
349
  );
350
  lbs_problem.def(
672✔
351
    "GetPhiNewLocal",
352
    [](LBSProblem& self)
728✔
353
    {
354
      return convert_vector(self.GetPhiNewLocal());
56✔
355
    },
356
    R"(
357
    Get the current scalar flux iterate (local vector).
358

359
    Returns
360
    -------
361
    memoryview
362
        Memory view of the local new scalar flux vector.
363
    )"
364
  );
365
  lbs_problem.def(
672✔
366
    "WriteFluxMoments",
367
    [](LBSProblem& self, const std::string& file_base)
704✔
368
    {
369
      LBSSolverIO::WriteFluxMoments(self, file_base);
32✔
370
    },
371
    R"(
372
    Write flux moments to file.
373

374
    Parameters
375
    ----------
376
    file_base: str
377
        File basename.
378
    )",
379
    py::arg("file_base")
672✔
380
  );
381
  lbs_problem.def(
672✔
382
    "CreateAndWriteSourceMoments",
383
    [](LBSProblem& self, const std::string& file_base)
676✔
384
    {
385
      std::vector<double> source_moments = self.MakeSourceMomentsFromPhi();
4✔
386
      LBSSolverIO::WriteFluxMoments(self, file_base, source_moments);
4✔
387
    },
4✔
388
    R"(
389
    Write source moments from latest flux iterate to file.
390

391
    Parameters
392
    ----------
393
    file_base: str
394
        File basename.
395
    )",
396
    py::arg("file_base")
672✔
397
  );
398
  lbs_problem.def(
672✔
399
    "ReadFluxMomentsAndMakeSourceMoments",
400
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
672✔
401
    {
UNCOV
402
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, self.GetExtSrcMomentsLocal());
×
UNCOV
403
      log.Log() << "Making source moments from flux file.";
×
UNCOV
404
      std::vector<double>& temp_phi = self.GetPhiOldLocal();
×
UNCOV
405
      self.GetPhiOldLocal() = self.GetExtSrcMomentsLocal();
×
UNCOV
406
      self.GetExtSrcMomentsLocal() = self.MakeSourceMomentsFromPhi();
×
UNCOV
407
      self.GetPhiOldLocal() = temp_phi;
×
UNCOV
408
    },
×
409
    R"(
410
    Read flux moments and compute corresponding source moments.
411

412
    Parameters
413
    ----------
414
    file_base: str
415
        File basename.
416
    single_file_flag: bool
417
        True if all flux moments are in a single file.
418
    )",
419
    py::arg("file_base"),
1,344✔
420
    py::arg("single_file_flag")
672✔
421
  );
422
  lbs_problem.def(
672✔
423
    "ReadSourceMoments",
424
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
676✔
425
    {
426
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, self.GetExtSrcMomentsLocal());
4✔
427
    },
4✔
428
    R"(
429
    Read source moments from file.
430

431
    Parameters
432
    ----------
433
    file_base: str
434
        File basename.
435
    single_file_flag: bool
436
        True if all source moments are in a single file.
437
    )",
438
    py::arg("file_base"),
1,344✔
439
    py::arg("single_file_flag")
672✔
440
  );
441
  lbs_problem.def(
672✔
442
    "ReadFluxMoments",
443
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
672✔
444
    {
UNCOV
445
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag);
×
446
    },
447
    R"(
448
    Read flux moment data.
449

450
    Parameters
451
    ----------
452
    file_base: str
453
        File basename.
454
    single_file_flag: bool
455
        True if all flux moments are in a single file.
456
    )",
457
    py::arg("file_base"),
1,344✔
458
    py::arg("single_file_flag")
672✔
459
  );
460
  lbs_problem.def(
672✔
461
    "WriteAngularFluxes",
462
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
676✔
463
    {
464
      DiscreteOrdinatesProblemIO::WriteAngularFluxes(self, file_base);
4✔
465
    },
466
    R"(
467
    Write angular flux data to file.
468

469
    Parameters
470
    ----------
471
    file_base: str
472
        File basename.
473
    )",
474
    py::arg("file_base")
672✔
475
  );
476
  lbs_problem.def(
672✔
477
    "ReadAngularFluxes",
478
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
676✔
479
    {
480
      DiscreteOrdinatesProblemIO::ReadAngularFluxes(self, file_base);
4✔
481
    },
482
    R"(
483
    Read angular fluxes from file.
484

485
    Parameters
486
    ----------
487
    file_base: str
488
        File basename.
489
    )",
490
    py::arg("file_base")
672✔
491
  );
492
  lbs_problem.def(
672✔
493
    "SetPointSources",
494
    [](LBSProblem& self, py::kwargs& params)
672✔
495
    {
UNCOV
496
      for (auto [key, value] : params)
×
497
      {
UNCOV
498
        auto c_key = key.cast<std::string>();
×
UNCOV
499
        if (c_key == "clear_point_sources")
×
UNCOV
500
          self.ClearPointSources();
×
UNCOV
501
        else if (c_key == "point_sources")
×
502
        {
UNCOV
503
          auto sources = value.cast<py::list>();
×
UNCOV
504
          for (auto source : sources)
×
505
          {
UNCOV
506
            self.AddPointSource(source.cast<std::shared_ptr<PointSource>>());
×
507
          }
UNCOV
508
        }
×
509
        else
UNCOV
510
          throw std::runtime_error("Invalid argument provided to SetPointSources.\n");
×
UNCOV
511
      }
×
UNCOV
512
    },
×
513
    R"(
514
    Set or clear point sources.
515

516
    Parameters
517
    ----------
518
    clear_point_sources: bool, default=False
519
        If true, all current the point sources of the problem are deleted.
520
    point_sources: List[pyopensn.source.PointSource]
521
        List of new point sources to be added to the problem.
522
    )"
523
  );
524
  lbs_problem.def(
672✔
525
    "SetVolumetricSources",
526
    [](LBSProblem& self, py::kwargs& params)
708✔
527
    {
528
      for (auto [key, value] : params)
108✔
529
      {
530
        auto c_key = key.cast<std::string>();
36✔
531
        if (c_key == "clear_volumetric_sources")
36✔
532
          self.ClearVolumetricSources();
12✔
533
        else if (c_key == "volumetric_sources")
24✔
534
        {
535
          auto sources = value.cast<py::list>();
24✔
536
          for (auto source : sources)
72✔
537
          {
538
            self.AddVolumetricSource(source.cast<std::shared_ptr<VolumetricSource>>());
48✔
539
          }
540
        }
24✔
541
        else
UNCOV
542
          throw std::runtime_error("Invalid argument provided to SetVolumetricSources.\n");
×
543
      }
36✔
544
    },
36✔
545
    R"(
546
    Set or clear volumetric sources.
547

548
    Parameters
549
    ----------
550
    clear_volumetric_sources: bool, default=False
551
        If true, all current the volumetric sources of the problem are deleted.
552
    volumetric_sources: List[pyopensn.source.VolumetricSource]
553
        List of new volumetric sources to be added to the problem.
554
    )"
555
  );
556
  lbs_problem.def(
672✔
557
    "SetXSMap",
558
    [](LBSProblem& self, py::kwargs& params)
840✔
559
    {
560
      BlockID2XSMap xs_map;
168✔
561
      for (auto [key, value] : params)
504✔
562
      {
563
        auto c_key = key.cast<std::string>();
168✔
564
        if (c_key == "xs_map")
168✔
565
        {
566
          auto xs_entries = value.cast<py::list>();
168✔
567
          for (auto entry : xs_entries)
504✔
568
          {
569
            InputParameters xs_entry_pars = LBSProblem::GetXSMapEntryBlock();
168✔
570
            xs_entry_pars.AssignParameters(pyobj_to_param_block("", entry.cast<py::dict>()));
168✔
571
            const auto& block_ids =
168✔
572
              xs_entry_pars.GetParam("block_ids").GetVectorValue<unsigned int>();
168✔
573
            auto xs = xs_entry_pars.GetSharedPtrParam<MultiGroupXS>("xs");
168✔
574
            for (const auto& block_id : block_ids)
336✔
575
              xs_map[block_id] = xs;
168✔
576
          }
168✔
577
        }
168✔
578
        else
UNCOV
579
          throw std::runtime_error("Invalid argument provided to SetXSMap.\n");
×
580
      }
168✔
581
      self.SetBlockID2XSMap(xs_map);
168✔
582
    },
168✔
583
    R"(
584
    Replace the block-id to cross-section map.
585

586
    Parameters
587
    ----------
588
    xs_map: List[Dict]
589
        A list of block-id to cross-section mapping dictionaries. Each dictionary supports:
590
          - block_ids: List[int] (required)
591
              Mesh block ids to associate with the cross section.
592
          - xs: pyopensn.xs.MultiGroupXS (required)
593
              Cross section object.
594

595
    Notes
596
    -----
597
    Forward/adjoint mode toggles via ``SetOptions(adjoint=...)`` (or the low-level
598
    :meth:`LBSProblem.SetAdjoint`) do not change this map.
599
    The ``MultiGroupXS`` objects themselves are mutable and shared by pointer. If the same
600
    ``MultiGroupXS`` handle is shared across multiple problems, toggling adjoint mode in one
601
    problem also changes the transport mode seen by the others.
602
    )"
603
  );
604
  lbs_problem.def(
672✔
605
    "SetSaveAngularFlux",
606
    [](LBSProblem& self, bool save)
672✔
607
    {
UNCOV
608
      self.SetSaveAngularFlux(save);
×
609
    },
610
    py::arg("save")
672✔
611
  );
612
  lbs_problem.def(
672✔
613
    "ZeroPhi",
614
    [](LBSProblem& self)
672✔
615
    {
UNCOV
616
      self.ZeroPhi();
×
617
    },
618
    R"(
619
    Zero the scalar-flux vectors (``phi_old`` and ``phi_new``) in-place.
620
    )"
621
  );
622
  lbs_problem.def(
672✔
623
    "ZeroPsi",
624
    [](LBSProblem& self)
672✔
625
    {
UNCOV
626
      self.ZeroPsi();
×
627
    },
628
    R"(
629
    Zero the angular-flux storage (if present for this problem type).
630
    )"
631
  );
632
  lbs_problem.def(
672✔
633
    "SetAdjoint",
634
    [](LBSProblem& self, bool adjoint)
692✔
635
    {
636
      self.SetAdjoint(adjoint);
20✔
637
    },
638
    R"(
639
    Set forward/adjoint transport mode.
640

641
    Parameters
642
    ----------
643
    adjoint: bool
644
        ``True`` enables adjoint mode and ``False`` enables forward mode.
645

646
    Notes
647
    -----
648
    This is one of three supported mode-setting paths in Python:
649
      1. ``options={'adjoint': ...}`` in the problem constructor.
650
      2. ``SetOptions(adjoint=...)``.
651
      3. ``SetAdjoint(...)`` (this method).
652

653
    In typical Python workflows, use constructor options or ``SetOptions``.
654

655
    If this changes mode, OpenSn performs a full mode-transition reset:
656
      - Materials are reinitialized in the selected mode.
657
      - Point and volumetric sources are cleared.
658
      - Boundary conditions are cleared.
659
      - Scalar and angular flux vectors are zeroed.
660

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

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

667
    After a mode change, reapply the desired driving terms before solving, typically:
668
      - :meth:`LBSProblem.SetPointSources`
669
      - :meth:`LBSProblem.SetVolumetricSources`
670
      - :meth:`DiscreteOrdinatesProblem.SetBoundaryOptions`
671

672
    This routine is intentionally destructive with respect to source/boundary/flux state
673
    to avoid hidden coupling between forward and adjoint setups.
674
    )"
675
  );
676
  lbs_problem.def(
672✔
677
    "IsAdjoint",
678
    &LBSProblem::IsAdjoint,
672✔
679
    R"(
680
    Return ``True`` if the problem is in adjoint mode, otherwise ``False``.
681
    )"
682
  );
683
  lbs_problem.def(
672✔
684
    "IsTimeDependent",
685
    &LBSProblem::IsTimeDependent,
672✔
686
    R"(
687
    Return ``True`` if the problem is in time-dependent mode, otherwise ``False``.
688
    )"
689
  );
690

691
  // discrete ordinate solver
692
  auto do_problem = py::class_<DiscreteOrdinatesProblem, std::shared_ptr<DiscreteOrdinatesProblem>,
672✔
693
                               LBSProblem>(
694
    slv,
695
    "DiscreteOrdinatesProblem",
696
    R"(
697
    Base class for discrete ordinates problems in Cartesian geometry.
698

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

703
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesProblem`.
704
    )"
705
  );
672✔
706
  do_problem.def(
1,344✔
707
    py::init(
672✔
708
      [](py::kwargs& params)
499✔
709
      {
710
        return DiscreteOrdinatesProblem::Create(kwargs_to_param_block(params));
499✔
711
      }
712
    ),
713
    R"(
714
    Construct a discrete ordinates problem with Cartesian geometry.
715

716
    Parameters
717
    ----------
718
    mesh : MeshContinuum
719
        The spatial mesh.
720
    num_groups : int
721
        The total number of energy groups.
722
    groupsets : List[Dict], default=[]
723
        A list of input parameter blocks, each block provides the iterative properties for a
724
        groupset. Each dictionary supports:
725
          - groups_from_to: List[int] (required)
726
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
727
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
728
              Handle to an angular quadrature.
729
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
730
              Angle aggregation method to use during sweeping.
731
          - angle_aggregation_num_subsets: int, default=1
732
              Number of angle subsets used for aggregation.
733
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
734
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
735
              Iterative method used for inner linear solves.
736
          - l_abs_tol: float, default=1.0e-6
737
              Inner linear solver absolute residual tolerance.
738
          - l_max_its: int, default=200
739
              Inner linear solver maximum iterations.
740
          - gmres_restart_interval: int, default=30
741
              GMRES restart interval, if GMRES is used.
742
          - allow_cycles: bool, default=True
743
              Whether cyclic dependencies are allowed in sweeps.
744
          - apply_wgdsa: bool, default=False
745
              Enable within-group DSA for this groupset.
746
          - wgdsa_l_abs_tol: float, default=1.0e-4
747
              WGDSA linear absolute tolerance.
748
          - wgdsa_l_max_its: int, default=30
749
              WGDSA maximum iterations.
750
          - wgdsa_verbose: bool, default=False
751
              Verbose WGDSA output.
752
          - wgdsa_petsc_options: str, default=''
753
              PETSc options string for the WGDSA solver.
754
          - apply_tgdsa: bool, default=False
755
              Enable two-grid DSA for this groupset.
756
          - tgdsa_l_abs_tol: float, default=1.0e-4
757
              TGDSA linear absolute tolerance.
758
          - tgdsa_l_max_its: int, default=30
759
              TGDSA maximum iterations.
760
          - tgdsa_verbose: bool, default=False
761
              Verbose TGDSA output.
762
          - tgdsa_petsc_options: str, default=''
763
              PETSc options string for the TGDSA solver.
764
    xs_map : List[Dict], default=[]
765
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
766
          - block_ids: List[int] (required)
767
              Mesh block IDs to associate with the cross section.
768
          - xs: pyopensn.xs.MultiGroupXS (required)
769
              Cross-section object to assign to the specified blocks.
770
    boundary_conditions: List[Dict], default=[]
771
        A list containing tables for each boundary specification. Each dictionary supports:
772
          - name: str (required)
773
              Boundary name that identifies the specific boundary.
774
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
775
              Boundary type specification.
776
          - group_strength: List[float], optional
777
              Required when ``type='isotropic'``. Isotropic strength per group.
778
          - function: AngularFluxFunction, optional
779
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
780
    point_sources: List[pyopensn.source.PointSource], default=[]
781
        A list of point sources.
782
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
783
        A list of volumetric sources.
784
    options : Dict, default={}
785
        A block of optional configuration parameters. Each dictionary supports the same keys as
786
        :meth:`LBSProblem.SetOptions`, including:
787
          - max_mpi_message_size: int, default=32768
788
          - restart_writes_enabled: bool, default=False
789
          - write_delayed_psi_to_restart: bool, default=True
790
          - read_restart_path: str, default=''
791
          - write_restart_path: str, default=''
792
          - write_restart_time_interval: int, default=0
793
            (must be 0 or >=30)
794
          - use_precursors: bool, default=False
795
          - use_source_moments: bool, default=False
796
          - save_angular_flux: bool, default=False
797
          - adjoint: bool, default=False
798
          - verbose_inner_iterations: bool, default=True
799
          - verbose_outer_iterations: bool, default=True
800
          - max_ags_iterations: int, default=100
801
          - ags_tolerance: float, default=1.0e-6
802
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
803
          - verbose_ags_iterations: bool, default=True
804
          - power_field_function_on: bool, default=False
805
          - power_default_kappa: float, default=3.20435e-11
806
          - power_normalization: float, default=-1.0
807
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
808
          - field_function_prefix: str, default=''
809
        Requirements are the same as :meth:`LBSProblem.SetOptions`.
810
    sweep_type : str, default="AAH"
811
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
812
    time_dependent : bool, default=False
813
        If true, the problem is constructed in time-dependent mode. Otherwise it starts in
814
        steady-state mode.
815
    use_gpus : bool, default=False
816
        A flag specifying whether GPU acceleration is used for the sweep. Currently, only ``AAH`` is
817
        supported.
818
    )"
819
  );
820
  do_problem.def(
672✔
821
    "SetTimeDependentMode",
822
    &DiscreteOrdinatesProblem::SetTimeDependentMode,
672✔
823
    R"(
824
    Set the problem to time-dependent mode.
825

826
    Notes
827
    -----
828
    Switch problem from steady-state to time-dependent mode. This updates problem
829
    internals (sweep chunk mode and source-function) while preserving user boundary
830
    conditions and fixed sources.  If ``save_angular_flux`` is false, this transition
831
    enables it.
832
    )"
833
  );
834
  do_problem.def(
672✔
835
    "SetSteadyStateMode",
836
    &DiscreteOrdinatesProblem::SetSteadyStateMode,
672✔
837
    R"(
838
    Set the problem to steady-state mode.
839

840
    Notes
841
    -----
842
    Switch problem from time-dependent to steady-state mode. This updates problem
843
    internals (sweep chunk mode and source-function) while preserving user boundary
844
    conditions and fixed sources. If ``save_angular_flux`` was auto-enabled when
845
    entering time-dependent mode, this call restores the previous value.
846
    )"
847
  );
848
  do_problem.def(
672✔
849
    "IsTimeDependent",
850
    &DiscreteOrdinatesProblem::IsTimeDependent,
672✔
851
    R"(
852
    Return ``True`` if the problem is currently in time-dependent mode.
853
    )"
854
  );
855
  do_problem.def(
672✔
856
    "SetOptions",
857
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
684✔
858
    {
859
      InputParameters input = DiscreteOrdinatesProblem::GetOptionsBlock();
12✔
860
      input.AssignParameters(kwargs_to_param_block(params));
12✔
861
      self.SetOptions(input);
12✔
862
    },
12✔
863
    R"(
864
    Set problem options from a large list of parameters.
865

866
    Parameters
867
    ----------
868
    **kwargs
869
        Same option keys and semantics as :meth:`LBSProblem.SetOptions`.
870

871
    Notes
872
    -----
873
    This call is additive: only options explicitly supplied are updated.
874

875
    As with :meth:`LBSProblem.SetOptions`, if ``adjoint`` is explicitly supplied and differs from
876
    the current mode, the same mode-transition behavior as :meth:`LBSProblem.SetAdjoint` is used.
877
    )"
878
  );
879
  do_problem.def(
672✔
880
    "SetBoundaryOptions",
881
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
676✔
882
    {
883
      for (auto [key, value] : params)
12✔
884
      {
885
        auto c_key = key.cast<std::string>();
4✔
886
        if (c_key == "clear_boundary_conditions")
4✔
UNCOV
887
          self.ClearBoundaries();
×
888
        else if (c_key == "boundary_conditions")
4✔
889
        {
890
          auto boundaries = value.cast<py::list>();
4✔
891
          for (auto boundary : boundaries)
24✔
892
          {
893
            InputParameters input = DiscreteOrdinatesProblem::GetBoundaryOptionsBlock();
16✔
894
            input.AssignParameters(pyobj_to_param_block("", boundary.cast<py::dict>()));
16✔
895
            self.SetBoundaryOptions(input);
16✔
896
          }
16✔
897
        }
4✔
898
        else
UNCOV
899
          throw std::runtime_error("Invalid argument provided to SetBoundaryOptions.\n");
×
900
      }
4✔
901
    },
4✔
902
    R"(
903
    Set or clear boundary conditions.
904

905
    Parameters
906
    ----------
907
    clear_boundary_conditions: bool, default=False
908
        If true, all current boundary conditions are deleted.
909
    boundary_conditions: List[Dict]
910
        A list of boundary condition dictionaries. Each dictionary supports:
911
          - name: str (required)
912
              Boundary name that identifies the specific boundary.
913
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
914
              Boundary type specification.
915
          - group_strength: List[float], optional
916
              Required when ``type='isotropic'``. Isotropic strength per group.
917
          - function: AngularFluxFunction, optional
918
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
919

920
    Notes
921
    -----
922
    Mode transitions via ``SetOptions(adjoint=...)`` (or the low-level
923
    :meth:`LBSProblem.SetAdjoint`) clear all boundary conditions.
924
    Reapply boundaries with this method before solving in the new mode.
925
    )"
926
  );
927
  do_problem.def(
672✔
928
    "GetPsi",
929
    [](DiscreteOrdinatesProblem& self)
680✔
930
    {
931
      const auto& psi = self.GetPsiNewLocal();
8✔
932
      py::list psi_list;
8✔
933
      for (const auto& vec : psi)
16✔
934
      {
935
        auto array = py::array_t<double>(static_cast<py::ssize_t>(vec.size()),
8✔
936
                                         vec.data(),
937
                                         py::cast(self));
8✔
938
        psi_list.append(array);
8✔
939
      }
8✔
940
      return psi_list;
8✔
UNCOV
941
    },
×
942
    R"(
943
    Return psi as a list of NumPy arrays (float64), using zero-copy views into the
944
    underlying data.
945
    )"
946
  );
947
  do_problem.def(
672✔
948
    "GetAngularFieldFunctionList",
949
    [](DiscreteOrdinatesProblem& self, py::list groups, py::list angles)
672✔
950
    {
UNCOV
951
      std::vector<unsigned int> group_ids;
×
UNCOV
952
      std::vector<size_t> angle_ids;
×
UNCOV
953
      group_ids.reserve(groups.size());
×
UNCOV
954
      angle_ids.reserve(angles.size());
×
955

UNCOV
956
      for (py::handle g : groups)
×
UNCOV
957
        group_ids.push_back(g.cast<unsigned int>());
×
UNCOV
958
      for (py::handle a : angles)
×
UNCOV
959
        angle_ids.push_back(a.cast<size_t>());
×
960

UNCOV
961
      auto ff_list = self.GetAngularFluxFieldFunctionList(group_ids, angle_ids);
×
962
      py::list out;
×
UNCOV
963
      for (const auto& ff : ff_list)
×
UNCOV
964
        out.append(ff);
×
UNCOV
965
      return out;
×
UNCOV
966
    },
×
967
    R"(
968
    Return field functions for selected angular flux components.
969

970
    Note: You must enable angular flux storage (``save_angular_flux=True``) in
971
    the problem options, or use a transient/time-dependent solver that retains
972
    angular fluxes, otherwise the field functions will remain zero.
973

974
    Example
975
    -------
976
    ```python
977
    solver.Initialize()
978
    solver.Execute()
979
    ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
980
    ```
981

982
    For transient/time-dependent runs, call this after each timestep. Two common
983
    patterns are:
984

985
    1) Use `TransientSolver.Execute()` with a post-advance callback:
986
    ```python
987
    solver = TransientSolver(problem=phys)
988
    solver.Initialize()
989

990
    def post_advance():
991
        ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
992
        FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
993

994
    solver.SetPostAdvanceCallback(post_advance)
995
    solver.Execute()
996
    ```
997

998
    2) Use a custom Python loop with `TransientSolver.Advance()`:
999
    ```python
1000
    solver = TransientSolver(problem=phys)
1001
    solver.Initialize()
1002
    for _ in range(num_steps):
1003
        solver.Advance()
1004
        ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
1005
        FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
1006
    ```
1007

1008
    Parameters
1009
    ----------
1010
    groups : List[int]
1011
        Global group indices to export.
1012
    angles : List[int]
1013
        Angle indices within the groupset quadrature for each group.
1014

1015
    Returns
1016
    -------
1017
    List[pyopensn.fieldfunc.FieldFunctionGridBased]
1018
        Field functions for the requested (group, angle) pairs.
1019
    )",
1020
    py::arg("groups"),
1,344✔
1021
    py::arg("angles")
672✔
1022
  );
1023
  do_problem.def(
672✔
1024
    "ComputeLeakage",
1025
    [](DiscreteOrdinatesProblem& self, py::list bnd_names)
711✔
1026
    {
1027
      auto grid = self.GetGrid();
39✔
1028
      // get the supported boundaries
1029
      std::map<std::string, std::uint64_t> allowed_bd_names = grid->GetBoundaryNameMap();
39✔
1030
      std::map<std::uint64_t, std::string> allowed_bd_ids = grid->GetBoundaryIDMap();
39✔
1031
      const auto coord_sys = grid->GetCoordinateSystem();
39✔
1032
      const auto mesh_type = grid->GetType();
39✔
1033
      const auto dim = grid->GetDimension();
39✔
1034
      // get the boundaries to parse, preserving user order
1035
      std::vector<std::uint64_t> bndry_ids;
39✔
1036
      if (bnd_names.size() > 1)
39✔
1037
      {
1038
        for (py::handle name : bnd_names)
184✔
1039
        {
1040
          auto sname = name.cast<std::string>();
112✔
1041
          if (coord_sys == CoordinateSystemType::CYLINDRICAL && dim == 2)
112✔
1042
          {
1043
            if (sname == "xmin" || sname == "xmax" || sname == "ymin" || sname == "ymax")
24✔
UNCOV
1044
              throw std::runtime_error("ComputeLeakage: Boundary name '" + sname +
×
1045
                                       "' is invalid for cylindrical orthogonal meshes. "
UNCOV
1046
                                       "Use rmin, rmax, zmin, zmax.");
×
1047

1048
            if (mesh_type == MeshType::ORTHOGONAL)
24✔
1049
            {
1050
              if (sname == "rmin") sname = "xmin";
24✔
1051
              else if (sname == "rmax") sname = "xmax";
24✔
1052
              else if (sname == "zmin") sname = "ymin";
16✔
1053
              else if (sname == "zmax") sname = "ymax";
8✔
1054
            }
1055
          }
1056
          bndry_ids.push_back(allowed_bd_names.at(sname));
112✔
1057
        }
112✔
1058
      }
1059
      else
1060
      {
1061
        bndry_ids = self.GetGrid()->GetUniqueBoundaryIDs();
9✔
1062
      }
1063
      // compute the leakage
1064
      std::map<std::uint64_t, std::vector<double>> leakage = ComputeLeakage(self, bndry_ids);
39✔
1065
      const bool rz_ortho = (coord_sys == CoordinateSystemType::CYLINDRICAL &&
78✔
1066
                             mesh_type == MeshType::ORTHOGONAL && dim == 2);
39✔
1067

1068
      auto to_rz_name = [](const std::string& name)
63✔
1069
      {
1070
        if (name == "xmin") return std::string("rmin");
24✔
1071
        if (name == "xmax") return std::string("rmax");
24✔
1072
        if (name == "ymin") return std::string("zmin");
16✔
1073
        if (name == "ymax") return std::string("zmax");
8✔
UNCOV
1074
        return name;
×
1075
      };
1076

1077
      // convert result to native Python
1078
      py::dict result;
39✔
1079
      for (const auto& bndry_id : bndry_ids)
157✔
1080
      {
1081
        const auto it = leakage.find(bndry_id);
118✔
1082
        if (it == leakage.end())
118✔
UNCOV
1083
          continue;
×
1084
        // construct numpy array and copy contents
1085
        const auto& grp_wise_leakage = it->second;
118✔
1086
        py::array_t<double> np_vector(py::ssize_t(grp_wise_leakage.size()));
118✔
1087
        auto buffer = np_vector.request();
118✔
1088
        auto *np_vector_data = static_cast<double*>(buffer.ptr);
118✔
1089
        std::copy(grp_wise_leakage.begin(), grp_wise_leakage.end(), np_vector_data);
118✔
1090
        std::string name = allowed_bd_ids.at(bndry_id);
118✔
1091
        if (rz_ortho)
118✔
1092
          name = to_rz_name(name);
24✔
1093
        result[py::str(name)] = std::move(np_vector);
236✔
1094
      }
118✔
1095

1096
      return result;
39✔
1097
    },
78✔
1098
    R"(
1099
    Compute leakage for the problem.
1100

1101
    Parameters
1102
    ----------
1103
    bnd_names : List[str]
1104
        A list of boundary names for which leakage should be computed.
1105

1106
    Returns
1107
    -------
1108
    Dict[str, numpy.ndarray]
1109
        A dictionary mapping boundary names to group-wise leakage vectors.
1110
        Each array contains the outgoing angular flux (per group) integrated over
1111
        the corresponding boundary surface.
1112

1113
    Raises
1114
    ------
1115
    RuntimeError
1116
        If `save_angular_flux` option was not enabled during problem setup.
1117

1118
    ValueError
1119
        If one or more boundary ids are not present on the current mesh.
1120
    )",
1121
    py::arg("bnd_names")
672✔
1122
  );
1123

1124
  // discrete ordinates curvilinear problem
1125
  auto do_curvilinear_problem = py::class_<DiscreteOrdinatesCurvilinearProblem,
672✔
1126
                                           std::shared_ptr<DiscreteOrdinatesCurvilinearProblem>,
1127
                                           DiscreteOrdinatesProblem>(
1128
    slv,
1129
    "DiscreteOrdinatesCurvilinearProblem",
1130
    R"(
1131
    Base class for discrete ordinates problems in curvilinear geometry.
1132

1133
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesCurvilinearProblem`.
1134
    )"
1135
  );
672✔
1136
  do_curvilinear_problem.def(
1,344✔
1137
    py::init(
672✔
1138
      [](py::kwargs& params)
80✔
1139
      {
1140
        return DiscreteOrdinatesCurvilinearProblem::Create(kwargs_to_param_block(params));
80✔
1141
      }
1142
    ),
1143
    R"(
1144
    Construct a discrete ordinates problem for curvilinear geometry.
1145

1146
    Warnings
1147
    --------
1148
       DiscreteOrdinatesCurvilinearProblem is **experimental** and should be used with caution!
1149

1150
    Parameters
1151
    ----------
1152
    mesh : MeshContinuum
1153
        The spatial mesh.
1154
    coord_system : int
1155
        Coordinate system to use. Must be set to 2 (cylindrical coordinates).
1156
    num_groups : int
1157
        The total number of energy groups.
1158
    groupsets : list of dict
1159
        A list of input parameter blocks, each block provides the iterative properties for a
1160
        groupset. Each dictionary supports:
1161
          - groups_from_to: List[int] (required)
1162
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
1163
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
1164
              Handle to an angular quadrature.
1165
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
1166
              Angle aggregation method to use during sweeping.
1167
          - angle_aggregation_num_subsets: int, default=1
1168
              Number of angle subsets used for aggregation.
1169
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
1170
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
1171
              Iterative method used for inner linear solves.
1172
          - l_abs_tol: float, default=1.0e-6
1173
              Inner linear solver absolute residual tolerance.
1174
          - l_max_its: int, default=200
1175
              Inner linear solver maximum iterations.
1176
          - gmres_restart_interval: int, default=30
1177
              GMRES restart interval, if GMRES is used.
1178
          - allow_cycles: bool, default=True
1179
              Whether cyclic dependencies are allowed in sweeps.
1180
          - apply_wgdsa: bool, default=False
1181
              Enable within-group DSA for this groupset.
1182
          - wgdsa_l_abs_tol: float, default=1.0e-4
1183
              WGDSA linear absolute tolerance.
1184
          - wgdsa_l_max_its: int, default=30
1185
              WGDSA maximum iterations.
1186
          - wgdsa_verbose: bool, default=False
1187
              Verbose WGDSA output.
1188
          - wgdsa_petsc_options: str, default=''
1189
              PETSc options string for the WGDSA solver.
1190
          - apply_tgdsa: bool, default=False
1191
              Enable two-grid DSA for this groupset.
1192
          - tgdsa_l_abs_tol: float, default=1.0e-4
1193
              TGDSA linear absolute tolerance.
1194
          - tgdsa_l_max_its: int, default=30
1195
              TGDSA maximum iterations.
1196
          - tgdsa_verbose: bool, default=False
1197
              Verbose TGDSA output.
1198
          - tgdsa_petsc_options: str, default=''
1199
    xs_map : list of dict
1200
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
1201
          - block_ids: List[int] (required)
1202
              Mesh block IDs to associate with the cross section.
1203
          - xs: pyopensn.xs.MultiGroupXS (required)
1204
              Cross-section object to assign to the specified blocks.
1205
    boundary_conditions: List[Dict], default=[]
1206
        A list containing tables for each boundary specification. Each dictionary supports:
1207
          - name: str (required)
1208
              Boundary name that identifies the specific boundary.
1209
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
1210
              Boundary type specification.
1211
          - group_strength: List[float], optional
1212
              Required when ``type='isotropic'``. Isotropic strength per group.
1213
          - function: AngularFluxFunction, optional
1214
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
1215
    point_sources: List[pyopensn.source.PointSource], default=[]
1216
        A list of point sources.
1217
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
1218
        A list of volumetric sources.
1219
    options : dict, optional
1220
        A block of optional configuration parameters. Each dictionary supports the same keys as
1221
        :meth:`LBSProblem.SetOptions`, including:
1222
          - max_mpi_message_size: int, default=32768
1223
          - restart_writes_enabled: bool, default=False
1224
          - write_delayed_psi_to_restart: bool, default=True
1225
          - read_restart_path: str, default=''
1226
          - write_restart_path: str, default=''
1227
          - write_restart_time_interval: int, default=0
1228
          - use_precursors: bool, default=False
1229
          - use_source_moments: bool, default=False
1230
          - save_angular_flux: bool, default=False
1231
          - verbose_inner_iterations: bool, default=True
1232
          - verbose_outer_iterations: bool, default=True
1233
          - max_ags_iterations: int, default=100
1234
          - ags_tolerance: float, default=1.0e-6
1235
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
1236
          - verbose_ags_iterations: bool, default=True
1237
          - power_field_function_on: bool, default=False
1238
          - power_default_kappa: float, default=3.20435e-11
1239
          - power_normalization: float, default=-1.0
1240
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
1241
          - field_function_prefix: str, default=''
1242
    sweep_type : str, optional
1243
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
1244
    )"
1245
  );
1246
}
672✔
1247

1248
// Wrap steady-state solver
1249
void
1250
WrapSteadyState(py::module& slv)
672✔
1251
{
1252
  // clang-format off
1253
  // steady state solver
1254
  auto steady_state_solver = py::class_<SteadyStateSourceSolver, std::shared_ptr<SteadyStateSourceSolver>,
672✔
1255
                                        Solver>(
1256
    slv,
1257
    "SteadyStateSourceSolver",
1258
    R"(
1259
    Steady state solver.
1260

1261
    Wrapper of :cpp:class:`opensn::SteadyStateSourceSolver`.
1262
    )"
1263
  );
672✔
1264
  steady_state_solver.def(
1,344✔
1265
    py::init(
672✔
1266
      [](py::kwargs& params)
357✔
1267
      {
1268
        return SteadyStateSourceSolver::Create(kwargs_to_param_block(params));
357✔
1269
      }
1270
    ),
1271
    R"(
1272
    Construct a steady state solver.
1273

1274
    Parameters
1275
    ----------
1276
    pyopensn.solver.LBSProblem : LBSProblem
1277
        Existing LBSProblem instance.
1278
    )"
1279
  );
1280
  // clang-format on
1281
}
672✔
1282

1283
// Wrap transient solver
1284
void
1285
WrapTransient(py::module& slv)
672✔
1286
{
1287
  // clang-format off
1288
  auto transient_solver =
672✔
1289
    py::class_<TransientSolver, std::shared_ptr<TransientSolver>, Solver>(
1290
      slv,
1291
      "TransientSolver",
1292
      R"(
1293
      Transient solver.
1294

1295
      Wrapper of :cpp:class:`opensn::TransientSolver`.
1296
      )"
1297
    );
672✔
1298
  transient_solver.def(
1,344✔
1299
    py::init(
672✔
1300
      [](py::kwargs& params)
156✔
1301
      {
1302
        return TransientSolver::Create(kwargs_to_param_block(params));
156✔
1303
      }
1304
    ),
1305
    R"(
1306
    Construct a transient solver.
1307

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

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

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

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

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

1384
    Parameters
1385
    ----------
1386
    callback : Optional[Callable[[], None]]
1387
        Function invoked after the solver advances a timestep. Pass None to clear.
1388
    )");
1389
  transient_solver.def(
672✔
1390
    "SetPostAdvanceCallback",
1391
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
672✔
1392
      &TransientSolver::SetPostAdvanceCallback),
1393
    "Clear the PostAdvance callback by passing None.");
1394
  slv.attr("BackwardEuler") = 1.0;
672✔
1395
  slv.attr("CrankNicolson") = 0.5;
1,344✔
1396
  // clang-format on
1397
}
672✔
1398

1399
// Wrap non-linear k-eigen solver
1400
void
1401
WrapNLKEigen(py::module& slv)
672✔
1402
{
1403
  // clang-format off
1404
  // non-linear k-eigen solver
1405
  auto non_linear_k_eigen_solver = py::class_<NonLinearKEigenSolver, std::shared_ptr<NonLinearKEigenSolver>,
672✔
1406
                                              Solver>(
1407
    slv,
1408
    "NonLinearKEigenSolver",
1409
    R"(
1410
    Non-linear k-eigenvalue solver.
1411

1412
    Wrapper of :cpp:class:`opensn::NonLinearKEigenSolver`.
1413
    )"
1414
  );
672✔
1415
  non_linear_k_eigen_solver.def(
1,344✔
1416
    py::init(
672✔
1417
      [](py::kwargs& params)
29✔
1418
      {
1419
        return NonLinearKEigenSolver::Create(kwargs_to_param_block(params));
29✔
1420
      }
1421
        ),
1422
    R"(
1423
    Construct a non-linear k-eigenvalue solver.
1424

1425
    Parameters
1426
    ----------
1427
    lbs_problem: pyopensn.solver.LBSProblem
1428
        Existing LBSProblem instance.
1429
    nl_abs_tol: float, default=1.0e-8
1430
        Non-linear absolute tolerance.
1431
    nl_rel_tol: float, default=1.0e-8
1432
        Non-linear relative tolerance.
1433
    nl_sol_tol: float, default=1.0e-50
1434
        Non-linear solution tolerance.
1435
    nl_max_its: int, default=50
1436
        Non-linear algorithm maximum iterations.
1437
    l_abs_tol: float, default=1.0e-8
1438
        Linear absolute tolerance.
1439
    l_rel_tol: float, default=1.0e-8
1440
        Linear relative tolerance.
1441
    l_div_tol: float, default=1.0e6
1442
        Linear divergence tolerance.
1443
    l_max_its: int, default=50
1444
        Linear algorithm maximum iterations.
1445
    l_gmres_restart_intvl: int, default=30
1446
        GMRES restart interval.
1447
    l_gmres_breakdown_tol: float, default=1.0e6
1448
        GMRES breakdown tolerance.
1449
    reset_phi0: bool, default=True
1450
        If true, reinitializes scalar fluxes to 1.0.
1451
    num_initial_power_iterations: int, default=0
1452
        Number of initial power iterations before the non-linear solve.
1453
    )"
1454
  );
1455
  non_linear_k_eigen_solver.def(
672✔
1456
    "GetEigenvalue",
1457
    &NonLinearKEigenSolver::GetEigenvalue,
672✔
1458
    R"(
1459
    Return the current k‑eigenvalue.
1460
    )"
1461
  );
1462
  // clang-format on
1463
}
672✔
1464

1465
// Wrap power iteration solvers
1466
void
1467
WrapPIteration(py::module& slv)
672✔
1468
{
1469
  // clang-format off
1470
  // power iteration k-eigen solver
1471
  auto pi_k_eigen_solver = py::class_<PowerIterationKEigenSolver, std::shared_ptr<PowerIterationKEigenSolver>,
672✔
1472
                                      Solver>(
1473
    slv,
1474
    "PowerIterationKEigenSolver",
1475
    R"(
1476
    Power iteration k-eigenvalue solver.
1477

1478
    Wrapper of :cpp:class:`opensn::PowerIterationKEigenSolver`.
1479
    )"
1480
  );
672✔
1481
  pi_k_eigen_solver.def(
1,344✔
1482
    py::init(
672✔
1483
      [](py::kwargs& params)
147✔
1484
      {
1485
        return PowerIterationKEigenSolver::Create(kwargs_to_param_block(params));
147✔
1486
      }
1487
    ),
1488
    R"(
1489
    Construct a power iteration k-eigen solver.
1490

1491
    Parameters
1492
    ----------
1493
    problem: pyopensn.solver.LBSProblem
1494
        Existing DiscreteOrdinatesProblem instance.
1495
    acceleration: pyopensn.solver.DiscreteOrdinatesKEigenAcceleration
1496
        Optional DiscreteOrdinatesKEigenAcceleration instance for acceleration.
1497
    max_iters: int, default = 1000
1498
        Maximum power iterations allowed.
1499
    k_tol: float, default = 1.0e-10
1500
        Tolerance on the k-eigenvalue.
1501
    reset_solution: bool, default=True
1502
        If true, initialize flux moments to 1.0.
1503
    reset_phi0: bool, default=True
1504
        If true, reinitializes scalar fluxes to 1.0.
1505
    )"
1506
  );
1507
  pi_k_eigen_solver.def(
672✔
1508
    "GetEigenvalue",
1509
    &PowerIterationKEigenSolver::GetEigenvalue,
672✔
1510
    R"(
1511
    Return the current k‑eigenvalue.
1512
    )"
1513
  );
1514
  // clang-format on
1515
}
672✔
1516

1517
// Wrap LBS solver
1518
void
1519
WrapDiscreteOrdinatesKEigenAcceleration(py::module& slv)
672✔
1520
{
1521
  // clang-format off
1522
  // discrete ordinates k-eigen acceleration base
1523
  auto acceleration = py::class_<DiscreteOrdinatesKEigenAcceleration,
672✔
1524
                                 std::shared_ptr<DiscreteOrdinatesKEigenAcceleration>>(
1525
    slv,
1526
    "DiscreteOrdinatesKEigenAcceleration",
1527
    R"(
1528
    Base class for discrete ordinates k-eigenvalue acceleration methods.
1529

1530
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesKEigenAcceleration`.
1531
    )"
1532
  );
672✔
1533
  // SCDSA acceleration
1534
  auto scdsa_acceleration = py::class_<SCDSAAcceleration,
672✔
1535
                                       std::shared_ptr<SCDSAAcceleration>,
1536
                                       DiscreteOrdinatesKEigenAcceleration>(
1537
    slv,
1538
    "SCDSAAcceleration",
1539
    R"(
1540
    Construct an SCDSA accelerator for the power iteration k-eigenvalue solver.
1541

1542
    Wrapper of :cpp:class:`opensn::SCDSAAcceleration`.
1543
    )"
1544
  );
672✔
1545
  scdsa_acceleration.def(
1,344✔
1546
    py::init(
672✔
1547
      [](py::kwargs& params)
8✔
1548
      {
1549
        return SCDSAAcceleration::Create(kwargs_to_param_block(params));
8✔
1550
      }
1551
    ),
1552
    R"(
1553
    SCDSA acceleration for the power iteration k-eigenvalue solver.
1554

1555
    Parameters
1556
    ----------
1557
    problem: pyopensn.solver.LBSProblem
1558
        Existing DiscreteOrdinatesProblem instance.
1559
    l_abs_tol: float, defauilt=1.0e-10
1560
        Absolute residual tolerance.
1561
    max_iters: int, default=100
1562
        Maximum allowable iterations.
1563
    verbose: bool, default=False
1564
        If true, enables verbose output.
1565
    petsc_options: str, default="ssss"
1566
        Additional PETSc options.
1567
    pi_max_its: int, default=50
1568
        Maximum allowable iterations for inner power iterations.
1569
    pi_k_tol: float, default=1.0e-10
1570
        k-eigenvalue tolerance for the inner power iterations.
1571
    sdm: str, default="pwld"
1572
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1573
            - 'pwld' : Piecewise Linear Discontinuous
1574
            - 'pwlc' : Piecewise Linear Continuous
1575
    )"
1576
  );
1577
  // SMM acceleration
1578
  auto smm_acceleration = py::class_<SMMAcceleration,
672✔
1579
                                     std::shared_ptr<SMMAcceleration>,
1580
                                     DiscreteOrdinatesKEigenAcceleration>(
1581
    slv,
1582
    "SMMAcceleration",
1583
    R"(
1584
    Construct an SMM accelerator for the power iteration k-eigenvalue solver.
1585

1586
    Wrapper of :cpp:class:`opensn::SMMAcceleration`.
1587
    )"
1588
  );
672✔
1589
  smm_acceleration.def(
1,344✔
1590
    py::init(
672✔
1591
      [](py::kwargs& params)
4✔
1592
      {
1593
        return SMMAcceleration::Create(kwargs_to_param_block(params));
4✔
1594
      }
1595
    ),
1596
    R"(
1597
    SMM acceleration for the power iteration k-eigenvalue solver.
1598

1599
    Warnings
1600
    --------
1601
       SMM acceleration is **experimental** and should be used with caution!
1602
       SMM accleration only supports problems with isotropic scattering.
1603

1604
    Parameters
1605
    ----------
1606
    problem: pyopensn.solver.LBSProblem
1607
        Existing DiscreteOrdinatesProblem instance.
1608
    l_abs_tol: float, defauilt=1.0e-10
1609
        Absolute residual tolerance.
1610
    max_iters: int, default=100
1611
        Maximum allowable iterations.
1612
    verbose: bool, default=False
1613
        If true, enables verbose output.
1614
    petsc_options: str, default="ssss"
1615
        Additional PETSc options.
1616
    pi_max_its: int, default=50
1617
        Maximum allowable iterations for inner power iterations.
1618
    pi_k_tol: float, default=1.0e-10
1619
        k-eigenvalue tolerance for the inner power iterations.
1620
    sdm: str, default="pwld"
1621
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1622
            - 'pwld' : Piecewise Linear Discontinuous
1623
            - 'pwlc' : Piecewise Linear Continuous
1624
    )"
1625
  );
1626
  // clang-format on
1627
}
672✔
1628

1629
// Wrap the solver components of OpenSn
1630
void
1631
py_solver(py::module& pyopensn)
63✔
1632
{
1633
  py::module slv = pyopensn.def_submodule("solver", "Solver module.");
63✔
1634
  WrapProblem(slv);
63✔
1635
  WrapSolver(slv);
63✔
1636
  WrapLBS(slv);
63✔
1637
  WrapSteadyState(slv);
63✔
1638
  WrapTransient(slv);
63✔
1639
  WrapNLKEigen(slv);
63✔
1640
  WrapDiscreteOrdinatesKEigenAcceleration(slv);
63✔
1641
  WrapPIteration(slv);
63✔
1642
}
63✔
1643

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