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

Open-Sn / opensn / 21773869314

06 Feb 2026 07:55PM UTC coverage: 74.829% (+0.3%) from 74.553%
21773869314

push

github

web-flow
Merge pull request #919 from wdhawkins/rz

Numerous fixes to RZ solver

190 of 230 new or added lines in 8 files covered. (82.61%)

2 existing lines in 1 file now uncovered.

19579 of 26165 relevant lines covered (74.83%)

69416660.18 hits per line

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

83.38
/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/solvers/time_dependent_solver.h"
14
#include "modules/linear_boltzmann_solvers/solvers/transient_solver.h"
15
#include "modules/linear_boltzmann_solvers/solvers/steady_state_solver.h"
16
#include "modules/linear_boltzmann_solvers/solvers/nl_keigen_solver.h"
17
#include "modules/linear_boltzmann_solvers/solvers/pi_keigen_solver.h"
18
#include "modules/linear_boltzmann_solvers/lbs_problem/io/lbs_problem_io.h"
19
#include "modules/linear_boltzmann_solvers/lbs_problem/lbs_problem.h"
20
#include "modules/linear_boltzmann_solvers/lbs_problem/lbs_compute.h"
21
#include "modules/solver.h"
22
#include <pybind11/numpy.h>
23
#include <algorithm>
24
#include <cstddef>
25
#include <cstdint>
26
#include <map>
27
#include <memory>
28
#include <string>
29
#include <vector>
30

31
namespace opensn
32
{
33

34
// Wrap problem
35
void
36
WrapProblem(py::module& slv)
663✔
37
{
38
  // clang-format off
39
  // problem base
40
  auto problem = py::class_<Problem, std::shared_ptr<Problem>>(
663✔
41
    slv,
42
    "Problem",
43
    R"(
44
    Base class for all problems.
45

46
    Wrapper of :cpp:class:`opensn::Problem`.
47
    )"
48
  );
663✔
49
  problem.def(
663✔
50
    "GetFieldFunctions",
51
    [](Problem& self)
663✔
52
    {
53
      py::list ff_list;
×
54
      std::vector<std::shared_ptr<FieldFunctionGridBased>>& cpp_ff_list = self.GetFieldFunctions();
×
55
      for (std::shared_ptr<FieldFunctionGridBased>& ff : cpp_ff_list) {
×
56
        ff_list.append(ff);
×
57
      }
58
      return ff_list;
×
59
    },
×
60
    R"(
61
    Get the list of field functions.
62

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

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

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

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

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

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

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

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

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

201
    Parameters
202
    ----------
203
    max_mpi_message_size: int default=32768
204
        The maximum MPI message size used during sweeps.
205
    restart_writes_enabled: bool, default=False
206
        Flag that controls writing of restart dumps.
207
    write_delayed_psi_to_restart: bool, default=True
208
        Flag that controls writing of delayed angular fluxes to restarts.
209
    read_restart_path: str, default=''
210
        Full path for reading restart dumps including file basename.
211
    write_restart_path: str, default=''
212
        Full path for writing restart dumps including file basename.
213
    write_restart_time_interval: int, default=0
214
        Time interval in seconds at which restart data is to be written.
215
    use_precursors: bool, default=False
216
        Flag for using delayed neutron precursors.
217
    use_source_moments: bool, default=False
218
        Flag for ignoring fixed sources and selectively using source moments obtained elsewhere.
219
    save_angular_flux: bool, default=False
220
        Flag indicating whether angular fluxes are to be stored or not.
221
    verbose_inner_iterations: bool, default=True
222
        Flag to control verbosity of inner iterations.
223
    verbose_outer_iterations: bool, default=True
224
        Flag to control verbosity of across-groupset iterations.
225
    max_ags_iterations: int, default=100
226
        Maximum number of across-groupset iterations.
227
    ags_tolerance: float, default=1.0e-6
228
        Across-groupset iterations tolerance.
229
    ags_convergence_check: {'l2', 'pointwise'}, default='l2'
230
        Type of convergence check for AGS iterations.
231
    verbose_ags_iterations: bool, default=True
232
        Flag to control verbosity of across-groupset iterations.
233
    power_field_function_on: bool, default=False
234
        Flag to control the creation of the power generation field function. If set to ``True``, a
235
        field function will be created with the general name ``<solver_name>_power_generation``.
236
    power_default_kappa: float, default=3.20435e-11
237
        Default ``kappa`` value (Energy released per fission) to use for power generation when cross
238
        sections do not have ``kappa`` values. Default corresponds to 200 MeV per fission.
239
    power_normalization: float, default=-1.0
240
        Power normalization factor to use. Supply a negative or zero number to turn this off.
241
    field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
242
        Prefix option on field function names. If unset, flux field functions will be exported as
243
        ``phi_gXXX_mYYY``, where ``XXX`` is the zero-padded 3-digit group number and ``YYY`` is the
244
        zero-padded 3-digit moment.
245
    field_function_prefix: str, default=''
246
        Prefix to use on all field functions. By default, this is empty. If specified, flux moments
247
        are exported as ``prefix_phi_gXXX_mYYY``.
248
    )"
249
  );
250
  lbs_problem.def(
663✔
251
    "ComputeFissionRate",
252
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
663✔
253
    {
254
      const std::vector<double>* phi_ptr = nullptr;
×
255
      if (scalar_flux_iterate == "old")
×
256
      {
257
        phi_ptr = &self.GetPhiOldLocal();
×
258
      }
259
      else if (scalar_flux_iterate == "new")
×
260
      {
261
        phi_ptr = &self.GetPhiNewLocal();
×
262
      }
263
      else
264
      {
265
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
266
      }
267
      return ComputeFissionRate(self, *phi_ptr);
×
268
    },
269
    R"(
270
    Computes the total fission rate.
271

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

279
    Returns
280
    -------
281
    float
282
        The total fission rate.
283

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

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

320
    Returns
321
    -------
322
    float
323
        The total fission production.
324

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

341
    Returns
342
    -------
343
    memoryview
344
        Memory view of the local old scalar flux vector.
345
    )"
