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

Open-Sn / opensn / 20219489296

12 Dec 2025 06:55PM UTC coverage: 74.333%. Remained the same
20219489296

push

github

web-flow
Merge pull request #859 from wdhawkins/td_source_driver

Adding time-dependent solver and time-dependent sources

367 of 398 new or added lines in 23 files covered. (92.21%)

359 existing lines in 17 files now uncovered.

18610 of 25036 relevant lines covered (74.33%)

68904591.22 hits per line

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

79.5
/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 "framework/runtime.h"
6
#include "framework/field_functions/field_function_grid_based.h"
7
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/acceleration/discrete_ordinates_keigen_acceleration.h"
8
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/acceleration/scdsa_acceleration.h"
9
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/acceleration/smm_acceleration.h"
10
#include "modules/linear_boltzmann_solvers/discrete_ordinates_curvilinear_problem/discrete_ordinates_curvilinear_problem.h"
11
#include "modules/linear_boltzmann_solvers/discrete_ordinates_problem/discrete_ordinates_problem.h"
12
#include "modules/linear_boltzmann_solvers/solvers/time_dependent_solver.h"
13
#include "modules/linear_boltzmann_solvers/solvers/steady_state_solver.h"
14
#include "modules/linear_boltzmann_solvers/solvers/nl_keigen_solver.h"
15
#include "modules/linear_boltzmann_solvers/solvers/pi_keigen_solver.h"
16
#include "modules/linear_boltzmann_solvers/lbs_problem/io/lbs_problem_io.h"
17
#include "modules/linear_boltzmann_solvers/lbs_problem/lbs_problem.h"
18
#include "modules/linear_boltzmann_solvers/lbs_problem/lbs_compute.h"
19
#include "modules/solver.h"
20
#include <pybind11/numpy.h>
21
#include <algorithm>
22
#include <cstddef>
23
#include <cstdint>
24
#include <map>
25
#include <memory>
26
#include <string>
27
#include <vector>
28

29
namespace opensn
30
{
31

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

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

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

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

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

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

115
    Wrapper of :cpp:class:`opensn::LBSProblem`.
116
    )"
117
  );
479✔
118
  lbs_problem.def(
479✔
119
    "GetScalarFieldFunctionList",
120
    [](LBSProblem& self, bool only_scalar_flux)
479✔
121
    {
122
      py::list field_function_list_per_group;
243✔
123
      for (std::size_t group = 0; group < self.GetNumGroups(); group++)
14,823✔
124
      {
125
        if (only_scalar_flux)
14,580✔
126
        {
127
          std::size_t ff_index = self.MapPhiFieldFunction(group, 0);
8,912✔
128
          field_function_list_per_group.append(self.GetFieldFunctions()[ff_index]);
8,912✔
129
        }
130
        else
131
        {
132
          py::list field_function_list_per_moment;
5,668✔
133
          for (std::size_t moment = 0; moment < self.GetNumMoments(); moment++)
22,256✔
134
          {
135
            std::size_t ff_index = self.MapPhiFieldFunction(group, moment);
16,588✔
136
            field_function_list_per_moment.append(self.GetFieldFunctions()[ff_index]);
16,588✔
137
          }
138
          field_function_list_per_group.append(field_function_list_per_moment);
5,668✔
139
        }
5,668✔
140
      }
141
      return field_function_list_per_group;
243✔
UNCOV
142
    },
×
143
    R"(
144
    Return field functions grouped by energy group and, optionally, by moment.
145

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

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

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

161
    Notes
162
    -----
163
    The moment index varies more rapidly than the group index when `only_scalar_flux` is False.
164
    )",
165
    py::arg("only_scalar_flux") = true
479✔
166
  );
167
  lbs_problem.def(
479✔
168
    "GetPowerFieldFunction",
169
    &LBSProblem::GetPowerFieldFunction,
479✔
170
    R"(
171
    Returns the power generation field function, if enabled.
172
    )"
173
  );
