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

Open-Sn / opensn / 22607884645

01 Mar 2026 08:01PM UTC coverage: 74.192% (+0.04%) from 74.152%
22607884645

push

github

web-flow
Merge pull request #956 from wdhawkins/refactor_problem

Refactor problem creation to be atomic and clean up mode switching

72 of 101 new or added lines in 6 files covered. (71.29%)

5 existing lines in 4 files now uncovered.

20123 of 27123 relevant lines covered (74.19%)

66818235.8 hits per line

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

83.16
/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
  problem.def(
672✔
51
    "GetFieldFunctions",
52
    [](Problem& self)
672✔
53
    {
54
      py::list ff_list;
×
55
      std::vector<std::shared_ptr<FieldFunctionGridBased>>& cpp_ff_list = self.GetFieldFunctions();
×
56
      for (std::shared_ptr<FieldFunctionGridBased>& ff : cpp_ff_list) {
×
57
        ff_list.append(ff);
×
58
      }
59
      return ff_list;
×
60
    },
×
61
    R"(
62
    Get the list of field functions.
63

64
    Returns
65
    -------
66
    List[pyopensn.fieldfunc.FieldFunctionGridBased]
67
        List of grid-based field functions representing solution data such as scalar fluxes.
68
    )"
69
  );
70
  // clang-format on
71
}
672✔
72

73
// Wrap solver
74
void
75
WrapSolver(py::module& slv)
672✔
76
{
77
  // clang-format off
78
  // solver base
79
  auto solver = py::class_<Solver, std::shared_ptr<Solver> >(
672✔
80
    slv,
81
    "Solver",
82
    R"(
83
    Base class for all solvers.
84

85
    Wrapper of :cpp:class:`opensn::Solver`.
86
    )"
87
  );
672✔
88
  solver.def(
672✔
89
    "Initialize",
90
    &Solver::Initialize,
672✔
91
    "Initialize the solver."
92
  );
93
  solver.def(
672✔
94
    "Execute",
95
    &Solver::Execute,
672✔
96
    "Execute the solver."
97
  );
98
  solver.def(
672✔
99
    "Advance",
100
    &Solver::Advance,
672✔
101
    "Advance time values function."
102
  );
103
  // clang-format on
104
}
672✔
105

106
// Wrap LBS solver
107
void
108
WrapLBS(py::module& slv)
672✔
109
{
110
  // clang-format off
111
  // LBS problem
112
  auto lbs_problem = py::class_<LBSProblem, std::shared_ptr<LBSProblem>, Problem>(
672✔
113
    slv,
114
    "LBSProblem",
115
    R"(
116
    Base class for all linear Boltzmann problems.
117

118
    Wrapper of :cpp:class:`opensn::LBSProblem`.
119
    )"
120
  );
672✔
121
  lbs_problem.def(
672✔
122
    "GetScalarFieldFunctionList",
123
    [](LBSProblem& self, bool only_scalar_flux)
672✔
124
    {
125
      py::list field_function_list_per_group;
383✔
126
      for (unsigned int group = 0; group < self.GetNumGroups(); group++)
18,447✔
127
      {
128
        if (only_scalar_flux)
18,064✔
129
        {
130
          std::size_t ff_index = self.MapPhiFieldFunction(group, 0);
11,608✔
131
          field_function_list_per_group.append(self.GetFieldFunctions()[ff_index]);
11,608✔
132
        }
133
        else
134
        {
135
          py::list field_function_list_per_moment;
6,456✔
136
          for (unsigned int moment = 0; moment < self.GetNumMoments(); moment++)
23,832✔
137
          {
138
            std::size_t ff_index = self.MapPhiFieldFunction(group, moment);
17,376✔
139
            field_function_list_per_moment.append(self.GetFieldFunctions()[ff_index]);
17,376✔
140
          }
141
          field_function_list_per_group.append(field_function_list_per_moment);
6,456✔
142
        }
6,456✔
143
      }
144
      return field_function_list_per_group;
383✔
145
    },
×
146
    R"(
147
    Return field functions grouped by energy group and, optionally, by moment.
148

149
    Parameters
150
    ----------
151
    only_scalar_flux : bool, default=True
152
        If True, returns only the zeroth moment (scalar flux) field function for each group.
153
        The result is a flat list of field functions, one per group.
154

155
        If False, returns all moment field functions for each group.
156
        The result is a nested list where each entry corresponds to a group and contains
157
        a list of field functions for all moments (e.g., scalar flux, higher-order moments).
158

159
    Returns
160
    -------
161
    Union[List[pyopensn.fieldfunc.FieldFunctionGridBased], List[List[pyopensn.fieldfunc.FieldFunctionGridBased]]]
162
        The structure of the returned list depends on the `only_scalar_flux` flag.
163

164
    Notes
165
    -----
166
    The moment index varies more rapidly than the group index when `only_scalar_flux` is False.
167
    )",
168
    py::arg("only_scalar_flux") = true
672✔
169
  );
170
  lbs_problem.def(
672✔
171
    "GetPowerFieldFunction",
172
    &LBSProblem::GetPowerFieldFunction,
672✔
173
    R"(
174
    Returns the power generation field function, if enabled.
175
    )"
176
  );
177
  lbs_problem.def(
672✔
178
    "GetTime",
179
    &LBSProblem::GetTime,
672✔
180
    R"(
181
    Get the current simulation time in seconds.
182
    )"
183
  );
184
  lbs_problem.def(
672✔
185
    "GetTimeStep",
186
    &LBSProblem::GetTimeStep,
672✔
187
    R"(
188
    Get the current timestep size.
189
    )"
190
  );