346
  );
347
  lbs_problem.def(
663✔
348
    "GetPhiNewLocal",
349
    [](LBSProblem& self)
711✔
350
    {
351
      return convert_vector(self.GetPhiNewLocal());
48✔
352
    },
353
    R"(
354
    Get the current scalar flux iterate (local vector).
355

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

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

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

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

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

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

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

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

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

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

583
    Parameters
584
    ----------
585
    xs_map: List[Dict]
586
        A list of block-id to cross-section mapping dictionaries. Each dictionary supports:
587
          - block_ids: List[int] (required)
588
              Mesh block ids to associate with the cross section.
589
          - xs: pyopensn.xs.MultiGroupXS (required)
590
              Cross section object.
591
    )"
592
  );
593
  lbs_problem.def(
663✔
594
    "SetAdjoint",
595
    [](LBSProblem& self, bool adjoint)
×
596
    {
597
      self.SetAdjoint(adjoint);
12✔
598
    }
599
  );
600

601
  // discrete ordinate solver
602
  auto do_problem = py::class_<DiscreteOrdinatesProblem, std::shared_ptr<DiscreteOrdinatesProblem>,
663✔
603
                               LBSProblem>(
604
    slv,
605
    "DiscreteOrdinatesProblem",
606
    R"(
607
    Base class for discrete ordinates problems in Cartesian geometry.
608

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

613
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesProblem`.
614
    )"
615
  );
663✔
616
  do_problem.def(
1,326✔
617
    py::init(
663✔
618
      [](py::kwargs& params)
486✔
619
      {
620
        return DiscreteOrdinatesProblem::Create(kwargs_to_param_block(params));
486✔
621
      }
622
    ),
623
    R"(
624
    Construct a discrete ordinates problem with Cartesian geometry.
625

626
    Parameters
627
    ----------
628
    mesh : MeshContinuum
629
        The spatial mesh.
630
    num_groups : int
631
        The total number of energy groups.
632
    groupsets : List[Dict], default=[]
633
        A list of input parameter blocks, each block provides the iterative properties for a
634
        groupset. Each dictionary supports:
635
          - groups_from_to: List[int] (required)
636
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
637
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
638
              Handle to an angular quadrature.
639
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
640
              Angle aggregation method to use during sweeping.
641
          - angle_aggregation_num_subsets: int, default=1
642
              Number of angle subsets used for aggregation.
643
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
644
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
645
              Iterative method used for inner linear solves.
646
          - l_abs_tol: float, default=1.0e-6
647
              Inner linear solver absolute residual tolerance.
648
          - l_max_its: int, default=200
649
              Inner linear solver maximum iterations.
650
          - gmres_restart_interval: int, default=30
651
              GMRES restart interval, if GMRES is used.
652
          - allow_cycles: bool, default=True
653
              Whether cyclic dependencies are allowed in sweeps.
654
          - apply_wgdsa: bool, default=False
655
              Enable within-group DSA for this groupset.
656
          - wgdsa_l_abs_tol: float, default=1.0e-4
657
              WGDSA linear absolute tolerance.
658
          - wgdsa_l_max_its: int, default=30
659
              WGDSA maximum iterations.
660
          - wgdsa_verbose: bool, default=False
661
              Verbose WGDSA output.
662
          - wgdsa_petsc_options: str, default=''
663
              PETSc options string for the WGDSA solver.
664
          - apply_tgdsa: bool, default=False
665
              Enable two-grid DSA for this groupset.
666
          - tgdsa_l_abs_tol: float, default=1.0e-4
667
              TGDSA linear absolute tolerance.
668
          - tgdsa_l_max_its: int, default=30
669
              TGDSA maximum iterations.
670
          - tgdsa_verbose: bool, default=False
671
              Verbose TGDSA output.
672
          - tgdsa_petsc_options: str, default=''
673
              PETSc options string for the TGDSA solver.
674
    xs_map : List[Dict], default=[]
675
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
676
          - block_ids: List[int] (required)
677
              Mesh block IDs to associate with the cross section.
678
          - xs: pyopensn.xs.MultiGroupXS (required)
679
              Cross-section object to assign to the specified blocks.
680
    boundary_conditions: List[Dict], default=[]
681
        A list containing tables for each boundary specification. Each dictionary supports:
682
          - name: str (required)
683
              Boundary name that identifies the specific boundary.
684
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
685
              Boundary type specification.
686
          - group_strength: List[float], optional
687
              Required when ``type='isotropic'``. Isotropic strength per group.
688
          - function: AngularFluxFunction, optional
689
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
690
    point_sources: List[pyopensn.source.PointSource], default=[]
691
        A list of point sources.
692
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
693
        A list of volumetric sources.
694
    options : Dict, default={}
695
        A block of optional configuration parameters. Each dictionary supports the same keys as
696
        :meth:`LBSProblem.SetOptions`, including:
697
          - max_mpi_message_size: int, default=32768
698
          - restart_writes_enabled: bool, default=False
699
          - write_delayed_psi_to_restart: bool, default=True
700
          - read_restart_path: str, default=''
701
          - write_restart_path: str, default=''
702
          - write_restart_time_interval: int, default=0
703
          - use_precursors: bool, default=False
704
          - use_source_moments: bool, default=False
705
          - save_angular_flux: bool, default=False
706
          - verbose_inner_iterations: bool, default=True
707
          - verbose_outer_iterations: bool, default=True
708
          - max_ags_iterations: int, default=100
709
          - ags_tolerance: float, default=1.0e-6
710
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
711
          - verbose_ags_iterations: bool, default=True
712
          - power_field_function_on: bool, default=False
713
          - power_default_kappa: float, default=3.20435e-11
714
          - power_normalization: float, default=-1.0
715
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
716
          - field_function_prefix: str, default=''
717
    sweep_type : str, default="AAH"
718
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
719
    use_gpus : bool, default=False
720
        A flag specifying whether GPU acceleration is used for the sweep. Currently, only ``AAH`` is
721
        supported.
722
    )"
723
  );
724
  do_problem.def(
663✔
725
    "SetOptions",
726
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
675✔
727
    {
728
      InputParameters input = DiscreteOrdinatesProblem::GetOptionsBlock();
12✔
729
      input.AssignParameters(kwargs_to_param_block(params));
12✔
730
      self.SetOptions(input);
12✔
731
    },
12✔
732
    R"(
733
    Set problem options from a large list of parameters.
734

735
    Parameters
736
    ----------
737
    adjoint: bool, default=False
738
        Flag for toggling whether the solver is in adjoint mode.
739
    )"
740
  );
741
  do_problem.def(
663✔
742
    "SetBoundaryOptions",
743
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
663✔
744
    {
745
      for (auto [key, value] : params)
×
746
      {
747
        auto c_key = key.cast<std::string>();
×
748
        if (c_key == "clear_boundary_conditions")
×
749
          self.ClearBoundaries();
×
750
        else if (c_key == "boundary_conditions")
×
751
        {
752
          auto boundaries = value.cast<py::list>();
×
753
          for (auto boundary : boundaries)
×
754
          {
755
            InputParameters input = DiscreteOrdinatesProblem::GetBoundaryOptionsBlock();
×
756
            input.AssignParameters(pyobj_to_param_block("", boundary.cast<py::dict>()));
×
757
            self.SetBoundaryOptions(input);
×
758
          }
×
759
        }
×
760
        else
761
          throw std::runtime_error("Invalid argument provided to SetBoundaryOptions.\n");
×
762
      }
×
763
    },
×
764
    R"(
765
    Set or clear boundary conditions.
766

767
    Parameters
768
    ----------
769
    clear_boundary_conditions: bool, default=False
770
        If true, all current boundary conditions are deleted.
771
    boundary_conditions: List[Dict]
772
        A list of boundary condition dictionaries. Each dictionary supports:
773
          - name: str (required)
774
              Boundary name that identifies the specific boundary.
775
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
776
              Boundary type specification.
777
          - group_strength: List[float], optional
778
              Required when ``type='isotropic'``. Isotropic strength per group.
779
          - function: AngularFluxFunction, optional
780
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
781
    )"
782
  );
783
  do_problem.def(
663✔
784
    "GetPsi",
785
    [](DiscreteOrdinatesProblem& self)
671✔
786
    {
787
      const auto& psi = self.GetPsiNewLocal();
8✔
788
      py::list psi_list;
8✔
789
      for (const auto& vec : psi)
16✔
790
      {
791
        auto array = py::array_t<double>(static_cast<py::ssize_t>(vec.size()),
8✔
792
                                         vec.data(),
793
                                         py::cast(self));
8✔
794
        psi_list.append(array);
8✔
795
      }
8✔
796
      return psi_list;
8✔
797
    },
×
798
    R"(
799
    Return psi as a list of NumPy arrays (float64), using zero-copy views into the
800
    underlying data.
801
    )"
802
  );
803
  do_problem.def(
663✔
804
    "ComputeLeakage",
805
    [](DiscreteOrdinatesProblem& self, py::list bnd_names)
702✔
806
    {
807
      auto grid = self.GetGrid();
39✔
808
      // get the supported boundaries
809
      std::map<std::string, std::uint64_t> allowed_bd_names = grid->GetBoundaryNameMap();
39✔
810
      std::map<std::uint64_t, std::string> allowed_bd_ids = grid->GetBoundaryIDMap();
39✔
811
      const auto coord_sys = grid->GetCoordinateSystem();
39✔
812
      const auto mesh_type = grid->GetType();
39✔
813
      const auto dim = grid->GetDimension();
39✔
814
      // get the boundaries to parse, preserving user order
815
      std::vector<std::uint64_t> bndry_ids;
39✔
816
      if (bnd_names.size() > 1)
39✔
817
      {
818
        for (py::handle name : bnd_names)
184✔
819
        {
820
          auto sname = name.cast<std::string>();
112✔
821
          if (coord_sys == CoordinateSystemType::CYLINDRICAL && dim == 2)
112✔
822
          {
823
            if (sname == "xmin" || sname == "xmax" || sname == "ymin" || sname == "ymax")
24✔
NEW
824
              throw std::runtime_error("ComputeLeakage: Boundary name '" + sname +
×
825
                                       "' is invalid for cylindrical orthogonal meshes. "
NEW
826
                                       "Use rmin, rmax, zmin, zmax.");
×
827

828
            if (mesh_type == MeshType::ORTHOGONAL)
24✔
829
            {
830
              if (sname == "rmin") sname = "xmin";
24✔
831
              else if (sname == "rmax") sname = "xmax";
24✔
832
              else if (sname == "zmin") sname = "ymin";
16✔
833
              else if (sname == "zmax") sname = "ymax";
8✔
834
            }
835
          }
836
          bndry_ids.push_back(allowed_bd_names.at(sname));
112✔
837
        }
112✔
838
      }
839
      else
840
      {
841
        bndry_ids = self.GetGrid()->GetUniqueBoundaryIDs();
9✔
842
      }
843
      // compute the leakage
844
      std::map<std::uint64_t, std::vector<double>> leakage = ComputeLeakage(self, bndry_ids);
39✔
845
      const bool rz_ortho = (coord_sys == CoordinateSystemType::CYLINDRICAL &&
78✔
846
                             mesh_type == MeshType::ORTHOGONAL && dim == 2);
39✔
847

848
      auto to_rz_name = [](const std::string& name)
63✔
849
      {
850
        if (name == "xmin") return std::string("rmin");
24✔
851
        if (name == "xmax") return std::string("rmax");
24✔
852
        if (name == "ymin") return std::string("zmin");
16✔
853
        if (name == "ymax") return std::string("zmax");
8✔
NEW
854
        return name;
×
855
      };
856

857
      // convert result to native Python
858
      py::dict result;
39✔
859
      for (const auto& bndry_id : bndry_ids)
157✔
860
      {
861
        const auto it = leakage.find(bndry_id);
118✔
862
        if (it == leakage.end())
118✔
863
          continue;
×
864
        // construct numpy array and copy contents
865
        const auto& grp_wise_leakage = it->second;
118✔
866
        py::array_t<double> np_vector(py::ssize_t(grp_wise_leakage.size()));
118✔
867
        auto buffer = np_vector.request();
118✔
868
        auto *np_vector_data = static_cast<double*>(buffer.ptr);
118✔
869
        std::copy(grp_wise_leakage.begin(), grp_wise_leakage.end(), np_vector_data);
118✔
870
        std::string name = allowed_bd_ids.at(bndry_id);
118✔
871
        if (rz_ortho)
118✔
872
          name = to_rz_name(name);
24✔
873
        result[py::str(name)] = std::move(np_vector);
236✔
874
      }
118✔
875

876
      return result;
39✔
877
    },
78✔
878
    R"(
879
    Compute leakage for the problem.
880

881
    Parameters
882
    ----------
883
    bnd_names : List[str]
884
        A list of boundary names for which leakage should be computed.
885

886
    Returns
887
    -------
888
    Dict[str, numpy.ndarray]
889
        A dictionary mapping boundary names to group-wise leakage vectors.
890
        Each array contains the outgoing angular flux (per group) integrated over
891
        the corresponding boundary surface.
892

893
    Raises
894
    ------
895
    RuntimeError
896
        If `save_angular_flux` option was not enabled during problem setup.
897

898
    ValueError
899
        If one or more boundary ids are not present on the current mesh.
900
    )",
901
    py::arg("bnd_names")
663✔
902
  );
903

904
  // discrete ordinates curvilinear problem
905
  auto do_curvilinear_problem = py::class_<DiscreteOrdinatesCurvilinearProblem,
663✔
906
                                           std::shared_ptr<DiscreteOrdinatesCurvilinearProblem>,
907
                                           DiscreteOrdinatesProblem>(
908
    slv,
909
    "DiscreteOrdinatesCurvilinearProblem",
910
    R"(
911
    Base class for discrete ordinates problems in curvilinear geometry.
912

913
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesCurvilinearProblem`.
914
    )"