174
  lbs_problem.def(
479✔
175
    "SetOptions",
176
    [](LBSProblem& self, py::kwargs& params)
479✔
177
    {
UNCOV
178
      InputParameters input = LBSProblem::GetOptionsBlock();
×
UNCOV
179
      input.AssignParameters(kwargs_to_param_block(params));
×
UNCOV
180
      self.SetOptions(input);
×
UNCOV
181
    },
×
182
    R"(
183
    Set problem options from a large list of parameters.
184

185
    Parameters
186
    ----------
187
    max_mpi_message_size: int default=32768
188
        The maximum MPI message size used during sweeps.
189
    restart_writes_enabled: bool, default=False
190
        Flag that controls writing of restart dumps.
191
    write_delayed_psi_to_restart: bool, default=True
192
        Flag that controls writing of delayed angular fluxes to restarts.
193
    read_restart_path: str, default=''
194
        Full path for reading restart dumps including file basename.
195
    write_restart_path: str, default=''
196
        Full path for writing restart dumps including file basename.
197
    write_restart_time_interval: int, default=0
198
        Time interval in seconds at which restart data is to be written.
199
    use_precursors: bool, default=False
200
        Flag for using delayed neutron precursors.
201
    use_source_moments: bool, default=False
202
        Flag for ignoring fixed sources and selectively using source moments obtained elsewhere.
203
    save_angular_flux: bool, default=False
204
        Flag indicating whether angular fluxes are to be stored or not.
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.
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
  );
234
  lbs_problem.def(
479✔
235
    "ComputeFissionRate",
236
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
479✔
237
    {
UNCOV
238
      const std::vector<double>* phi_ptr = nullptr;
×
239
      if (scalar_flux_iterate == "old")
×
240
      {
241
        phi_ptr = &self.GetPhiOldLocal();
×
242
      }
UNCOV
243
      else if (scalar_flux_iterate == "new")
×
244
      {
245
        phi_ptr = &self.GetPhiNewLocal();
×
246
      }
247
      else
248
      {
UNCOV
249
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
250
      }
UNCOV
251
      return ComputeFissionRate(self, *phi_ptr);
×
252
    },
253
    R"(
254
    Computes the total fission rate.
255

256
    Parameters
257
    ----------
258
    scalar_flux_iterate : {'old', 'new'}
259
        Specifies which scalar flux vector to use in the calculation.
260
            - 'old': Use the previous scalar flux iterate.
261
            - 'new': Use the current scalar flux iterate.
262

263
    Returns
264
    -------
265
    float
266
        The total fission rate.
267

268
    Raises
269
    ------
270
    ValueError
271
        If `scalar_flux_iterate` is not 'old' or 'new'.
272
    )",
273
    py::arg("scalar_flux_iterate")
479✔
274
  );
275
  lbs_problem.def(
479✔
276
    "WriteFluxMoments",
277
    [](LBSProblem& self, const std::string& file_base)
511✔
278
    {
279
      LBSSolverIO::WriteFluxMoments(self, file_base);
32✔
280
    },
281
    R"(
282
    Write flux moments to file.
283

284
    Parameters
285
    ----------
286
    file_base: str
287
        File basename.
288
    )",
289
    py::arg("file_base")
479✔
290
  );
291
  lbs_problem.def(
479✔
292
    "CreateAndWriteSourceMoments",
293
    [](LBSProblem& self, const std::string& file_base)
483✔
294
    {
295
      std::vector<double> source_moments = self.MakeSourceMomentsFromPhi();
4✔
296
      LBSSolverIO::WriteFluxMoments(self, file_base, source_moments);
4✔
297
    },
4✔
298
    R"(
299
    Write source moments from latest flux iterate to file.
300

301
    Parameters
302
    ----------
303
    file_base: str
304
        File basename.
305
    )",
306
    py::arg("file_base")
479✔
307
  );
308
  lbs_problem.def(
479✔
309
    "ReadFluxMomentsAndMakeSourceMoments",
310
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
479✔
311
    {
312
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, self.GetExtSrcMomentsLocal());
×
313
      log.Log() << "Making source moments from flux file.";
×
314
      std::vector<double>& temp_phi = self.GetPhiOldLocal();
×
UNCOV
315
      self.GetPhiOldLocal() = self.GetExtSrcMomentsLocal();
×
UNCOV
316
      self.GetExtSrcMomentsLocal() = self.MakeSourceMomentsFromPhi();
×
UNCOV
317
      self.GetPhiOldLocal() = temp_phi;
×
UNCOV
318
    },
×
319
    R"(
320
    Read flux moments and compute corresponding source moments.
321

322
    Parameters
323
    ----------
324
    file_base: str
325
        File basename.
326
    single_file_flag: bool
327
        True if all flux moments are in a single file.
328
    )",
329
    py::arg("file_base"),
958✔
330
    py::arg("single_file_flag")
479✔
331
  );
332
  lbs_problem.def(
479✔
333
    "ReadSourceMoments",
334
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
483✔
335
    {
336
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, self.GetExtSrcMomentsLocal());
4✔
337
    },
4✔
338
    R"(
339
    Read source moments from file.
340

341
    Parameters
342
    ----------
343
    file_base: str
