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

Open-Sn / opensn / 22837428894

09 Mar 2026 03:41AM UTC coverage: 74.368% (+0.06%) from 74.311%
22837428894

push

github

web-flow
Merge pull request #969 from wdhawkins/testing_artifacts

Log test output and post as workflow artifacts

20130 of 27068 relevant lines covered (74.37%)

66820174.91 hits per line

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

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

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

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

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

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

98
    Wrapper of :cpp:class:`opensn::LBSProblem`.
99
    )"
100
  );
704✔
101
  lbs_problem.def(
704✔
102
    "GetScalarFluxFieldFunction",
103
    [](LBSProblem& self, bool only_scalar_flux)
704✔
104
    {
105
      py::list field_function_list_per_group;
396✔
106
      for (unsigned int group = 0; group < self.GetNumGroups(); group++)
18,473✔
107
      {
108
        if (only_scalar_flux)
18,077✔
109
        {
110
          field_function_list_per_group.append(self.GetScalarFluxFieldFunction(group, 0));
23,242✔
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;
396✔
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
704✔
148
  );
149
  lbs_problem.def(
704✔
150
    "GetPowerFieldFunction",
151
    &LBSProblem::GetPowerFieldFunction,
704✔
152
    R"(
153
    Returns the power generation field function, if enabled.
154
    )"
155
  );
156
  lbs_problem.def(
704✔
157
    "GetTime",
158
    &LBSProblem::GetTime,
704✔
159
    R"(
160
    Get the current simulation time in seconds.
161
    )"
162
  );
163
  lbs_problem.def(
704✔
164
    "GetTimeStep",
165
    &LBSProblem::GetTimeStep,
704✔
166
    R"(
167
    Get the current timestep size.
168
    )"
169
  );
170
  lbs_problem.def(
704✔
171
    "SetOptions",
172
    [](LBSProblem& self, py::kwargs& params)
704✔
173
    {
174
      InputParameters input = LBSProblem::GetOptionsBlock();
×
175
      input.AssignParameters(kwargs_to_param_block(params));
×
176
      self.SetOptions(input);
×
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(
704✔
254
    "ComputeFissionRate",
255
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
704✔
256
    {
257
      const std::vector<double>* phi_ptr = nullptr;
×
258
      if (scalar_flux_iterate == "old")
×
259
      {
260
        phi_ptr = &self.GetPhiOldLocal();
×
261
      }
262
      else if (scalar_flux_iterate == "new")
×
263
      {
264
        phi_ptr = &self.GetPhiNewLocal();
×
265
      }
266
      else
267
      {
268
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
269
      }
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")
704✔
293
  );
294
  lbs_problem.def(
704✔
295
    "ComputeFissionProduction",
296
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
1,648✔
297
    {
298
      const std::vector<double>* phi_ptr = nullptr;
944✔
299
      if (scalar_flux_iterate == "old")
944✔
300
      {
301
        phi_ptr = &self.GetPhiOldLocal();
44✔
302
      }
303
      else if (scalar_flux_iterate == "new")
900✔
304
      {
305
        phi_ptr = &self.GetPhiNewLocal();
900✔
306
      }
307
      else
308
      {
309
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
310
      }
311
      return ComputeFissionProduction(self, *phi_ptr);
944✔
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")
704✔
334
  );
335
  lbs_problem.def(
704✔
336
    "GetPhiOldLocal",
337
    [](LBSProblem& self)
752✔
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(
704✔
351
    "GetPhiNewLocal",
352
    [](LBSProblem& self)
760✔
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(
704✔
366
    "WriteFluxMoments",
367
    [](LBSProblem& self, const std::string& file_base)
736✔
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")
704✔
380
  );
381
  lbs_problem.def(
704✔
382
    "CreateAndWriteSourceMoments",
383
    [](LBSProblem& self, const std::string& file_base)
708✔
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")
704✔
397
  );
398
  lbs_problem.def(
704✔
399
    "ReadFluxMomentsAndMakeSourceMoments",
400
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
704✔
401
    {
402
      auto ext_src_moments = self.GetExtSrcMomentsLocal();
×
403
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, ext_src_moments);
×
404
      self.SetExtSrcMomentsFrom(ext_src_moments);
×
405
      log.Log() << "Making source moments from flux file.";
×
406
      const auto temp_phi = self.GetPhiOldLocal();
×
407
      self.SetPhiOldFrom(self.GetExtSrcMomentsLocal());
×
408
      self.SetExtSrcMomentsFrom(self.MakeSourceMomentsFromPhi());
×
409
      self.SetPhiOldFrom(temp_phi);
×
410
    },
×
411
    R"(
412
    Read flux moments and compute corresponding source moments.
413

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

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

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

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

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

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

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

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

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

645
    Parameters
646
    ----------
647
    adjoint: bool
648
        ``True`` enables adjoint mode and ``False`` enables forward mode.
649

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

657
    In typical Python workflows, use constructor options or ``SetOptions``.
658

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

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

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

671
    After a mode change, reapply the desired driving terms before solving, typically:
672
      - :meth:`LBSProblem.SetPointSources`
673
      - :meth:`LBSProblem.SetVolumetricSources`
674
      - :meth:`DiscreteOrdinatesProblem.SetBoundaryOptions`
675

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

695
  // discrete ordinate solver
696
  auto do_problem = py::class_<DiscreteOrdinatesProblem, std::shared_ptr<DiscreteOrdinatesProblem>,
704✔
697
                               LBSProblem>(
698
    slv,
699
    "DiscreteOrdinatesProblem",
700
    R"(
701
    Base class for discrete ordinates problems in Cartesian geometry.
702

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

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

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

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

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

870
    Parameters
871
    ----------
872
    **kwargs
873
        Same option keys and semantics as :meth:`LBSProblem.SetOptions`.
874

875
    Notes
876
    -----
877
    This call is additive: only options explicitly supplied are updated.
878

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

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

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

960
      for (py::handle g : groups)
×
961
        group_ids.push_back(g.cast<unsigned int>());
×
962
      for (py::handle a : angles)
×
963
        angle_ids.push_back(a.cast<size_t>());
×
964

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

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

978
    Example
979
    -------
980
    ```python
981
    solver.Initialize()
982
    solver.Execute()
983
    ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
984
    ```
985

986
    For transient/time-dependent runs, call this after each timestep. Two common
987
    patterns are:
988

989
    1) Use `TransientSolver.Execute()` with a post-advance callback:
990
    ```python
991
    solver = TransientSolver(problem=phys)
992
    solver.Initialize()
993

994
    def post_advance():
995
        ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
996
        FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
997

998
    solver.SetPostAdvanceCallback(post_advance)
999
    solver.Execute()
1000
    ```
1001

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

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

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

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

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

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

1100
      return result;
39✔
1101
    },
78✔
1102
    R"(
1103
    Compute leakage for the problem.
1104

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

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

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

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

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

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

1150
    Warnings
1151
    --------
1152
       DiscreteOrdinatesCurvilinearProblem is **experimental** and should be used with caution!
1153

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

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

1265
    Wrapper of :cpp:class:`opensn::SteadyStateSourceSolver`.
1266
    )"
1267
  );
704✔
1268
  steady_state_solver.def(
1,408✔
1269
    py::init(
704✔
1270
      [](py::kwargs& params)
365✔
1271
      {
1272
        return SteadyStateSourceSolver::Create(kwargs_to_param_block(params));
365✔
1273
      }
1274
    ),
1275
    R"(
1276
    Construct a steady state solver.
1277

1278
    Parameters
1279
    ----------
1280
    pyopensn.solver.LBSProblem : LBSProblem
1281
        Existing LBSProblem instance.
1282
    )"
1283
  );
1284
  // clang-format on
1285
}
704✔
1286

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

1299
      Wrapper of :cpp:class:`opensn::TransientSolver`.
1300
      )"