915
  );
663✔
916
  do_curvilinear_problem.def(
1,326✔
917
    py::init(
663✔
918
      [](py::kwargs& params)
80✔
919
      {
920
        return DiscreteOrdinatesCurvilinearProblem::Create(kwargs_to_param_block(params));
80✔
921
      }
922
    ),
923
    R"(
924
    Construct a discrete ordinates problem for curvilinear geometry.
925

926
    Warnings
927
    --------
928
       DiscreteOrdinatesCurvilinearProblem is **experimental** and should be used with caution!
929

930
    Parameters
931
    ----------
932
    mesh : MeshContinuum
933
        The spatial mesh.
934
    coord_system : int
935
        Coordinate system to use. Must be set to 2 (cylindrical coordinates).
936
    num_groups : int
937
        The total number of energy groups.
938
    groupsets : list of dict
939
        A list of input parameter blocks, each block provides the iterative properties for a
940
        groupset. Each dictionary supports:
941
          - groups_from_to: List[int] (required)
942
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
943
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
944
              Handle to an angular quadrature.
945
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
946
              Angle aggregation method to use during sweeping.
947
          - angle_aggregation_num_subsets: int, default=1
948
              Number of angle subsets used for aggregation.
949
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
950
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
951
              Iterative method used for inner linear solves.
952
          - l_abs_tol: float, default=1.0e-6
953
              Inner linear solver absolute residual tolerance.
954
          - l_max_its: int, default=200
955
              Inner linear solver maximum iterations.
956
          - gmres_restart_interval: int, default=30
957
              GMRES restart interval, if GMRES is used.
958
          - allow_cycles: bool, default=True
959
              Whether cyclic dependencies are allowed in sweeps.
960
          - apply_wgdsa: bool, default=False
961
              Enable within-group DSA for this groupset.
962
          - wgdsa_l_abs_tol: float, default=1.0e-4
963
              WGDSA linear absolute tolerance.
964
          - wgdsa_l_max_its: int, default=30
965
              WGDSA maximum iterations.
966
          - wgdsa_verbose: bool, default=False
967
              Verbose WGDSA output.
968
          - wgdsa_petsc_options: str, default=''
969
              PETSc options string for the WGDSA solver.
970
          - apply_tgdsa: bool, default=False
971
              Enable two-grid DSA for this groupset.
972
          - tgdsa_l_abs_tol: float, default=1.0e-4
973
              TGDSA linear absolute tolerance.
974
          - tgdsa_l_max_its: int, default=30
975
              TGDSA maximum iterations.
976
          - tgdsa_verbose: bool, default=False
977
              Verbose TGDSA output.
978
          - tgdsa_petsc_options: str, default=''
979
    xs_map : list of dict
980
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
981
          - block_ids: List[int] (required)
982
              Mesh block IDs to associate with the cross section.
983
          - xs: pyopensn.xs.MultiGroupXS (required)
984
              Cross-section object to assign to the specified blocks.
985
    boundary_conditions: List[Dict], default=[]
986
        A list containing tables for each boundary specification. Each dictionary supports:
987
          - name: str (required)
988
              Boundary name that identifies the specific boundary.
989
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
990
              Boundary type specification.
991
          - group_strength: List[float], optional
992
              Required when ``type='isotropic'``. Isotropic strength per group.
993
          - function: AngularFluxFunction, optional
994
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
995
    point_sources: List[pyopensn.source.PointSource], default=[]
996
        A list of point sources.
997
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
998
        A list of volumetric sources.
999
    options : dict, optional
1000
        A block of optional configuration parameters. Each dictionary supports the same keys as
1001
        :meth:`LBSProblem.SetOptions`, including:
1002
          - max_mpi_message_size: int, default=32768
1003
          - restart_writes_enabled: bool, default=False
1004
          - write_delayed_psi_to_restart: bool, default=True
1005
          - read_restart_path: str, default=''
1006
          - write_restart_path: str, default=''
1007
          - write_restart_time_interval: int, default=0
1008
          - use_precursors: bool, default=False
1009
          - use_source_moments: bool, default=False
1010
          - save_angular_flux: bool, default=False
1011
          - verbose_inner_iterations: bool, default=True
1012
          - verbose_outer_iterations: bool, default=True
1013
          - max_ags_iterations: int, default=100
1014
          - ags_tolerance: float, default=1.0e-6
1015
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
1016
          - verbose_ags_iterations: bool, default=True
1017
          - power_field_function_on: bool, default=False
1018
          - power_default_kappa: float, default=3.20435e-11
1019
          - power_normalization: float, default=-1.0
1020
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
1021
          - field_function_prefix: str, default=''
1022
    sweep_type : str, optional
1023
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
1024
    )"