344
        File basename.
345
    single_file_flag: bool
346
        True if all source moments are in a single file.
347
    )",
348
    py::arg("file_base"),
958✔
349
    py::arg("single_file_flag")
479✔
350
  );
351
  lbs_problem.def(
479✔
352
    "ReadFluxMoments",
353
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
479✔
354
    {
UNCOV
355
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag);
×
356
    },
357
    R"(
358
    Read flux moment data.
359

360
    Parameters
361
    ----------
362
    file_base: str
363
        File basename.
364
    single_file_flag: bool
365
        True if all flux moments are in a single file.
366
    )",
367
    py::arg("file_base"),
958✔
368
    py::arg("single_file_flag")
479✔
369
  );
370
  lbs_problem.def(
479✔
371
    "WriteAngularFluxes",
372
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
483✔
373
    {
374
      LBSSolverIO::WriteAngularFluxes(self, file_base);
4✔
375
    },
376
    R"(
377
    Write angular flux data to file.
378

379
    Parameters
380
    ----------
381
    file_base: str
382
        File basename.
383
    )",
384
    py::arg("file_base")
479✔
385
  );
386
  lbs_problem.def(
479✔
387
    "ReadAngularFluxes",
388
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
483✔
389
    {
390
      LBSSolverIO::ReadAngularFluxes(self, file_base);
4✔
391
    },
392
    R"(
393
    Read angular fluxes from file.
394

395
    Parameters
396
    ----------
397
    file_base: str
398
        File basename.
399
    )",
400
    py::arg("file_base")
479✔
401
  );
402
  lbs_problem.def(
479✔
403
    "SetPointSources",
404
    [](LBSProblem& self, py::kwargs& params)
479✔
405
    {
406
      for (auto [key, value] : params)
×
407
      {
UNCOV
408
        auto c_key = key.cast<std::string>();
×
409
        if (c_key == "clear_point_sources")
×
410
          self.ClearPointSources();
×
UNCOV
411
        else if (c_key == "point_sources")
×
412
        {
UNCOV
413
          auto sources = value.cast<py::list>();
×
414
          for (auto source : sources)
×
415
          {
416
            self.AddPointSource(source.cast<std::shared_ptr<PointSource>>());
×
417
          }
418
        }
×
419
        else
UNCOV
420
          throw std::runtime_error("Invalid argument provided to SetPointSources.\n");
×
UNCOV
421
      }
×
UNCOV
422
    },
×
423
    R"(
424
    Set or clear point sources.
425

426
    Parameters
427
    ----------
428
    clear_point_sources: bool, default=False
429
        If true, all current the point sources of the problem are deleted.
430
    point_sources: List[pyopensn.source.PointSource]
431
        List of new point sources to be added to the problem.
432
    )"
433
  );
434
  lbs_problem.def(
479✔
435
    "SetVolumetricSources",
436
    [](LBSProblem& self, py::kwargs& params)
499✔
437
    {
438
      for (auto [key, value] : params)
60✔
439
      {
440
        auto c_key = key.cast<std::string>();
20✔
441
        if (c_key == "clear_volumetric_sources")
20✔
442
          self.ClearVolumetricSources();
4✔
443
        else if (c_key == "volumetric_sources")
16✔
444
        {
445
          auto sources = value.cast<py::list>();
16✔
446
          for (auto source : sources)
48✔
447
          {
448
            self.AddVolumetricSource(source.cast<std::shared_ptr<VolumetricSource>>());
32✔
449
          }
450
        }
16✔
451
        else
UNCOV
452
          throw std::runtime_error("Invalid argument provided to SetVolumetricSources.\n");
×
453
      }
20✔
454
    },
20✔
455
    R"(
456
    Set or clear volumetric sources.
457

458
    Parameters
459
    ----------
460
    clear_volumetric_sources: bool, default=False
461
        If true, all current the volumetric sources of the problem are deleted.
462
    volumetric_sources: List[pyopensn.source.VolumetricSource]
463
        List of new volumetric sources to be added to the problem.
464
    )"
465
  );
466
  lbs_problem.def(
479✔
467
    "SetBoundaryOptions",
468
    [](LBSProblem& self, py::kwargs& params)
479✔
469
    {
470
      for (auto [key, value] : params)
×
471
      {
UNCOV
472
        auto c_key = key.cast<std::string>();
×
473
        if (c_key == "clear_boundary_conditions")
×
474
          self.ClearBoundaries();
×
UNCOV
475
        else if (c_key == "boundary_conditions")
×
476
        {
477
          auto boundaries = value.cast<py::list>();
×
478
          for (auto boundary : boundaries)
×
479
          {
480
            InputParameters input = LBSProblem::GetBoundaryOptionsBlock();
×
UNCOV
481
            input.AssignParameters(pyobj_to_param_block("", boundary.cast<py::dict>()));
×
482
            self.SetBoundaryOptions(input);
×
483
          }
×
484
        }
×
485
        else
UNCOV
486
          throw std::runtime_error("Invalid argument provided to SetBoundaryOptions.\n");
×
UNCOV
487
      }
×
488
    }
×
489
  );