191
  lbs_problem.def(
672✔
192
    "SetOptions",
193
    [](LBSProblem& self, py::kwargs& params)
672✔
194
    {
195
      InputParameters input = LBSProblem::GetOptionsBlock();
×
196
      input.AssignParameters(kwargs_to_param_block(params));
×
197
      self.SetOptions(input);
×
198
    },
×
199
    R"(
200
    Set problem options from a large list of parameters.
201

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

254
    Notes
255
    -----
256
    This call is additive: only options explicitly supplied are updated.
257

258
    This is one of three supported mode-setting paths in Python:
259
      1. ``options={'adjoint': ...}`` in the problem constructor.
260
      2. ``SetOptions(adjoint=...)`` (this method).
261
      3. ``SetAdjoint(...)``.
262

263
    If ``adjoint`` is explicitly supplied in this call and differs from the current mode, OpenSn
264
    performs the same mode-transition behavior as :meth:`LBSProblem.SetAdjoint`. If ``adjoint``
265
    is omitted, the existing mode is unchanged.
266

267
    Options requirements:
268
    - ``restart_writes_enabled=True`` requires non-empty ``write_restart_path``.
269
    - ``write_restart_time_interval>0`` requires ``restart_writes_enabled=True``.
270
    - ``write_restart_time_interval`` must be ``0`` or ``>=30``.
271
    - non-empty ``field_function_prefix`` requires ``field_function_prefix_option='prefix'``.
272
    )"
273
  );
274
  lbs_problem.def(
672✔
275
    "ComputeFissionRate",
276
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
672✔
277
    {
278
      const std::vector<double>* phi_ptr = nullptr;
×
279
      if (scalar_flux_iterate == "old")
×
280
      {
281
        phi_ptr = &self.GetPhiOldLocal();
×
282
      }
283
      else if (scalar_flux_iterate == "new")
×
284
      {
285
        phi_ptr = &self.GetPhiNewLocal();
×
286
      }
287
      else
288
      {
289
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
290
      }
291
      return ComputeFissionRate(self, *phi_ptr);
×
292
    },
293
    R"(
294
    Computes the total fission rate.
295

296
    Parameters
297
    ----------
298
    scalar_flux_iterate : {'old', 'new'}
299
        Specifies which scalar flux vector to use in the calculation.
300
            - 'old': Use the previous scalar flux iterate.
301
            - 'new': Use the current scalar flux iterate.
302

303
    Returns
304
    -------
305
    float
306
        The total fission rate.
307

308
    Raises
309
    ------
310
    ValueError
311
        If `scalar_flux_iterate` is not 'old' or 'new'.
312
    )",
313
    py::arg("scalar_flux_iterate")
672✔
314
  );
315
  lbs_problem.def(
672✔
316
    "ComputeFissionProduction",
317
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
1,416✔
318
    {
319
      const std::vector<double>* phi_ptr = nullptr;
744✔
320
      if (scalar_flux_iterate == "old")
744✔
321
      {
322
        phi_ptr = &self.GetPhiOldLocal();
44✔
323
      }
324
      else if (scalar_flux_iterate == "new")
700✔
325
      {
326
        phi_ptr = &self.GetPhiNewLocal();
700✔
327
      }
328
      else
329
      {
330
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
331
      }
332
      return ComputeFissionProduction(self, *phi_ptr);
744✔
333
    },
334
    R"(
335
    Computes the total fission production (nu*fission).
336

337
    Parameters
338
    ----------
339
    scalar_flux_iterate : {'old', 'new'}
340
        Specifies which scalar flux vector to use in the calculation.
341
            - 'old': Use the previous scalar flux iterate.
342
            - 'new': Use the current scalar flux iterate.
343

344
    Returns
345
    -------
346
    float
347
        The total fission production.
348

349
    Raises
350
    ------
351
    ValueError
352
        If `scalar_flux_iterate` is not 'old' or 'new'.
353
    )",
354
    py::arg("scalar_flux_iterate")
672✔
355
  );
356
  lbs_problem.def(
672✔
357
    "GetPhiOldLocal",
358
    [](LBSProblem& self)
720✔
359
    {
360
      return convert_vector(self.GetPhiOldLocal());
48✔
361
    },
362
    R"(
363
    Get the previous scalar flux iterate (local vector).
364

365
    Returns
366
    -------
367
    memoryview
368
        Memory view of the local old scalar flux vector.
369
    )"
370
  );
371
  lbs_problem.def(
672✔
372
    "GetPhiNewLocal",
373
    [](LBSProblem& self)
728✔
374
    {
375
      return convert_vector(self.GetPhiNewLocal());
56✔
376
    },
377
    R"(
378
    Get the current scalar flux iterate (local vector).
379

380
    Returns
381
    -------
382
    memoryview
383
        Memory view of the local new scalar flux vector.
384
    )"
385
  );
386
  lbs_problem.def(
672✔
387
    "WriteFluxMoments",
388
    [](LBSProblem& self, const std::string& file_base)
704✔
389
    {
390
      LBSSolverIO::WriteFluxMoments(self, file_base);
32✔
391
    },
392
    R"(
393
    Write flux moments to file.
394

395
    Parameters
396
    ----------
397
    file_base: str
398
        File basename.
399
    )",
400
    py::arg("file_base")
672✔
401
  );
402
  lbs_problem.def(
672✔
403
    "CreateAndWriteSourceMoments",
404
    [](LBSProblem& self, const std::string& file_base)
676✔
405
    {
406
      std::vector<double> source_moments = self.MakeSourceMomentsFromPhi();
4✔
407
      LBSSolverIO::WriteFluxMoments(self, file_base, source_moments);
4✔
408
    },
4✔
409
    R"(
410
    Write source moments from latest flux iterate to file.
411

412
    Parameters
413
    ----------
414
    file_base: str
415
        File basename.
416
    )",
417
    py::arg("file_base")
672✔
418
  );
419
  lbs_problem.def(
672✔
420
    "ReadFluxMomentsAndMakeSourceMoments",
421
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
672✔
422
    {
423
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, self.GetExtSrcMomentsLocal());
×
424
      log.Log() << "Making source moments from flux file.";
×
425
      std::vector<double>& temp_phi = self.GetPhiOldLocal();
×
426
      self.GetPhiOldLocal() = self.GetExtSrcMomentsLocal();
×
427
      self.GetExtSrcMomentsLocal() = self.MakeSourceMomentsFromPhi();
×
428
      self.GetPhiOldLocal() = temp_phi;
×
429
    },
×
430
    R"(
431
    Read flux moments and compute corresponding source moments.
432

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

452
    Parameters
453
    ----------
454
    file_base: str
455
        File basename.
456
    single_file_flag: bool
457
        True if all source moments are in a single file.
458
    )",
459
    py::arg("file_base"),
1,344✔
460
    py::arg("single_file_flag")
672✔
461
  );