1025
  );
1026
}
663✔
1027

1028
// Wrap steady-state solver
1029
void
1030
WrapSteadyState(py::module& slv)
663✔
1031
{
1032
  // clang-format off
1033
  // steady state solver
1034
  auto steady_state_solver = py::class_<SteadyStateSourceSolver, std::shared_ptr<SteadyStateSourceSolver>,
663✔
1035
                                        Solver>(
1036
    slv,
1037
    "SteadyStateSourceSolver",
1038
    R"(
1039
    Steady state solver.
1040

1041
    Wrapper of :cpp:class:`opensn::SteadyStateSourceSolver`.
1042
    )"
1043
  );
663✔
1044
  steady_state_solver.def(
1,326✔
1045
    py::init(
663✔
1046
      [](py::kwargs& params)
345✔
1047
      {
1048
        return SteadyStateSourceSolver::Create(kwargs_to_param_block(params));
345✔
1049
      }
1050
    ),
1051
    R"(
1052
    Construct a steady state solver.
1053

1054
    Parameters
1055
    ----------
1056
    pyopensn.solver.LBSProblem : LBSProblem
1057
        Existing LBSProblem instance.
1058
    )"
1059
  );
1060
  // clang-format on
1061
}
663✔
1062

1063
// Wrap transient solver
1064
void
1065
WrapTransient(py::module& slv)
663✔
1066
{
1067
  // clang-format off
1068
  auto transient_solver =
663✔
1069
    py::class_<TransientSolver, std::shared_ptr<TransientSolver>, Solver>(
1070
      slv,
1071
      "TransientSolver",
1072
      R"(
1073
      Transient solver.
1074

1075
      Wrapper of :cpp:class:`opensn::TransientSolver`.
1076
      )"
1077
    );