490
  lbs_problem.def(
479✔
491
    "SetAdjoint",
UNCOV
492
    [](LBSProblem& self, bool adjoint)
×
493
    {
494
      self.SetAdjoint(adjoint);
12✔
495
    }
496
  );
497

498
  // discrete ordinate solver
499
  auto do_problem = py::class_<DiscreteOrdinatesProblem, std::shared_ptr<DiscreteOrdinatesProblem>,
479✔
500
                               LBSProblem>(
501
    slv,
502
    "DiscreteOrdinatesProblem",
503
    R"(
504
    Base class for discrete ordinates problems in Cartesian geometry.
505

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

510
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesProblem`.
511
    )"
512
  );
479✔
513
  do_problem.def(
958✔
514
    py::init(
479✔
515
      [](py::kwargs& params)
346✔
516
      {
517
        return DiscreteOrdinatesProblem::Create(kwargs_to_param_block(params));
346✔
518
      }
519
    ),
520
    R"(
521
    Construct a discrete ordinates problem with Cartesian geometry.
522

523
    Parameters
524
    ----------
525
    mesh : MeshContinuum
526
        The spatial mesh.
527
    num_groups : int
528
        The total number of energy groups.
529
    groupsets : List[Dict], default=[]
530
        A list of input parameter blocks, each block provides the iterative properties for a
531
        groupset.
532
    xs_map : List[Dict], default=[]
533
        A list of mappings from block ids to cross-section definitions.
534
    boundary_conditions: List[Dict], default=[]
535
        A list containing tables for each boundary specification.
536
    point_sources: List[pyopensn.source.PointSource], default=[]
537
        A list of point sources.
538
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
539
        A list of volumetric sources.
540
    options : Dict, default={}
541
        A block of optional configuration parameters. See `SetOptions` for available settings.
542
    sweep_type : str, default="AAH"
543
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
544
    use_gpus : bool, default=False
545
        A flag specifying whether GPU acceleration is used for the sweep. Currently, only ``AAH`` is
546
        supported.
547
    time_dependent : bool, default=False
548
        Enable time-dependent sweeps. Currently only supported with ``sweep_type="AAH"``.
549
    )"
550
  );
551
  do_problem.def(
479✔
552
    "SetOptions",
553
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
491✔
554
    {
555
      InputParameters input = DiscreteOrdinatesProblem::GetOptionsBlock();
12✔
556
      input.AssignParameters(kwargs_to_param_block(params));
12✔
557
      self.SetOptions(input);
12✔
558
    },
12✔
559
    R"(
560
    Set problem options from a large list of parameters.
561

562
    Parameters
563
    ----------
564
    adjoint: bool, default=False
565
        Flag for toggling whether the solver is in adjoint mode.
566
    )"
567
  );
568
  do_problem.def(
479✔
569
    "GetPsi",
570
    [](DiscreteOrdinatesProblem& self)
487✔
571
    {
572
      const auto& psi = self.GetPsiNewLocal();
8✔
573
      py::list psi_list;
8✔
574
      for (const auto& vec : psi)
16✔
575
      {
576
        auto array = py::array_t<double>(static_cast<py::ssize_t>(vec.size()),
8✔
577
                                         vec.data(),
578
                                         py::cast(self));
8✔
579
        psi_list.append(array);
8✔
580
      }
8✔
581
      return psi_list;
8✔
UNCOV
582
    },
×
583
    R"(
584
    Return psi as a list of NumPy arrays (float64), using zero-copy views into the
585
    underlying data.
586
    )"
587
  );
588
  do_problem.def(
479✔
589
    "ComputeBalance",
590
    [](DiscreteOrdinatesProblem& self)
506✔
591
    {
592
      ComputeBalance(self);
27✔
593
    },
594
    R"(
595
    Compute and print particle balance for the problem.
596
    )"
597
  );
598
  do_problem.def(
479✔
599
    "ComputeLeakage",
600
    [](DiscreteOrdinatesProblem& self, py::list bnd_names)
506✔
601
    {
602
      auto grid = self.GetGrid();
27✔
603
      // get the supported boundaries
604
      std::map<std::string, std::uint64_t> allowed_bd_names = grid->GetBoundaryNameMap();
27✔
605
      std::map<std::uint64_t, std::string> allowed_bd_ids = grid->GetBoundaryIDMap();
27✔
606
      // get the boundaries to parse, preserving user order
607
      std::vector<std::uint64_t> bndry_ids;
27✔
608
      if (bnd_names.size() > 1)
27✔
609
      {
610
        for (py::handle name : bnd_names)
120✔
611
        {
612
          auto sname = name.cast<std::string>();
72✔
613
          bndry_ids.push_back(allowed_bd_names.at(sname));
72✔
614
        }
72✔
615
      }
616
      else
617
      {
618
        bndry_ids = self.GetGrid()->GetUniqueBoundaryIDs();
9✔
619
      }
620
      // compute the leakage
621
      std::map<std::uint64_t, std::vector<double>> leakage = ComputeLeakage(self, bndry_ids);
27✔
622
      // convert result to native Python
623
      py::dict result;
27✔
624
      for (const auto& bndry_id : bndry_ids)
105✔
625
      {
626
        const auto it = leakage.find(bndry_id);
78✔
627
        if (it == leakage.end())
78✔
UNCOV
628
          continue;
×
629
        // construct numpy array and copy contents
630
        const auto& grp_wise_leakage = it->second;
78✔
631
        py::array_t<double> np_vector(py::ssize_t(grp_wise_leakage.size()));
78✔
632
        auto buffer = np_vector.request();
78✔
633
        auto *np_vector_data = static_cast<double*>(buffer.ptr);
78✔
634
        std::copy(grp_wise_leakage.begin(), grp_wise_leakage.end(), np_vector_data);
78✔
635
        const std::string& name = allowed_bd_ids.at(bndry_id);
78✔
636
        result[py::str(name)] = std::move(np_vector);
156✔
637
      }
78✔
638

639
      return result;
27✔
640
    },
54✔
641
    R"(
642
    Compute leakage for the problem.
643

644
    Parameters
645
    ----------
646
    bnd_names : List[str]
647
        A list of boundary names for which leakage should be computed.
648

649
    Returns
650
    -------
651
    Dict[str, numpy.ndarray]
652
        A dictionary mapping boundary names to group-wise leakage vectors.
653
        Each array contains the outgoing angular flux (per group) integrated over
654
        the corresponding boundary surface.
655

656
    Raises
657
    ------
658
    RuntimeError
659
        If `save_angular_flux` option was not enabled during problem setup.
660

661
    ValueError
662
        If one or more boundary ids are not present on the current mesh.
663
    )",
664
    py::arg("bnd_names")
479✔
665
  );
666

667
  // discrete ordinates curvilinear problem
668
  auto do_curvilinear_problem = py::class_<DiscreteOrdinatesCurvilinearProblem,
479✔
669
                                           std::shared_ptr<DiscreteOrdinatesCurvilinearProblem>,
670
                                           DiscreteOrdinatesProblem>(
671
    slv,
672
    "DiscreteOrdinatesCurvilinearProblem",
673
    R"(
674
    Base class for discrete ordinates problems in curvilinear geometry.
675

676
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesCurvilinearProblem`.
677
    )"