462
  lbs_problem.def(
672✔
463
    "ReadFluxMoments",
464
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
672✔
465
    {
466
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag);
×
467
    },
468
    R"(
469
    Read flux moment data.
470

471
    Parameters
472
    ----------
473
    file_base: str
474
        File basename.
475
    single_file_flag: bool
476
        True if all flux moments are in a single file.
477
    )",
478
    py::arg("file_base"),
1,344✔
479
    py::arg("single_file_flag")
672✔
480
  );
481
  lbs_problem.def(
672✔
482
    "WriteAngularFluxes",
483
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
676✔
484
    {
485
      DiscreteOrdinatesProblemIO::WriteAngularFluxes(self, file_base);
4✔
486
    },
487
    R"(
488
    Write angular flux data to file.
489

490
    Parameters
491
    ----------
492
    file_base: str
493
        File basename.
494
    )",
495
    py::arg("file_base")
672✔
496
  );
497
  lbs_problem.def(
672✔
498
    "ReadAngularFluxes",
499
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
676✔
500
    {
501
      DiscreteOrdinatesProblemIO::ReadAngularFluxes(self, file_base);
4✔
502
    },
503
    R"(
504
    Read angular fluxes from file.
505

506
    Parameters
507
    ----------
508
    file_base: str
509
        File basename.
510
    )",
511
    py::arg("file_base")
672✔
512
  );
513
  lbs_problem.def(
672✔
514
    "SetPointSources",
515
    [](LBSProblem& self, py::kwargs& params)
672✔
516
    {
517
      for (auto [key, value] : params)
×
518
      {
519
        auto c_key = key.cast<std::string>();
×
520
        if (c_key == "clear_point_sources")
×
521
          self.ClearPointSources();
×
522
        else if (c_key == "point_sources")
×
523
        {
524
          auto sources = value.cast<py::list>();
×
525
          for (auto source : sources)
×
526
          {
527
            self.AddPointSource(source.cast<std::shared_ptr<PointSource>>());
×
528
          }
529
        }
×
530
        else
531
          throw std::runtime_error("Invalid argument provided to SetPointSources.\n");
×
532
      }
×
533
    },
×
534
    R"(
535
    Set or clear point sources.
536

537
    Parameters
538
    ----------
539
    clear_point_sources: bool, default=False
540
        If true, all current the point sources of the problem are deleted.
541
    point_sources: List[pyopensn.source.PointSource]
542
        List of new point sources to be added to the problem.
543
    )"
544
  );
545
  lbs_problem.def(
672✔
546
    "SetVolumetricSources",
547
    [](LBSProblem& self, py::kwargs& params)
708✔
548
    {
549
      for (auto [key, value] : params)
108✔
550
      {
551
        auto c_key = key.cast<std::string>();
36✔
552
        if (c_key == "clear_volumetric_sources")
36✔
553
          self.ClearVolumetricSources();
12✔
554
        else if (c_key == "volumetric_sources")
24✔
555
        {
556
          auto sources = value.cast<py::list>();
24✔
557
          for (auto source : sources)
72✔
558
          {
559
            self.AddVolumetricSource(source.cast<std::shared_ptr<VolumetricSource>>());
48✔
560
          }
561
        }
24✔
562
        else
563
          throw std::runtime_error("Invalid argument provided to SetVolumetricSources.\n");
×
564
      }
36✔
565
    },
36✔
566
    R"(
567
    Set or clear volumetric sources.
568

569
    Parameters
570
    ----------
571
    clear_volumetric_sources: bool, default=False
572
        If true, all current the volumetric sources of the problem are deleted.
573
    volumetric_sources: List[pyopensn.source.VolumetricSource]
574
        List of new volumetric sources to be added to the problem.
575
    )"
576
  );
577
  lbs_problem.def(
672✔
578
    "SetXSMap",
579
    [](LBSProblem& self, py::kwargs& params)
840✔
580
    {
581
      BlockID2XSMap xs_map;
168✔
582
      for (auto [key, value] : params)
504✔
583
      {
584
        auto c_key = key.cast<std::string>();
168✔
585
        if (c_key == "xs_map")
168✔
586
        {
587
          auto xs_entries = value.cast<py::list>();
168✔
588
          for (auto entry : xs_entries)
504✔
589
          {
590
            InputParameters xs_entry_pars = LBSProblem::GetXSMapEntryBlock();
168✔
591
            xs_entry_pars.AssignParameters(pyobj_to_param_block("", entry.cast<py::dict>()));
168✔
592
            const auto& block_ids =
168✔
593
              xs_entry_pars.GetParam("block_ids").GetVectorValue<unsigned int>();
168✔
594
            auto xs = xs_entry_pars.GetSharedPtrParam<MultiGroupXS>("xs");
168✔
595
            for (const auto& block_id : block_ids)
336✔
596
              xs_map[block_id] = xs;
168✔
597
          }
168✔
598
        }
168✔
599
        else
600
          throw std::runtime_error("Invalid argument provided to SetXSMap.\n");
×
601
      }
168✔
602
      self.SetBlockID2XSMap(xs_map);
168✔
603
    },
168✔
604
    R"(
605
    Replace the block-id to cross-section map.
606

607
    Parameters
608
    ----------
609
    xs_map: List[Dict]
610
        A list of block-id to cross-section mapping dictionaries. Each dictionary supports:
611
          - block_ids: List[int] (required)
612
              Mesh block ids to associate with the cross section.
613
          - xs: pyopensn.xs.MultiGroupXS (required)
614
              Cross section object.
615

616
    Notes
617
    -----
618
    Forward/adjoint mode toggles via ``SetOptions(adjoint=...)`` (or the low-level
619
    :meth:`LBSProblem.SetAdjoint`) do not change this map.
620
    The ``MultiGroupXS`` objects themselves are mutable and shared by pointer. If the same
621
    ``MultiGroupXS`` handle is shared across multiple problems, toggling adjoint mode in one
622
    problem also changes the transport mode seen by the others.
623
    )"
624
  );
625
  lbs_problem.def(
672✔
626
    "SetSaveAngularFlux",
627
    [](LBSProblem& self, bool save)
672✔
628
    {
629
      self.SetSaveAngularFlux(save);
×
630
    },
631
    py::arg("save")
672✔
632
  );