1301
    );
704✔
1302
  transient_solver.def(
1,408✔
1303
    py::init(
704✔
1304
      [](py::kwargs& params)
192✔
1305
      {
1306
        return TransientSolver::Create(kwargs_to_param_block(params));
192✔
1307
      }
1308
    ),
1309
    R"(
1310
    Construct a transient solver.
1311

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

1335
    Parameters
1336
    ----------
1337
    dt : float
1338
        New timestep size.
1339
    )");
1340
  transient_solver.def(
704✔
1341
    "SetTheta",
1342
    &TransientSolver::SetTheta,
704✔
1343
    R"(
1344
    Set the theta parameter used by :meth:`Advance`.
1345

1346
    Parameters
1347
    ----------
1348
    theta : float
1349
        Theta value between 1.0e-16 and 1.
1350
    )");
1351
  transient_solver.def(
704✔
1352
    "Advance",
1353
    &TransientSolver::Advance,
704✔
1354
    R"(
1355
    Advance the solver by a single timestep.
1356

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

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

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

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

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

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

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

1482
    Wrapper of :cpp:class:`opensn::PowerIterationKEigenSolver`.
1483
    )"
1484
  );
704✔
1485
  pi_k_eigen_solver.def(
1,408✔
1486
    py::init(
704✔
1487
      [](py::kwargs& params)
175✔
1488
      {
1489
        return PowerIterationKEigenSolver::Create(kwargs_to_param_block(params));
175✔
1490
      }
1491
    ),
1492
    R"(
1493
    Construct a power iteration k-eigen solver.
1494

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

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

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

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

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

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

1603
    Warnings
1604
    --------
1605
       SMM acceleration is **experimental** and should be used with caution!
1606
       SMM accleration only supports problems with isotropic scattering.
1607

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

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

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