678
  );
479✔
679
  do_curvilinear_problem.def(
958✔
680
    py::init(
479✔
681
      [](py::kwargs& params)
8✔
682
      {
683
        return DiscreteOrdinatesCurvilinearProblem::Create(kwargs_to_param_block(params));
8✔
684
      }
685
    ),
686
    R"(
687
    Construct a discrete ordinates problem for curvilinear geometry.
688

689
    Warnings
690
    --------
691
       DiscreteOrdinatesCurvilinearProblem is **experimental** and should be used with caution!
692

693
    Parameters
694
    ----------
695
    mesh : MeshContinuum
696
        The spatial mesh.
697
    coord_system : int
698
        Coordinate system to use. Must be set to 2 (cylindrical coordinates).
699
    num_groups : int
700
        The total number of energy groups.
701
    groupsets : list of dict
702
        A list of input parameter blocks, each block provides the iterative properties for a
703
        groupset.
704
    xs_map : list of dict
705
        A list of mappings from block ids to cross-section definitions.
706
    boundary_conditions: List[Dict], default=[]
707
        A list containing tables for each boundary specification.
708
    point_sources: List[pyopensn.source.PointSource], default=[]
709
        A list of point sources.
710
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
711
        A list of volumetric sources.
712
    options : dict, optional
713
        A block of optional configuration parameters. See `SetOptions` for available settings.
714
    sweep_type : str, optional
715
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
716
    time_dependent : bool, default=False
717
        Enable time-dependent sweeps. Currently only supported with ``sweep_type="AAH"``.
718
    )"