633
  lbs_problem.def(
672✔
634
    "ZeroPhi",
635
    [](LBSProblem& self)
672✔
636
    {
637
      self.ZeroPhi();
×
638
    },
639
    R"(
640
    Zero the scalar-flux vectors (``phi_old`` and ``phi_new``) in-place.
641
    )"
642
  );
643
  lbs_problem.def(
672✔
644
    "ZeroPsi",
645
    [](LBSProblem& self)
672✔
646
    {
647
      self.ZeroPsi();
×
648
    },
649
    R"(
650
    Zero the angular-flux storage (if present for this problem type).
651
    )"
652
  );
653
  lbs_problem.def(
672✔
654
    "SetAdjoint",
655
    [](LBSProblem& self, bool adjoint)
692✔
656
    {
657
      self.SetAdjoint(adjoint);
20✔
658
    },
659
    R"(
660
    Set forward/adjoint transport mode.
661

662
    Parameters
663
    ----------
664
    adjoint: bool
665
        ``True`` enables adjoint mode and ``False`` enables forward mode.
666

667
    Notes
668
    -----
669
    This is one of three supported mode-setting paths in Python:
670
      1. ``options={'adjoint': ...}`` in the problem constructor.
671
      2. ``SetOptions(adjoint=...)``.
672
      3. ``SetAdjoint(...)`` (this method).
673

674
    In typical Python workflows, use constructor options or ``SetOptions``.
675

676
    If this changes mode, OpenSn performs a full mode-transition reset:
677
      - Materials are reinitialized in the selected mode.
678
      - Point and volumetric sources are cleared.
679
      - Boundary conditions are cleared.
680
      - Scalar and angular flux vectors are zeroed.
681

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

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

688
    After a mode change, reapply the desired driving terms before solving, typically:
689
      - :meth:`LBSProblem.SetPointSources`
690
      - :meth:`LBSProblem.SetVolumetricSources`
691
      - :meth:`DiscreteOrdinatesProblem.SetBoundaryOptions`
692

693
    This routine is intentionally destructive with respect to source/boundary/flux state
694
    to avoid hidden coupling between forward and adjoint setups.
695
    )"
696
  );
697
  lbs_problem.def(
672✔
698
    "IsAdjoint",
699
    &LBSProblem::IsAdjoint,
672✔
700
    R"(
701
    Return ``True`` if the problem is in adjoint mode, otherwise ``False``.
702
    )"
703
  );
704
  lbs_problem.def(
672✔
705
    "IsTimeDependent",
706
    &LBSProblem::IsTimeDependent,
672✔
707
    R"(
708
    Return ``True`` if the problem is in time-dependent mode, otherwise ``False``.
709
    )"
710
  );
711

712
  // discrete ordinate solver
713
  auto do_problem = py::class_<DiscreteOrdinatesProblem, std::shared_ptr<DiscreteOrdinatesProblem>,