663✔
1078
  transient_solver.def(
1,326✔
1079
    py::init(
663✔
1080
      [](py::kwargs& params)
120✔
1081
      {
1082
        return TransientSolver::Create(kwargs_to_param_block(params));
120✔
1083
      }
1084
    ),
1085
    R"(
1086
    Construct a transient solver.
1087

1088
    Parameters
1089
    ----------
1090
    pyopensn.solver.DiscreteOrdinatesProblem : DiscreteOrdinatesProblem
1091
        Existing discrete ordinates problem instance.
1092
    dt : float, optional, default=2.0e-3
1093
        Time step size used during the simulation.
1094
    stop_time : float, optional, default=0.1
1095
        Simulation end time.
1096
    theta : float, optional, default=0.5
1097
        Time differencing scheme parameter.
1098
    initial_state : str, optional, default="existing"
1099
        Initial state for the transient solve. Allowed values: existing, zero.
1100
        In "zero" mode, the solver may initialize the problem internally if needed.
1101
    verbose : bool, optional, default=True
1102
        Enable verbose logging.
1103
    )"
1104
  );
1105
  transient_solver.def(
663✔
1106
    "SetTimeStep",
1107
    &TransientSolver::SetTimeStep,
663✔
1108
    R"(
1109
    Set the timestep size used by :meth:`Advance`.
1110

1111
    Parameters
1112
    ----------
1113
    dt : float
1114
        New timestep size.
1115
    )");
1116
  transient_solver.def(
663✔
1117
    "SetTheta",
1118
    &TransientSolver::SetTheta,
663✔
1119
    R"(
1120
    Set the theta parameter used by :meth:`Advance`.
1121

1122
    Parameters
1123
    ----------
1124
    theta : float
1125
        Theta value between 1.0e-16 and 1.
1126
    )");