719
  );
720
}
479✔
721

722
// Wrap steady-state solver
723
void
724
WrapSteadyState(py::module& slv)
479✔
725
{
726
  // clang-format off
727
  // steady state solver
728
  auto steady_state_solver = py::class_<SteadyStateSourceSolver, std::shared_ptr<SteadyStateSourceSolver>,
479✔
729
                                        Solver>(
730
    slv,
731
    "SteadyStateSourceSolver",
732
    R"(
733
    Steady state solver.
734

735
    Wrapper of :cpp:class:`opensn::SteadyStateSourceSolver`.
736
    )"
737
  );
479✔
738
  steady_state_solver.def(
958✔
739
    py::init(
479✔
740
      [](py::kwargs& params)
261✔
741
      {
742
        return SteadyStateSourceSolver::Create(kwargs_to_param_block(params));
261✔
743
      }
744
    ),
745
    R"(
746
    Construct a steady state solver.
747

748
    Parameters
749
    ----------
750
    pyopensn.solver.LBSProblem : LBSProblem
751
        Existing LBSProblem instance.
752
    )"
753
  );
754
  // clang-format on
755
}
479✔
756

757
// Wrap time-dependent solver
758
void
759
WrapTimeDependent(py::module& slv)
479✔
760
{
761
  // clang-format off
762
  auto time_dependent_solver =
479✔
763
    py::class_<TimeDependentSourceSolver, std::shared_ptr<TimeDependentSourceSolver>, Solver>(
764
      slv,
765
      "TimeDependentSourceSolver",
766
      R"(
767
      Time dependent solver.
768

769
      Wrapper of :cpp:class:`opensn::TimeDependentSourceSolver`.
770
      )"
771
    );
479✔
772
  time_dependent_solver.def(
958✔
773
    py::init(
479✔
774
      [](py::kwargs& params)
28✔
775
      {
776
        return TimeDependentSourceSolver::Create(kwargs_to_param_block(params));
28✔
777
      }
778
    ),
779
    R"(
780
    Construct a time dependent solver.
781

782
    Parameters
783
    ----------
784
    pyopensn.solver.LBSProblem : LBSProblem
785
        Existing LBSProblem instance.
786
    dt : float, optional, default=1.0
787
        Time step size used during the simulation.
788
    stop_time : float, optional, default=1.0
789
        Simulation end time.
790
    )"
791
  );
792
  time_dependent_solver.def(
479✔
793
    "Advance",
794
    &TimeDependentSourceSolver::Advance,
479✔
795
    R"(
796
    Advance the solver by a single timestep.
797

798
    This method uses the configured `dt` and `theta` values and will return
799
    immediately if the stop time has already been reached. Calling it
800
    repeatedly allows users to write custom python time loops.
801
    )");
802
  time_dependent_solver.def(
479✔
803
    "SetTimeStep",
804
    &TimeDependentSourceSolver::SetTimeStep,
479✔
805
    R"(
806
    Set the timestep size used by :meth:`Advance`.
807

808
    Parameters
809
    ----------
810
    dt : float
811
        New timestep size.
812
    )");
813
  time_dependent_solver.def(
479✔
814
    "SetTheta",
815
    &TimeDependentSourceSolver::SetTheta,
479✔
816
    R"(
817
    Set the theta parameter used by :meth:`Advance`.
818

819
    Parameters
820
    ----------
821
    theta : float
822
        Theta value between 0 and 1.
823
    )");
824
  // clang-format on
825
}
479✔
826

827
// Wrap non-linear k-eigen solver
828
void
829
WrapNLKEigen(py::module& slv)
479✔
830
{
831
  // clang-format off
832
  // non-linear k-eigen solver
833
  auto non_linear_k_eigen_solver = py::class_<NonLinearKEigenSolver, std::shared_ptr<NonLinearKEigenSolver>,
479✔
834
                                              Solver>(
835
    slv,
836
    "NonLinearKEigenSolver",
837
    R"(
838
    Non-linear k-eigenvalue solver.
839

840
    Wrapper of :cpp:class:`opensn::NonLinearKEigenSolver`.
841
    )"
842
  );