672✔
714
                               LBSProblem>(
715
    slv,
716
    "DiscreteOrdinatesProblem",
717
    R"(
718
    Base class for discrete ordinates problems in Cartesian geometry.
719

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

724
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesProblem`.
725
    )"
726
  );
672✔
727
  do_problem.def(
1,344✔
728
    py::init(
672✔
729
      [](py::kwargs& params)
498✔
730
      {
731
        return DiscreteOrdinatesProblem::Create(kwargs_to_param_block(params));
498✔
732
      }
733
    ),
734
    R"(
735
    Construct a discrete ordinates problem with Cartesian geometry.
736

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

847
    Notes
848
    -----
849
    Switch problem from steady-state to time-dependent mode. This updates problem
850
    internals (sweep chunk mode and source-function) while preserving user boundary
851
    conditions and fixed sources.  If ``save_angular_flux`` is false, this transition
852
    enables it.
853
    )"
854
  );
855
  do_problem.def(
672✔
856
    "SetSteadyStateMode",
857
    &DiscreteOrdinatesProblem::SetSteadyStateMode,
672✔
858
    R"(
859
    Set the problem to steady-state mode.
860

861
    Notes
862
    -----
863
    Switch problem from time-dependent to steady-state mode. This updates problem
864
    internals (sweep chunk mode and source-function) while preserving user boundary
865
    conditions and fixed sources. If ``save_angular_flux`` was auto-enabled when
866
    entering time-dependent mode, this call restores the previous value.
867
    )"
868
  );
869
  do_problem.def(
672✔
870
    "IsTimeDependent",
871
    &DiscreteOrdinatesProblem::IsTimeDependent,
672✔
872
    R"(
873
    Return ``True`` if the problem is currently in time-dependent mode.
874
    )"
875
  );
876
  do_problem.def(
672✔
877
    "SetOptions",
878
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
684✔
879
    {
880
      InputParameters input = DiscreteOrdinatesProblem::GetOptionsBlock();
12✔
881
      input.AssignParameters(kwargs_to_param_block(params));
12✔
882
      self.SetOptions(input);
12✔
883
    },
12✔
884
    R"(
885
    Set problem options from a large list of parameters.
886

887
    Parameters
888
    ----------
889
    **kwargs
890
        Same option keys and semantics as :meth:`LBSProblem.SetOptions`.
891

892
    Notes
893
    -----
894
    This call is additive: only options explicitly supplied are updated.
895

896
    As with :meth:`LBSProblem.SetOptions`, if ``adjoint`` is explicitly supplied and differs from
897
    the current mode, the same mode-transition behavior as :meth:`LBSProblem.SetAdjoint` is used.
898
    )"
899
  );
900
  do_problem.def(
672✔
901
    "SetBoundaryOptions",
902
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
676✔
903
    {
904
      for (auto [key, value] : params)
12✔
905
      {
906
        auto c_key = key.cast<std::string>();
4✔
907
        if (c_key == "clear_boundary_conditions")
4✔
908
          self.ClearBoundaries();
×
909
        else if (c_key == "boundary_conditions")
4✔
910
        {
911
          auto boundaries = value.cast<py::list>();
4✔
912
          for (auto boundary : boundaries)
24✔
913
          {
914
            InputParameters input = DiscreteOrdinatesProblem::GetBoundaryOptionsBlock();
16✔
915
            input.AssignParameters(pyobj_to_param_block("", boundary.cast<py::dict>()));
16✔
916
            self.SetBoundaryOptions(input);
16✔
917
          }
16✔
918
        }
4✔
919
        else
920
          throw std::runtime_error("Invalid argument provided to SetBoundaryOptions.\n");
×
921
      }
4✔
922
    },
4✔
923
    R"(
924
    Set or clear boundary conditions.
925

926
    Parameters
927
    ----------
928
    clear_boundary_conditions: bool, default=False
929
        If true, all current boundary conditions are deleted.
930
    boundary_conditions: List[Dict]
931
        A list of boundary condition dictionaries. Each dictionary supports:
932
          - name: str (required)
933
              Boundary name that identifies the specific boundary.
934
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
935
              Boundary type specification.
936
          - group_strength: List[float], optional
937
              Required when ``type='isotropic'``. Isotropic strength per group.
938
          - function: AngularFluxFunction, optional
939
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
940

941
    Notes
942
    -----
943
    Mode transitions via ``SetOptions(adjoint=...)`` (or the low-level
944
    :meth:`LBSProblem.SetAdjoint`) clear all boundary conditions.
945
    Reapply boundaries with this method before solving in the new mode.
946
    )"
947
  );
948
  do_problem.def(
672✔
949
    "GetPsi",
950
    [](DiscreteOrdinatesProblem& self)
680✔
951
    {
952
      const auto& psi = self.GetPsiNewLocal();
8✔
953
      py::list psi_list;
8✔
954
      for (const auto& vec : psi)
16✔
955
      {
956
        auto array = py::array_t<double>(static_cast<py::ssize_t>(vec.size()),
8✔
957
                                         vec.data(),
958
                                         py::cast(self));
8✔
959
        psi_list.append(array);
8✔
960
      }
8✔
961
      return psi_list;
8✔
962
    },
×
963
    R"(
964
    Return psi as a list of NumPy arrays (float64), using zero-copy views into the
965
    underlying data.
966
    )"
967
  );
968
  do_problem.def(
672✔
969
    "GetAngularFieldFunctionList",
970
    [](DiscreteOrdinatesProblem& self, py::list groups, py::list angles)
672✔
971
    {
972
      std::vector<unsigned int> group_ids;
×
973
      std::vector<size_t> angle_ids;
×
974
      group_ids.reserve(groups.size());
×
975
      angle_ids.reserve(angles.size());
×
976

977
      for (py::handle g : groups)
×
978
        group_ids.push_back(g.cast<unsigned int>());
×
979
      for (py::handle a : angles)
×
980
        angle_ids.push_back(a.cast<size_t>());
×
981

NEW
982
      auto ff_list = self.GetAngularFluxFieldFunctionList(group_ids, angle_ids);
×
NEW
983
      py::list out;
×
NEW
984
      for (const auto& ff : ff_list)
×
NEW
985
        out.append(ff);
×
NEW
986
      return out;
×
UNCOV
987
    },
×
988
    R"(
989
    Return field functions for selected angular flux components.
990

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

995
    Example
996
    -------
997
    ```python
998
    solver.Initialize()
999
    solver.Execute()
1000
    ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
1001
    ```
1002

1003
    For transient/time-dependent runs, call this after each timestep. Two common
1004
    patterns are:
1005

1006
    1) Use `TransientSolver.Execute()` with a post-advance callback:
1007
    ```python
1008
    solver = TransientSolver(problem=phys)
1009
    solver.Initialize()
1010

1011
    def post_advance():
1012
        ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
1013
        FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
1014

1015
    solver.SetPostAdvanceCallback(post_advance)
1016
    solver.Execute()
1017
    ```
1018

1019
    2) Use a custom Python loop with `TransientSolver.Advance()`:
1020
    ```python
1021
    solver = TransientSolver(problem=phys)
1022
    solver.Initialize()
1023
    for _ in range(num_steps):
1024
        solver.Advance()
1025
        ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
1026
        FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
1027
    ```
1028

1029
    Parameters
1030
    ----------
1031
    groups : List[int]
1032
        Global group indices to export.
1033
    angles : List[int]
1034
        Angle indices within the groupset quadrature for each group.
1035

1036
    Returns
1037
    -------
1038
    List[pyopensn.fieldfunc.FieldFunctionGridBased]
1039
        Field functions for the requested (group, angle) pairs.
1040
    )",
1041
    py::arg("groups"),
1,344✔
1042
    py::arg("angles")
672✔
1043
  );
1044
  do_problem.def(
672✔
1045
    "ComputeLeakage",
1046
    [](DiscreteOrdinatesProblem& self, py::list bnd_names)
711✔
1047
    {
1048
      auto grid = self.GetGrid();
39✔
1049
      // get the supported boundaries
1050
      std::map<std::string, std::uint64_t> allowed_bd_names = grid->GetBoundaryNameMap();
39✔
1051
      std::map<std::uint64_t, std::string> allowed_bd_ids = grid->GetBoundaryIDMap();
39✔
1052
      const auto coord_sys = grid->GetCoordinateSystem();
39✔
1053
      const auto mesh_type = grid->GetType();
39✔
1054
      const auto dim = grid->GetDimension();
39✔
1055
      // get the boundaries to parse, preserving user order
1056
      std::vector<std::uint64_t> bndry_ids;
39✔
1057
      if (bnd_names.size() > 1)
39✔
1058
      {
1059
        for (py::handle name : bnd_names)
184✔
1060
        {
1061
          auto sname = name.cast<std::string>();
112✔
1062
          if (coord_sys == CoordinateSystemType::CYLINDRICAL && dim == 2)
112✔
1063
          {
1064
            if (sname == "xmin" || sname == "xmax" || sname == "ymin" || sname == "ymax")
24✔
1065
              throw std::runtime_error("ComputeLeakage: Boundary name '" + sname +
×
1066
                                       "' is invalid for cylindrical orthogonal meshes. "
1067
                                       "Use rmin, rmax, zmin, zmax.");
×
1068

1069
            if (mesh_type == MeshType::ORTHOGONAL)
24✔
1070
            {
1071
              if (sname == "rmin") sname = "xmin";
24✔
1072
              else if (sname == "rmax") sname = "xmax";
24✔
1073
              else if (sname == "zmin") sname = "ymin";
16✔
1074
              else if (sname == "zmax") sname = "ymax";
8✔
1075
            }
1076
          }
1077
          bndry_ids.push_back(allowed_bd_names.at(sname));
112✔
1078
        }
112✔
1079
      }
1080
      else
1081
      {
1082
        bndry_ids = self.GetGrid()->GetUniqueBoundaryIDs();
9✔
1083
      }
1084
      // compute the leakage
1085
      std::map<std::uint64_t, std::vector<double>> leakage = ComputeLeakage(self, bndry_ids);
39✔
1086
      const bool rz_ortho = (coord_sys == CoordinateSystemType::CYLINDRICAL &&
78✔
1087
                             mesh_type == MeshType::ORTHOGONAL && dim == 2);
39✔
1088

1089
      auto to_rz_name = [](const std::string& name)
63✔
1090
      {
1091
        if (name == "xmin") return std::string("rmin");
24✔
1092
        if (name == "xmax") return std::string("rmax");
24✔
1093
        if (name == "ymin") return std::string("zmin");
16✔
1094
        if (name == "ymax") return std::string("zmax");
8✔
1095
        return name;
×
1096
      };
1097

1098
      // convert result to native Python
1099
      py::dict result;
39✔
1100
      for (const auto& bndry_id : bndry_ids)
157✔
1101
      {
1102
        const auto it = leakage.find(bndry_id);
118✔
1103
        if (it == leakage.end())
118✔
1104
          continue;
×
1105
        // construct numpy array and copy contents
1106
        const auto& grp_wise_leakage = it->second;
118✔
1107
        py::array_t<double> np_vector(py::ssize_t(grp_wise_leakage.size()));
118✔
1108
        auto buffer = np_vector.request();
118✔
1109
        auto *np_vector_data = static_cast<double*>(buffer.ptr);
118✔
1110
        std::copy(grp_wise_leakage.begin(), grp_wise_leakage.end(), np_vector_data);
118✔
1111
        std::string name = allowed_bd_ids.at(bndry_id);
118✔
1112
        if (rz_ortho)
118✔
1113
          name = to_rz_name(name);
24✔
1114
        result[py::str(name)] = std::move(np_vector);
236✔
1115
      }
118✔
1116

1117
      return result;
39✔
1118
    },
78✔
1119
    R"(
1120
    Compute leakage for the problem.
1121

1122
    Parameters
1123
    ----------
1124
    bnd_names : List[str]
1125
        A list of boundary names for which leakage should be computed.
1126

1127
    Returns
1128
    -------
1129
    Dict[str, numpy.ndarray]
1130
        A dictionary mapping boundary names to group-wise leakage vectors.
1131
        Each array contains the outgoing angular flux (per group) integrated over
1132
        the corresponding boundary surface.
1133

1134
    Raises
1135
    ------
1136
    RuntimeError
1137
        If `save_angular_flux` option was not enabled during problem setup.
1138

1139
    ValueError
1140
        If one or more boundary ids are not present on the current mesh.
1141
    )",
1142
    py::arg("bnd_names")
672✔
1143
  );
1144

1145
  // discrete ordinates curvilinear problem
1146
  auto do_curvilinear_problem = py::class_<DiscreteOrdinatesCurvilinearProblem,
672✔
1147
                                           std::shared_ptr<DiscreteOrdinatesCurvilinearProblem>,
1148
                                           DiscreteOrdinatesProblem>(
1149
    slv,
1150
    "DiscreteOrdinatesCurvilinearProblem",
1151
    R"(
1152
    Base class for discrete ordinates problems in curvilinear geometry.
1153

1154
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesCurvilinearProblem`.
1155
    )"