1127
  transient_solver.def(
663✔
1128
    "Advance",
1129
    &TransientSolver::Advance,
663✔
1130
    R"(
1131
    Advance the solver by a single timestep.
1132

1133
    Notes
1134
    -----
1135
    You must call :meth:`Initialize` before calling :meth:`Advance` or
1136
    :meth:`Execute`.
1137
    )");
1138
  transient_solver.def(
663✔
1139
    "SetPreAdvanceCallback",
1140
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
663✔
1141
      &TransientSolver::SetPreAdvanceCallback),
1142
    R"(
1143
    Register a callback that runs before each advance within :meth:`Execute`.
1144

1145
    Parameters
1146
    ----------
1147
    callback : Optional[Callable[[], None]]
1148
        Function invoked before the solver advances a timestep. Pass None to clear.
1149
        If the callback modifies the timestep, the new value is used for the
1150
        upcoming step.
1151
    )");
1152
  transient_solver.def(
663✔
1153
    "SetPreAdvanceCallback",
1154
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
663✔
1155
      &TransientSolver::SetPreAdvanceCallback),
1156
    "Clear the PreAdvance callback by passing None.");
1157
  transient_solver.def(
663✔
1158
    "SetPostAdvanceCallback",
1159
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
663✔
1160
      &TransientSolver::SetPostAdvanceCallback),
1161
    R"(
1162
    Register a callback that runs after each advance within :meth:`Execute`.
1163

1164
    Parameters
1165
    ----------
1166
    callback : Optional[Callable[[], None]]
1167
        Function invoked after the solver advances a timestep. Pass None to clear.
1168
    )");
1169
  transient_solver.def(
663✔
1170
    "SetPostAdvanceCallback",
1171
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
663✔
1172
      &TransientSolver::SetPostAdvanceCallback),
1173
    "Clear the PostAdvance callback by passing None.");
1174
  slv.attr("BackwardEuler") = 1.0;
663✔
1175
  slv.attr("CrankNicolson") = 0.5;
1,326✔
1176
  // clang-format on
1177
}
663✔
1178

1179
// Wrap time-dependent solver
1180
void
1181
WrapTimeDependent(py::module& slv)
663✔
1182
{
1183
  // clang-format off
1184
  auto time_dependent_solver =
663✔
1185
    py::class_<TimeDependentSourceSolver, std::shared_ptr<TimeDependentSourceSolver>, Solver>(
1186
      slv,
1187
      "TimeDependentSourceSolver",
1188
      R"(
1189
      Time dependent solver.
1190

1191
      Wrapper of :cpp:class:`opensn::TimeDependentSourceSolver`.
1192
      )"
1193
    );
663✔
1194
  time_dependent_solver.def(
1,326✔
1195
    py::init(
663✔
1196
      [](py::kwargs& params)
44✔
1197
      {
1198
        return TimeDependentSourceSolver::Create(kwargs_to_param_block(params));
44✔
1199
      }
1200
    ),
1201
    R"(
1202
    Construct a time dependent solver.
1203

1204
    Parameters
1205
    ----------
1206
    pyopensn.solver.LBSProblem : LBSProblem
1207
        Existing LBSProblem instance.
1208
    dt : float, optional, default=1.0
1209
        Time step size used during the simulation.
1210
    stop_time : float, optional, default=1.0
1211
        Simulation end time.
1212
    )"
1213
  );
1214
  time_dependent_solver.def(
663✔
1215
    "Advance",
1216
    &TimeDependentSourceSolver::Advance,
663✔
1217
    R"(
1218
    Advance the solver by a single timestep.
1219

1220
    This method uses the configured `dt` and `theta` values and will return
1221
    immediately if the stop time has already been reached. Calling it
1222
    repeatedly allows users to write custom python time loops.
1223
    )");
1224
  time_dependent_solver.def(
663✔
1225
    "SetTimeStep",
1226
    &TimeDependentSourceSolver::SetTimeStep,
663✔
1227
    R"(
1228
    Set the timestep size used by :meth:`Advance`.
1229

1230
    Parameters
1231
    ----------
1232
    dt : float
1233
        New timestep size.
1234
    )");
1235
  time_dependent_solver.def(
663✔
1236
    "SetTheta",
1237
    &TimeDependentSourceSolver::SetTheta,
663✔
1238
    R"(
1239
    Set the theta parameter used by :meth:`Advance`.
1240

1241
    Parameters
1242
    ----------
1243
    theta : float
1244
        Theta value between 0 and 1.
1245
    )");
1246
  time_dependent_solver.def(
663✔
1247
    "SetPreAdvanceCallback",
1248
    static_cast<void (TimeDependentSourceSolver::*)(std::function<void()>)>(
663✔
1249
      &TimeDependentSourceSolver::SetPreAdvanceCallback),
1250
    R"(
1251
    Register a callback that runs before each call to :meth:`Advance`.
1252

1253
    Parameters
1254
    ----------
1255
    callback : Optional[Callable[[], None]]
1256
        Function invoked before the solver advances a timestep. Pass None to clear.
1257
        If the callback modifies the timestep, the new value is used for the
1258
        upcoming step.
1259
    )");
1260
  time_dependent_solver.def(
663✔
1261
    "SetPreAdvanceCallback",
1262
    static_cast<void (TimeDependentSourceSolver::*)(std::nullptr_t)>(
663✔
1263
      &TimeDependentSourceSolver::SetPreAdvanceCallback),
1264
    "Clear the PreAdvance callback by passing None.");