479✔
843
  non_linear_k_eigen_solver.def(
958✔
844
    py::init(
479✔
845
      [](py::kwargs& params)
28✔
846
      {
847
        return NonLinearKEigenSolver::Create(kwargs_to_param_block(params));
28✔
848
      }
849
        ),
850
    R"(
851
    Construct a non-linear k-eigenvalue solver.
852

853
    Parameters
854
    ----------
855
    lbs_problem: pyopensn.solver.LBSProblem
856
        Existing LBSProblem instance.
857
    nl_abs_tol: float, default=1.0e-8
858
        Non-linear absolute tolerance.
859
    nl_rel_tol: float, default=1.0e-8
860
        Non-linear relative tolerance.
861
    nl_sol_tol: float, default=1.0e-50
862
        Non-linear solution tolerance.
863
    nl_max_its: int, default=50
864
        Non-linear algorithm maximum iterations.
865
    l_abs_tol: float, default=1.0e-8
866
        Linear absolute tolerance.
867
    l_rel_tol: float, default=1.0e-8
868
        Linear relative tolerance.
869
    l_div_tol: float, default=1.0e6
870
        Linear divergence tolerance.
871
    l_max_its: int, default=50
872
        Linear algorithm maximum iterations.
873
    l_gmres_restart_intvl: int, default=30
874
        GMRES restart interval.
875
    l_gmres_breakdown_tol: float, default=1.0e6
876
        GMRES breakdown tolerance.
877
    reset_phi0: bool, default=True
878
        If true, reinitializes scalar fluxes to 1.0.
879
    num_initial_power_iterations: int, default=0
880
        Number of initial power iterations before the non-linear solve.
881
    )"
882
  );
883
  non_linear_k_eigen_solver.def(
479✔
884
    "GetEigenvalue",
885
    &NonLinearKEigenSolver::GetEigenvalue,
479✔
886
    R"(
887
    Return the current k‑eigenvalue.
888
    )"
889
  );
890
  // clang-format on
891
}
479✔
892

893
// Wrap power iteration solvers
894
void
895
WrapPIteration(py::module& slv)
479✔
896
{
897
  // clang-format off
898
  // power iteration k-eigen solver
899
  auto pi_k_eigen_solver = py::class_<PowerIterationKEigenSolver, std::shared_ptr<PowerIterationKEigenSolver>,
479✔
900
                                      Solver>(
901
    slv,
902
    "PowerIterationKEigenSolver",
903
    R"(
904
    Power iteration k-eigenvalue solver.
905

906
    Wrapper of :cpp:class:`opensn::PowerIterationKEigenSolver`.
907
    )"
908
  );
479✔
909
  pi_k_eigen_solver.def(
958✔
910
    py::init(
479✔
911
      [](py::kwargs& params)
35✔
912
      {
913
        return PowerIterationKEigenSolver::Create(kwargs_to_param_block(params));
35✔
914
      }
915
    ),
916
    R"(
917
    Construct a power iteration k-eigen solver.
918

919
    Parameters
920
    ----------
921
    problem: pyopensn.solver.LBSProblem
922
        Existing DiscreteOrdinatesProblem instance.
923
    acceleration: pyopensn.solver.DiscreteOrdinatesKEigenAcceleration
924
        Optional DiscreteOrdinatesKEigenAcceleration instance for acceleration.
925
    max_iters: int, default = 1000
926
        Maximum power iterations allowed.
927
    k_tol: float, default = 1.0e-10
928
        Tolerance on the k-eigenvalue.
929
    reset_solution: bool, default=True
930
        If true, initialize flux moments to 1.0.
931
    reset_phi0: bool, default=True
932
        If true, reinitializes scalar fluxes to 1.0.
933
    )"
934
  );
935
  pi_k_eigen_solver.def(
479✔
936
    "GetEigenvalue",
937
    &PowerIterationKEigenSolver::GetEigenvalue,
479✔
938
    R"(
939
    Return the current k‑eigenvalue.
940
    )"
941
  );
942
  // clang-format on
943
}
479✔
944