1156
  );
672✔
1157
  do_curvilinear_problem.def(
1,344✔
1158
    py::init(
672✔
1159
      [](py::kwargs& params)
80✔
1160
      {
1161
        return DiscreteOrdinatesCurvilinearProblem::Create(kwargs_to_param_block(params));
80✔
1162
      }
1163
    ),
1164
    R"(
1165
    Construct a discrete ordinates problem for curvilinear geometry.
1166

1167
    Warnings
1168
    --------
1169
       DiscreteOrdinatesCurvilinearProblem is **experimental** and should be used with caution!
1170

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

1269
// Wrap steady-state solver
1270
void
1271
WrapSteadyState(py::module& slv)
672✔
1272
{
1273
  // clang-format off
1274
  // steady state solver
1275
  auto steady_state_solver = py::class_<SteadyStateSourceSolver, std::shared_ptr<SteadyStateSourceSolver>,
672✔
1276
                                        Solver>(
1277
    slv,
1278
    "SteadyStateSourceSolver",
1279
    R"(
1280
    Steady state solver.
1281

1282
    Wrapper of :cpp:class:`opensn::SteadyStateSourceSolver`.
1283
    )"
1284
  );
672✔
1285
  steady_state_solver.def(
1,344✔
1286
    py::init(
672✔
1287
      [](py::kwargs& params)
357✔
1288
      {
1289
        return SteadyStateSourceSolver::Create(kwargs_to_param_block(params));
357✔
1290
      }
1291
    ),
1292
    R"(
1293
    Construct a steady state solver.
1294

1295
    Parameters
1296
    ----------
1297
    pyopensn.solver.LBSProblem : LBSProblem
1298
        Existing LBSProblem instance.
1299
    )"
1300
  );
1301
  // clang-format on
1302
}
672✔
1303

1304
// Wrap transient solver
1305
void
1306
WrapTransient(py::module& slv)
672✔
1307
{
1308
  // clang-format off
1309
  auto transient_solver =
672✔
1310
    py::class_<TransientSolver, std::shared_ptr<TransientSolver>, Solver>(
1311
      slv,
1312
      "TransientSolver",
1313
      R"(
1314
      Transient solver.
1315

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

1329
    Parameters
1330
    ----------
1331
    pyopensn.solver.DiscreteOrdinatesProblem : DiscreteOrdinatesProblem
1332
        Existing discrete ordinates problem instance.
1333
    dt : float, optional, default=2.0e-3
1334
        Time step size used during the simulation.
1335
    stop_time : float, optional, default=0.1
1336
        Simulation end time.
1337
    theta : float, optional, default=0.5
1338
        Time differencing scheme parameter.
1339
    initial_state : str, optional, default="existing"
1340
        Initial state for the transient solve. Allowed values: existing, zero.
1341
        In "zero" mode, scalar flux vectors are reset to zero.
1342
    verbose : bool, optional, default=True
1343
        Enable verbose logging.
1344
    )"
1345
  );