1265
  time_dependent_solver.def(
663✔
1266
    "SetPostAdvanceCallback",
1267
    static_cast<void (TimeDependentSourceSolver::*)(std::function<void()>)>(
663✔
1268
      &TimeDependentSourceSolver::SetPostAdvanceCallback),
1269
    R"(
1270
    Register a callback that runs after each call to :meth:`Advance`.
1271

1272
    Parameters
1273
    ----------
1274
    callback : Optional[Callable[[], None]]
1275
        Function invoked after the solver advances a timestep. Pass None to clear.
1276
    )");
1277
  time_dependent_solver.def(
663✔
1278
    "SetPostAdvanceCallback",
1279
    static_cast<void (TimeDependentSourceSolver::*)(std::nullptr_t)>(
663✔
1280
      &TimeDependentSourceSolver::SetPostAdvanceCallback),
1281
    "Clear the PostAdvance callback by passing None.");
1282
  slv.attr("BackwardEuler") = 1.0;
663✔
1283
  slv.attr("CrankNicolson") = 0.5;
1,326✔
1284
  // clang-format on
1285
}
663✔
1286

1287
// Wrap non-linear k-eigen solver
1288
void
1289
WrapNLKEigen(py::module& slv)
663✔
1290
{
1291
  // clang-format off
1292
  // non-linear k-eigen solver
1293
  auto non_linear_k_eigen_solver = py::class_<NonLinearKEigenSolver, std::shared_ptr<NonLinearKEigenSolver>,
663✔
1294
                                              Solver>(
1295
    slv,
1296
    "NonLinearKEigenSolver",
1297
    R"(
1298
    Non-linear k-eigenvalue solver.
1299

1300
    Wrapper of :cpp:class:`opensn::NonLinearKEigenSolver`.
1301
    )"
1302
  );
663✔
1303
  non_linear_k_eigen_solver.def(
1,326✔
1304
    py::init(
663✔
1305
      [](py::kwargs& params)
28✔
1306
      {
1307
        return NonLinearKEigenSolver::Create(kwargs_to_param_block(params));
28✔
1308
      }
1309
        ),
1310
    R"(
1311
    Construct a non-linear k-eigenvalue solver.
1312

1313
    Parameters
1314
    ----------
1315
    lbs_problem: pyopensn.solver.LBSProblem
1316
        Existing LBSProblem instance.
1317
    nl_abs_tol: float, default=1.0e-8
1318
        Non-linear absolute tolerance.
1319
    nl_rel_tol: float, default=1.0e-8
1320
        Non-linear relative tolerance.
1321
    nl_sol_tol: float, default=1.0e-50
1322
        Non-linear solution tolerance.
1323
    nl_max_its: int, default=50
1324
        Non-linear algorithm maximum iterations.
1325
    l_abs_tol: float, default=1.0e-8
1326
        Linear absolute tolerance.
1327
    l_rel_tol: float, default=1.0e-8
1328
        Linear relative tolerance.
1329
    l_div_tol: float, default=1.0e6
1330
        Linear divergence tolerance.
1331
    l_max_its: int, default=50
1332
        Linear algorithm maximum iterations.
1333
    l_gmres_restart_intvl: int, default=30
1334
        GMRES restart interval.
1335
    l_gmres_breakdown_tol: float, default=1.0e6
1336
        GMRES breakdown tolerance.
1337
    reset_phi0: bool, default=True
1338
        If true, reinitializes scalar fluxes to 1.0.
1339
    num_initial_power_iterations: int, default=0
1340
        Number of initial power iterations before the non-linear solve.
1341
    )"
1342
  );
1343
  non_linear_k_eigen_solver.def(
663✔
1344
    "GetEigenvalue",
1345
    &NonLinearKEigenSolver::GetEigenvalue,
663✔
1346
    R"(
1347
    Return the current k‑eigenvalue.
1348
    )"
1349
  );
1350
  // clang-format on
1351
}
663✔
1352

1353
// Wrap power iteration solvers
1354
void
1355
WrapPIteration(py::module& slv)
663✔
1356
{
1357
  // clang-format off
1358
  // power iteration k-eigen solver
1359
  auto pi_k_eigen_solver = py::class_<PowerIterationKEigenSolver, std::shared_ptr<PowerIterationKEigenSolver>,
663✔
1360
                                      Solver>(
1361
    slv,
1362
    "PowerIterationKEigenSolver",
1363
    R"(
1364
    Power iteration k-eigenvalue solver.
1365

1366
    Wrapper of :cpp:class:`opensn::PowerIterationKEigenSolver`.
1367
    )"
1368
  );
663✔
1369
  pi_k_eigen_solver.def(
1,326✔
1370
    py::init(
663✔
1371
      [](py::kwargs& params)
139✔
1372
      {
1373
        return PowerIterationKEigenSolver::Create(kwargs_to_param_block(params));
139✔
1374
      }
1375
    ),
1376
    R"(
1377
    Construct a power iteration k-eigen solver.
1378

1379
    Parameters
1380
    ----------
1381
    problem: pyopensn.solver.LBSProblem
1382
        Existing DiscreteOrdinatesProblem instance.
1383
    acceleration: pyopensn.solver.DiscreteOrdinatesKEigenAcceleration
1384
        Optional DiscreteOrdinatesKEigenAcceleration instance for acceleration.
1385
    max_iters: int, default = 1000
1386
        Maximum power iterations allowed.
1387
    k_tol: float, default = 1.0e-10
1388
        Tolerance on the k-eigenvalue.
1389
    reset_solution: bool, default=True
1390
        If true, initialize flux moments to 1.0.
1391
    reset_phi0: bool, default=True
1392
        If true, reinitializes scalar fluxes to 1.0.
1393
    )"
1394
  );