945
// Wrap LBS solver
946
void
947
WrapDiscreteOrdinatesKEigenAcceleration(py::module& slv)
479✔
948
{
949
  // clang-format off
950
  // discrete ordinates k-eigen acceleration base
951
  auto acceleration = py::class_<DiscreteOrdinatesKEigenAcceleration,
479✔
952
                                 std::shared_ptr<DiscreteOrdinatesKEigenAcceleration>>(
953
    slv,
954
    "DiscreteOrdinatesKEigenAcceleration",
955
    R"(
956
    Base class for discrete ordinates k-eigenvalue acceleration methods.
957

958
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesKEigenAcceleration`.
959
    )"
960
  );
479✔
961
  // SCDSA acceleration
962
  auto scdsa_acceleration = py::class_<SCDSAAcceleration,
479✔
963
                                       std::shared_ptr<SCDSAAcceleration>,
964
                                       DiscreteOrdinatesKEigenAcceleration>(
965
    slv,
966
    "SCDSAAcceleration",
967
    R"(
968
    Construct an SCDSA accelerator for the power iteration k-eigenvalue solver.
969

970
    Wrapper of :cpp:class:`opensn::SCDSAAcceleration`.
971
    )"
972
  );
479✔
973
  scdsa_acceleration.def(
958✔
974
    py::init(
479✔
975
      [](py::kwargs& params)
8✔
976
      {
977
        return SCDSAAcceleration::Create(kwargs_to_param_block(params));
8✔
978
      }
979
    ),
980
    R"(
981
    SCDSA acceleration for the power iteration k-eigenvalue solver.
982

983
    Parameters
984
    ----------
985
    problem: pyopensn.solver.LBSProblem
986
        Existing DiscreteOrdinatesProblem instance.
987
    l_abs_tol: float, defauilt=1.0e-10
988
        Absolute residual tolerance.
989
    max_iters: int, default=100
990
        Maximum allowable iterations.
991
    verbose: bool, default=False
992
        If true, enables verbose output.
993
    petsc_options: str, default="ssss"
994
        Additional PETSc options.
995
    pi_max_its: int, default=50
996
        Maximum allowable iterations for inner power iterations.
997
    pi_k_tol: float, default=1.0e-10
998
        k-eigenvalue tolerance for the inner power iterations.
999
    sdm: str, default="pwld"
1000
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1001
            - 'pwld' : Piecewise Linear Discontinuous
1002
            - 'pwlc' : Piecewise Linear Continuous
1003
    )"
1004
  );
1005
  // SMM acceleration
1006
  auto smm_acceleration = py::class_<SMMAcceleration,
479✔
1007
                                     std::shared_ptr<SMMAcceleration>,
1008
                                     DiscreteOrdinatesKEigenAcceleration>(
1009
    slv,
1010
    "SMMAcceleration",
1011
    R"(
1012
    Construct an SMM accelerator for the power iteration k-eigenvalue solver.
1013

1014
    Wrapper of :cpp:class:`opensn::SMMAcceleration`.
1015
    )"
1016
  );
479✔
1017
  smm_acceleration.def(
958✔
1018
    py::init(
479✔
1019
      [](py::kwargs& params)
4✔
1020
      {
1021
        return SMMAcceleration::Create(kwargs_to_param_block(params));
4✔
1022
      }
1023
    ),
1024
    R"(
1025
    SMM acceleration for the power iteration k-eigenvalue solver.
1026

1027
    Warnings
1028
    --------
1029
       SMM acceleration is **experimental** and should be used with caution!
1030
       SMM accleration only supports problems with isotropic scattering.
1031

1032
    Parameters
1033
    ----------
1034
    problem: pyopensn.solver.LBSProblem
1035
        Existing DiscreteOrdinatesProblem instance.
1036
    l_abs_tol: float, defauilt=1.0e-10
1037
        Absolute residual tolerance.
1038
    max_iters: int, default=100
1039
        Maximum allowable iterations.
1040
    verbose: bool, default=False
1041
        If true, enables verbose output.
1042
    petsc_options: str, default="ssss"
1043
        Additional PETSc options.
1044
    pi_max_its: int, default=50
1045
        Maximum allowable iterations for inner power iterations.
1046
    pi_k_tol: float, default=1.0e-10
1047
        k-eigenvalue tolerance for the inner power iterations.
1048
    sdm: str, default="pwld"
1049
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1050
            - 'pwld' : Piecewise Linear Discontinuous
1051
            - 'pwlc' : Piecewise Linear Continuous
1052
    )"
1053
  );
1054
  // clang-format on
1055
}
479✔
1056

1057
// Wrap the solver components of OpenSn
1058
void
1059
py_solver(py::module& pyopensn)
62✔
1060
{
1061
  py::module slv = pyopensn.def_submodule("solver", "Solver module.");
62✔
1062
  WrapProblem(slv);
62✔
1063
  WrapSolver(slv);
62✔
1064
  WrapLBS(slv);
62✔
1065
  WrapSteadyState(slv);
62✔
1066
  WrapTimeDependent(slv);
62✔
1067
  WrapNLKEigen(slv);
62✔
1068
  WrapDiscreteOrdinatesKEigenAcceleration(slv);
62✔
1069
  WrapPIteration(slv);
62✔
1070
}
62✔
1071

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