1346
  transient_solver.def(
672✔
1347
    "SetTimeStep",
1348
    &TransientSolver::SetTimeStep,
672✔
1349
    R"(
1350
    Set the timestep size used by :meth:`Advance`.
1351

1352
    Parameters
1353
    ----------
1354
    dt : float
1355
        New timestep size.
1356
    )");
1357
  transient_solver.def(
672✔
1358
    "SetTheta",
1359
    &TransientSolver::SetTheta,
672✔
1360
    R"(
1361
    Set the theta parameter used by :meth:`Advance`.
1362

1363
    Parameters
1364
    ----------
1365
    theta : float
1366
        Theta value between 1.0e-16 and 1.
1367
    )");
1368
  transient_solver.def(
672✔
1369
    "Advance",
1370
    &TransientSolver::Advance,
672✔
1371
    R"(
1372
    Advance the solver by a single timestep.
1373

1374
    Notes
1375
    -----
1376
    You must call :meth:`Initialize` before calling :meth:`Advance` or
1377
    :meth:`Execute`.
1378
    )");
1379
  transient_solver.def(
672✔
1380
    "SetPreAdvanceCallback",
1381
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
672✔
1382
      &TransientSolver::SetPreAdvanceCallback),
1383
    R"(
1384
    Register a callback that runs before each advance within :meth:`Execute`.
1385

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

1405
    Parameters
1406
    ----------
1407
    callback : Optional[Callable[[], None]]
1408
        Function invoked after the solver advances a timestep. Pass None to clear.
1409
    )");
1410
  transient_solver.def(
672✔
1411
    "SetPostAdvanceCallback",
1412
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
672✔
1413
      &TransientSolver::SetPostAdvanceCallback),
1414
    "Clear the PostAdvance callback by passing None.");
1415
  slv.attr("BackwardEuler") = 1.0;
672✔
1416
  slv.attr("CrankNicolson") = 0.5;
1,344✔
1417
  // clang-format on
1418
}
672✔
1419

1420
// Wrap non-linear k-eigen solver
1421
void
1422
WrapNLKEigen(py::module& slv)
672✔
1423
{
1424
  // clang-format off
1425
  // non-linear k-eigen solver
1426
  auto non_linear_k_eigen_solver = py::class_<NonLinearKEigenSolver, std::shared_ptr<NonLinearKEigenSolver>,
672✔
1427
                                              Solver>(
1428
    slv,
1429
    "NonLinearKEigenSolver",
1430
    R"(
1431
    Non-linear k-eigenvalue solver.
1432

1433
    Wrapper of :cpp:class:`opensn::NonLinearKEigenSolver`.
1434
    )"
1435
  );
672✔
1436
  non_linear_k_eigen_solver.def(
1,344✔
1437
    py::init(
672✔
1438
      [](py::kwargs& params)
28✔
1439
      {
1440
        return NonLinearKEigenSolver::Create(kwargs_to_param_block(params));
28✔
1441
      }
1442
        ),
1443
    R"(
1444
    Construct a non-linear k-eigenvalue solver.
1445

1446
    Parameters
1447
    ----------
1448
    lbs_problem: pyopensn.solver.LBSProblem
1449
        Existing LBSProblem instance.
1450
    nl_abs_tol: float, default=1.0e-8
1451
        Non-linear absolute tolerance.
1452
    nl_rel_tol: float, default=1.0e-8
1453
        Non-linear relative tolerance.
1454
    nl_sol_tol: float, default=1.0e-50
1455
        Non-linear solution tolerance.
1456
    nl_max_its: int, default=50
1457
        Non-linear algorithm maximum iterations.
1458
    l_abs_tol: float, default=1.0e-8
1459
        Linear absolute tolerance.
1460
    l_rel_tol: float, default=1.0e-8
1461
        Linear relative tolerance.
1462
    l_div_tol: float, default=1.0e6
1463
        Linear divergence tolerance.
1464
    l_max_its: int, default=50
1465
        Linear algorithm maximum iterations.
1466
    l_gmres_restart_intvl: int, default=30
1467
        GMRES restart interval.
1468
    l_gmres_breakdown_tol: float, default=1.0e6
1469
        GMRES breakdown tolerance.
1470
    reset_phi0: bool, default=True
1471
        If true, reinitializes scalar fluxes to 1.0.
1472
    num_initial_power_iterations: int, default=0
1473
        Number of initial power iterations before the non-linear solve.
1474
    )"
1475
  );
1476
  non_linear_k_eigen_solver.def(
672✔
1477
    "GetEigenvalue",
1478
    &NonLinearKEigenSolver::GetEigenvalue,
672✔
1479
    R"(
1480
    Return the current k‑eigenvalue.
1481
    )"
1482
  );
1483
  // clang-format on
1484
}
672✔
1485

1486
// Wrap power iteration solvers
1487
void
1488
WrapPIteration(py::module& slv)
672✔
1489
{
1490
  // clang-format off
1491
  // power iteration k-eigen solver
1492
  auto pi_k_eigen_solver = py::class_<PowerIterationKEigenSolver, std::shared_ptr<PowerIterationKEigenSolver>,
672✔
1493
                                      Solver>(
1494
    slv,
1495
    "PowerIterationKEigenSolver",
1496
    R"(
1497
    Power iteration k-eigenvalue solver.
1498

1499
    Wrapper of :cpp:class:`opensn::PowerIterationKEigenSolver`.
1500
    )"
1501
  );
672✔
1502
  pi_k_eigen_solver.def(
1,344✔
1503
    py::init(
672✔
1504
      [](py::kwargs& params)
147✔
1505
      {
1506
        return PowerIterationKEigenSolver::Create(kwargs_to_param_block(params));
147✔
1507
      }
1508
    ),
1509
    R"(
1510
    Construct a power iteration k-eigen solver.
1511

1512
    Parameters
1513
    ----------
1514
    problem: pyopensn.solver.LBSProblem
1515
        Existing DiscreteOrdinatesProblem instance.
1516
    acceleration: pyopensn.solver.DiscreteOrdinatesKEigenAcceleration
1517
        Optional DiscreteOrdinatesKEigenAcceleration instance for acceleration.
1518
    max_iters: int, default = 1000
1519
        Maximum power iterations allowed.
1520
    k_tol: float, default = 1.0e-10
1521
        Tolerance on the k-eigenvalue.
1522
    reset_solution: bool, default=True
1523
        If true, initialize flux moments to 1.0.
1524
    reset_phi0: bool, default=True
1525
        If true, reinitializes scalar fluxes to 1.0.
1526
    )"