1395
  pi_k_eigen_solver.def(
663✔
1396
    "GetEigenvalue",
1397
    &PowerIterationKEigenSolver::GetEigenvalue,
663✔
1398
    R"(
1399
    Return the current k‑eigenvalue.
1400
    )"
1401
  );
1402
  // clang-format on
1403
}
663✔
1404

1405
// Wrap LBS solver
1406
void
1407
WrapDiscreteOrdinatesKEigenAcceleration(py::module& slv)
663✔
1408
{
1409
  // clang-format off
1410
  // discrete ordinates k-eigen acceleration base
1411
  auto acceleration = py::class_<DiscreteOrdinatesKEigenAcceleration,
663✔
1412
                                 std::shared_ptr<DiscreteOrdinatesKEigenAcceleration>>(
1413
    slv,
1414
    "DiscreteOrdinatesKEigenAcceleration",
1415
    R"(
1416
    Base class for discrete ordinates k-eigenvalue acceleration methods.
1417

1418
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesKEigenAcceleration`.
1419
    )"
1420
  );
663✔
1421
  // SCDSA acceleration
1422
  auto scdsa_acceleration = py::class_<SCDSAAcceleration,
663✔
1423
                                       std::shared_ptr<SCDSAAcceleration>,
1424
                                       DiscreteOrdinatesKEigenAcceleration>(
1425
    slv,
1426
    "SCDSAAcceleration",
1427
    R"(
1428
    Construct an SCDSA accelerator for the power iteration k-eigenvalue solver.
1429

1430
    Wrapper of :cpp:class:`opensn::SCDSAAcceleration`.
1431
    )"
1432
  );
663✔
1433
  scdsa_acceleration.def(
1,326✔
1434
    py::init(
663✔
1435
      [](py::kwargs& params)
8✔
1436
      {
1437
        return SCDSAAcceleration::Create(kwargs_to_param_block(params));
8✔
1438
      }
1439
    ),
1440
    R"(
1441
    SCDSA acceleration for the power iteration k-eigenvalue solver.
1442

1443
    Parameters
1444
    ----------
1445
    problem: pyopensn.solver.LBSProblem
1446
        Existing DiscreteOrdinatesProblem instance.
1447
    l_abs_tol: float, defauilt=1.0e-10
1448
        Absolute residual tolerance.
1449
    max_iters: int, default=100
1450
        Maximum allowable iterations.
1451
    verbose: bool, default=False
1452
        If true, enables verbose output.
1453
    petsc_options: str, default="ssss"
1454
        Additional PETSc options.
1455
    pi_max_its: int, default=50
1456
        Maximum allowable iterations for inner power iterations.
1457
    pi_k_tol: float, default=1.0e-10
1458
        k-eigenvalue tolerance for the inner power iterations.
1459
    sdm: str, default="pwld"
1460
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1461
            - 'pwld' : Piecewise Linear Discontinuous
1462
            - 'pwlc' : Piecewise Linear Continuous
1463
    )"
1464
  );
1465
  // SMM acceleration
1466
  auto smm_acceleration = py::class_<SMMAcceleration,
663✔
1467
                                     std::shared_ptr<SMMAcceleration>,
1468
                                     DiscreteOrdinatesKEigenAcceleration>(
1469
    slv,
1470
    "SMMAcceleration",
1471
    R"(
1472
    Construct an SMM accelerator for the power iteration k-eigenvalue solver.
1473

1474
    Wrapper of :cpp:class:`opensn::SMMAcceleration`.
1475
    )"
1476
  );
663✔
1477
  smm_acceleration.def(
1,326✔
1478
    py::init(
663✔
1479
      [](py::kwargs& params)
4✔
1480
      {
1481
        return SMMAcceleration::Create(kwargs_to_param_block(params));
4✔
1482
      }
1483
    ),
1484
    R"(
1485
    SMM acceleration for the power iteration k-eigenvalue solver.
1486

1487
    Warnings
1488
    --------
1489
       SMM acceleration is **experimental** and should be used with caution!
1490
       SMM accleration only supports problems with isotropic scattering.
1491

1492
    Parameters
1493
    ----------
1494
    problem: pyopensn.solver.LBSProblem
1495
        Existing DiscreteOrdinatesProblem instance.
1496
    l_abs_tol: float, defauilt=1.0e-10
1497
        Absolute residual tolerance.
1498
    max_iters: int, default=100
1499
        Maximum allowable iterations.
1500
    verbose: bool, default=False
1501
        If true, enables verbose output.
1502
    petsc_options: str, default="ssss"
1503
        Additional PETSc options.
1504
    pi_max_its: int, default=50
1505
        Maximum allowable iterations for inner power iterations.
1506
    pi_k_tol: float, default=1.0e-10
1507
        k-eigenvalue tolerance for the inner power iterations.
1508
    sdm: str, default="pwld"
1509
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1510
            - 'pwld' : Piecewise Linear Discontinuous
1511
            - 'pwlc' : Piecewise Linear Continuous
1512
    )"
1513
  );
1514
  // clang-format on
1515
}
663✔
1516

1517
// Wrap the solver components of OpenSn
1518
void
1519
py_solver(py::module& pyopensn)
63✔
1520
{
1521
  py::module slv = pyopensn.def_submodule("solver", "Solver module.");
63✔
1522
  WrapProblem(slv);
63✔
1523
  WrapSolver(slv);
63✔
1524
  WrapLBS(slv);
63✔
1525
  WrapSteadyState(slv);
63✔
1526
  WrapTransient(slv);
63✔
1527
  WrapTimeDependent(slv);
63✔
1528
  WrapNLKEigen(slv);
63✔
1529
  WrapDiscreteOrdinatesKEigenAcceleration(slv);
63✔
1530
  WrapPIteration(slv);
63✔
1531
}
63✔
1532

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