1527
  );
1528
  pi_k_eigen_solver.def(
672✔
1529
    "GetEigenvalue",
1530
    &PowerIterationKEigenSolver::GetEigenvalue,
672✔
1531
    R"(
1532
    Return the current k‑eigenvalue.
1533
    )"
1534
  );
1535
  // clang-format on
1536
}
672✔
1537

1538
// Wrap LBS solver
1539
void
1540
WrapDiscreteOrdinatesKEigenAcceleration(py::module& slv)
672✔
1541
{
1542
  // clang-format off
1543
  // discrete ordinates k-eigen acceleration base
1544
  auto acceleration = py::class_<DiscreteOrdinatesKEigenAcceleration,
672✔
1545
                                 std::shared_ptr<DiscreteOrdinatesKEigenAcceleration>>(
1546
    slv,
1547
    "DiscreteOrdinatesKEigenAcceleration",
1548
    R"(
1549
    Base class for discrete ordinates k-eigenvalue acceleration methods.
1550

1551
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesKEigenAcceleration`.
1552
    )"
1553
  );
672✔
1554
  // SCDSA acceleration
1555
  auto scdsa_acceleration = py::class_<SCDSAAcceleration,
672✔
1556
                                       std::shared_ptr<SCDSAAcceleration>,
1557
                                       DiscreteOrdinatesKEigenAcceleration>(
1558
    slv,
1559
    "SCDSAAcceleration",
1560
    R"(
1561
    Construct an SCDSA accelerator for the power iteration k-eigenvalue solver.
1562

1563
    Wrapper of :cpp:class:`opensn::SCDSAAcceleration`.
1564
    )"
1565
  );
672✔
1566
  scdsa_acceleration.def(
1,344✔
1567
    py::init(
672✔
1568
      [](py::kwargs& params)
8✔
1569
      {
1570
        return SCDSAAcceleration::Create(kwargs_to_param_block(params));
8✔
1571
      }
1572
    ),
1573
    R"(
1574
    SCDSA acceleration for the power iteration k-eigenvalue solver.
1575

1576
    Parameters
1577
    ----------
1578
    problem: pyopensn.solver.LBSProblem
1579
        Existing DiscreteOrdinatesProblem instance.
1580
    l_abs_tol: float, defauilt=1.0e-10
1581
        Absolute residual tolerance.
1582
    max_iters: int, default=100
1583
        Maximum allowable iterations.
1584
    verbose: bool, default=False
1585
        If true, enables verbose output.
1586
    petsc_options: str, default="ssss"
1587
        Additional PETSc options.
1588
    pi_max_its: int, default=50
1589
        Maximum allowable iterations for inner power iterations.
1590
    pi_k_tol: float, default=1.0e-10
1591
        k-eigenvalue tolerance for the inner power iterations.
1592
    sdm: str, default="pwld"
1593
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1594
            - 'pwld' : Piecewise Linear Discontinuous
1595
            - 'pwlc' : Piecewise Linear Continuous
1596
    )"
1597
  );
1598
  // SMM acceleration
1599
  auto smm_acceleration = py::class_<SMMAcceleration,
672✔
1600
                                     std::shared_ptr<SMMAcceleration>,
1601
                                     DiscreteOrdinatesKEigenAcceleration>(
1602
    slv,
1603
    "SMMAcceleration",
1604
    R"(
1605
    Construct an SMM accelerator for the power iteration k-eigenvalue solver.
1606

1607
    Wrapper of :cpp:class:`opensn::SMMAcceleration`.
1608
    )"
1609
  );
672✔
1610
  smm_acceleration.def(
1,344✔
1611
    py::init(
672✔
1612
      [](py::kwargs& params)
4✔
1613
      {
1614
        return SMMAcceleration::Create(kwargs_to_param_block(params));
4✔
1615
      }
1616
    ),
1617
    R"(
1618
    SMM acceleration for the power iteration k-eigenvalue solver.
1619

1620
    Warnings
1621
    --------
1622
       SMM acceleration is **experimental** and should be used with caution!
1623
       SMM accleration only supports problems with isotropic scattering.
1624

1625
    Parameters
1626
    ----------
1627
    problem: pyopensn.solver.LBSProblem
1628
        Existing DiscreteOrdinatesProblem instance.
1629
    l_abs_tol: float, defauilt=1.0e-10
1630
        Absolute residual tolerance.
1631
    max_iters: int, default=100
1632
        Maximum allowable iterations.
1633
    verbose: bool, default=False
1634
        If true, enables verbose output.
1635
    petsc_options: str, default="ssss"
1636
        Additional PETSc options.
1637
    pi_max_its: int, default=50
1638
        Maximum allowable iterations for inner power iterations.
1639
    pi_k_tol: float, default=1.0e-10
1640
        k-eigenvalue tolerance for the inner power iterations.
1641
    sdm: str, default="pwld"
1642
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1643
            - 'pwld' : Piecewise Linear Discontinuous
1644
            - 'pwlc' : Piecewise Linear Continuous
1645
    )"
1646
  );
1647
  // clang-format on
1648
}
672✔
1649

1650
// Wrap the solver components of OpenSn
1651
void
1652
py_solver(py::module& pyopensn)
63✔
1653
{
1654
  py::module slv = pyopensn.def_submodule("solver", "Solver module.");
63✔
1655
  WrapProblem(slv);
63✔
1656
  WrapSolver(slv);
63✔
1657
  WrapLBS(slv);
63✔
1658
  WrapSteadyState(slv);
63✔
1659
  WrapTransient(slv);
63✔
1660
  WrapNLKEigen(slv);
63✔
1661
  WrapDiscreteOrdinatesKEigenAcceleration(slv);
63✔
1662
  WrapPIteration(slv);
63✔
1663
}
63✔
1664

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