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

Open-Sn / opensn / 22336315317

23 Feb 2026 06:34PM UTC coverage: 74.149% (+0.03%) from 74.123%
22336315317

push

github

web-flow
Merge pull request #889 from andrsd/issue/761

Fix PETSc build issue when using cmake 4

20027 of 27009 relevant lines covered (74.15%)

67102519.58 hits per line

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

77.31
/python/lib/solver.cc
1
// SPDX-FileCopyrightText: 2025 The OpenSn Authors <https://open-sn.github.io/opensn/>
2
// SPDX-License-Identifier: MIT
3

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

30
namespace opensn
31
{
32

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

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

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

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

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

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

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

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

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

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

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

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

250
    Notes
251
    -----
252
    This call is additive: only options explicitly supplied are updated.
253
    Options requirements:
254
    - ``restart_writes_enabled=True`` requires non-empty ``write_restart_path``.
255
    - ``write_restart_time_interval>0`` requires ``restart_writes_enabled=True``.
256
    - ``write_restart_time_interval`` must be ``0`` or ``>=30``.
257
    - non-empty ``field_function_prefix`` requires ``field_function_prefix_option='prefix'``.
258
    )"
259
  );
260
  lbs_problem.def(
668✔
261
    "ComputeFissionRate",
262
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
668✔
263
    {
264
      const std::vector<double>* phi_ptr = nullptr;
×
265
      if (scalar_flux_iterate == "old")
×
266
      {
267
        phi_ptr = &self.GetPhiOldLocal();
×
268
      }
269
      else if (scalar_flux_iterate == "new")
×
270
      {
271
        phi_ptr = &self.GetPhiNewLocal();
×
272
      }
273
      else
274
      {
275
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
276
      }
277
      return ComputeFissionRate(self, *phi_ptr);
×
278
    },
279
    R"(
280
    Computes the total fission rate.
281

282
    Parameters
283
    ----------
284
    scalar_flux_iterate : {'old', 'new'}
285
        Specifies which scalar flux vector to use in the calculation.
286
            - 'old': Use the previous scalar flux iterate.
287
            - 'new': Use the current scalar flux iterate.
288

289
    Returns
290
    -------
291
    float
292
        The total fission rate.
293

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

323
    Parameters
324
    ----------
325
    scalar_flux_iterate : {'old', 'new'}
326
        Specifies which scalar flux vector to use in the calculation.
327
            - 'old': Use the previous scalar flux iterate.
328
            - 'new': Use the current scalar flux iterate.
329

330
    Returns
331
    -------
332
    float
333
        The total fission production.
334

335
    Raises
336
    ------
337
    ValueError
338
        If `scalar_flux_iterate` is not 'old' or 'new'.
339
    )",
340
    py::arg("scalar_flux_iterate")
668✔
341
  );
342
  lbs_problem.def(
668✔
343
    "GetPhiOldLocal",
344
    [](LBSProblem& self)
716✔
345
    {
346
      return convert_vector(self.GetPhiOldLocal());
48✔
347
    },
348
    R"(
349
    Get the previous scalar flux iterate (local vector).
350

351
    Returns
352
    -------
353
    memoryview
354
        Memory view of the local old scalar flux vector.
355
    )"
356
  );
357
  lbs_problem.def(
668✔
358
    "GetPhiNewLocal",
359
    [](LBSProblem& self)
716✔
360
    {
361
      return convert_vector(self.GetPhiNewLocal());
48✔
362
    },
363
    R"(
364
    Get the current scalar flux iterate (local vector).
365

366
    Returns
367
    -------
368
    memoryview
369
        Memory view of the local new scalar flux vector.
370
    )"
371
  );
372
  lbs_problem.def(
668✔
373
    "WriteFluxMoments",
374
    [](LBSProblem& self, const std::string& file_base)
700✔
375
    {
376
      LBSSolverIO::WriteFluxMoments(self, file_base);
32✔
377
    },
378
    R"(
379
    Write flux moments to file.
380

381
    Parameters
382
    ----------
383
    file_base: str
384
        File basename.
385
    )",
386
    py::arg("file_base")
668✔
387
  );
388
  lbs_problem.def(
668✔
389
    "CreateAndWriteSourceMoments",
390
    [](LBSProblem& self, const std::string& file_base)
672✔
391
    {
392
      std::vector<double> source_moments = self.MakeSourceMomentsFromPhi();
4✔
393
      LBSSolverIO::WriteFluxMoments(self, file_base, source_moments);
4✔
394
    },
4✔
395
    R"(
396
    Write source moments from latest flux iterate to file.
397

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

419
    Parameters
420
    ----------
421
    file_base: str
422
        File basename.
423
    single_file_flag: bool
424
        True if all flux moments are in a single file.
425
    )",
426
    py::arg("file_base"),
1,336✔
427
    py::arg("single_file_flag")
668✔
428
  );
429
  lbs_problem.def(
668✔
430
    "ReadSourceMoments",
431
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
672✔
432
    {
433
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, self.GetExtSrcMomentsLocal());
4✔
434
    },
4✔
435
    R"(
436
    Read source moments from file.
437

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

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

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

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

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

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

593
    Parameters
594
    ----------
595
    xs_map: List[Dict]
596
        A list of block-id to cross-section mapping dictionaries. Each dictionary supports:
597
          - block_ids: List[int] (required)
598
              Mesh block ids to associate with the cross section.
599
          - xs: pyopensn.xs.MultiGroupXS (required)
600
              Cross section object.
601
    )"
602
  );
603
  lbs_problem.def(
668✔
604
    "SetSaveAngularFlux",
605
    [](LBSProblem& self, bool save)
668✔
606
    {
607
      self.SetSaveAngularFlux(save);
×
608
    },
609
    py::arg("save")
668✔
610
  );
611
  lbs_problem.def(
668✔
612
    "SetAdjoint",
613
    [](LBSProblem& self, bool adjoint)
×
614
    {
615
      self.SetAdjoint(adjoint);
12✔
616
    }
617
  );
618

619
  // discrete ordinate solver
620
  auto do_problem = py::class_<DiscreteOrdinatesProblem, std::shared_ptr<DiscreteOrdinatesProblem>,
668✔
621
                               LBSProblem>(
622
    slv,
623
    "DiscreteOrdinatesProblem",
624
    R"(
625
    Base class for discrete ordinates problems in Cartesian geometry.
626

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

631
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesProblem`.
632
    )"
633
  );
668✔
634
  do_problem.def(
1,336✔
635
    py::init(
668✔
636
      [](py::kwargs& params)
494✔
637
      {
638
        return DiscreteOrdinatesProblem::Create(kwargs_to_param_block(params));
494✔
639
      }
640
    ),
641
    R"(
642
    Construct a discrete ordinates problem with Cartesian geometry.
643

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

755
    Parameters
756
    ----------
757
    **kwargs
758
        Same option keys and semantics as :meth:`LBSProblem.SetOptions`.
759

760
    Notes
761
    -----
762
    This call is additive: only options explicitly supplied are updated.
763
    )"
764
  );
765
  do_problem.def(
668✔
766
    "SetBoundaryOptions",
767
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
668✔
768
    {
769
      for (auto [key, value] : params)
×
770
      {
771
        auto c_key = key.cast<std::string>();
×
772
        if (c_key == "clear_boundary_conditions")
×
773
          self.ClearBoundaries();
×
774
        else if (c_key == "boundary_conditions")
×
775
        {
776
          auto boundaries = value.cast<py::list>();
×
777
          for (auto boundary : boundaries)
×
778
          {
779
            InputParameters input = DiscreteOrdinatesProblem::GetBoundaryOptionsBlock();
×
780
            input.AssignParameters(pyobj_to_param_block("", boundary.cast<py::dict>()));
×
781
            self.SetBoundaryOptions(input);
×
782
          }
×
783
        }
×
784
        else
785
          throw std::runtime_error("Invalid argument provided to SetBoundaryOptions.\n");
×
786
      }
×
787
    },
×
788
    R"(
789
    Set or clear boundary conditions.
790

791
    Parameters
792
    ----------
793
    clear_boundary_conditions: bool, default=False
794
        If true, all current boundary conditions are deleted.
795
    boundary_conditions: List[Dict]
796
        A list of boundary condition dictionaries. Each dictionary supports:
797
          - name: str (required)
798
              Boundary name that identifies the specific boundary.
799
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
800
              Boundary type specification.
801
          - group_strength: List[float], optional
802
              Required when ``type='isotropic'``. Isotropic strength per group.
803
          - function: AngularFluxFunction, optional
804
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
805
    )"
806
  );
807
  do_problem.def(
668✔
808
    "GetPsi",
809
    [](DiscreteOrdinatesProblem& self)
676✔
810
    {
811
      const auto& psi = self.GetPsiNewLocal();
8✔
812
      py::list psi_list;
8✔
813
      for (const auto& vec : psi)
16✔
814
      {
815
        auto array = py::array_t<double>(static_cast<py::ssize_t>(vec.size()),
8✔
816
                                         vec.data(),
817
                                         py::cast(self));
8✔
818
        psi_list.append(array);
8✔
819
      }
8✔
820
      return psi_list;
8✔
821
    },
×
822
    R"(
823
    Return psi as a list of NumPy arrays (float64), using zero-copy views into the
824
    underlying data.
825
    )"
826
  );
827
  do_problem.def(
668✔
828
    "GetAngularFieldFunctionList",
829
    [](DiscreteOrdinatesProblem& self, py::list groups, py::list angles)
668✔
830
    {
831
      std::vector<unsigned int> group_ids;
×
832
      std::vector<size_t> angle_ids;
×
833
      group_ids.reserve(groups.size());
×
834
      angle_ids.reserve(angles.size());
×
835

836
      for (py::handle g : groups)
×
837
        group_ids.push_back(g.cast<unsigned int>());
×
838
      for (py::handle a : angles)
×
839
        angle_ids.push_back(a.cast<size_t>());
×
840

841
      try
×
842
      {
843
        auto ff_list = self.GetAngularFluxFieldFunctionList(group_ids, angle_ids);
×
844
        py::list out;
×
845
        for (const auto& ff : ff_list)
×
846
          out.append(ff);
×
847
        return out;
×
848
      }
×
849
      catch (const std::exception& err)
×
850
      {
851
        const std::string message = err.what();
×
852
        if (message.find("problem not initialized") != std::string::npos)
×
853
          throw std::runtime_error(
×
854
            "GetAngularFieldFunctionList requires Initialize() to be called first. "
855
            "Call solver.Initialize() (or problem.Initialize()) before requesting "
856
            "angular flux field functions.");
×
857
        throw;
×
858
      }
×
859
    },
×
860
    R"(
861
    Return field functions for selected angular flux components.
862

863
    This requires `Initialize()` to have completed; it throws if the problem is
864
    not yet initialized.
865
    Note: You must enable angular flux storage (``save_angular_flux=True``) in
866
    the problem options, or use a transient/time-dependent solver that retains
867
    angular fluxes, otherwise the field functions will remain zero.
868

869
    Example
870
    -------
871
    ```python
872
    solver.Initialize()
873
    solver.Execute()
874
    ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
875
    ```
876

877
    For transient/time-dependent runs, call this after each timestep. Two common
878
    patterns are:
879

880
    1) Use `TransientSolver.Execute()` with a post-advance callback:
881
    ```python
882
    solver = TransientSolver(problem=phys)
883
    solver.Initialize()
884

885
    def post_advance():
886
        ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
887
        FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
888

889
    solver.SetPostAdvanceCallback(post_advance)
890
    solver.Execute()
891
    ```
892

893
    2) Use a custom Python loop with `TransientSolver.Advance()`:
894
    ```python
895
    solver = TransientSolver(problem=phys)
896
    solver.Initialize()
897
    for _ in range(num_steps):
898
        solver.Advance()
899
        ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
900
        FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
901
    ```
902

903
    Parameters
904
    ----------
905
    groups : List[int]
906
        Global group indices to export.
907
    angles : List[int]
908
        Angle indices within the groupset quadrature for each group.
909

910
    Returns
911
    -------
912
    List[pyopensn.fieldfunc.FieldFunctionGridBased]
913
        Field functions for the requested (group, angle) pairs.
914
    )",
915
    py::arg("groups"),
1,336✔
916
    py::arg("angles")
668✔
917
  );
918
  do_problem.def(
668✔
919
    "ComputeLeakage",
920
    [](DiscreteOrdinatesProblem& self, py::list bnd_names)
707✔
921
    {
922
      auto grid = self.GetGrid();
39✔
923
      // get the supported boundaries
924
      std::map<std::string, std::uint64_t> allowed_bd_names = grid->GetBoundaryNameMap();
39✔
925
      std::map<std::uint64_t, std::string> allowed_bd_ids = grid->GetBoundaryIDMap();
39✔
926
      const auto coord_sys = grid->GetCoordinateSystem();
39✔
927
      const auto mesh_type = grid->GetType();
39✔
928
      const auto dim = grid->GetDimension();
39✔
929
      // get the boundaries to parse, preserving user order
930
      std::vector<std::uint64_t> bndry_ids;
39✔
931
      if (bnd_names.size() > 1)
39✔
932
      {
933
        for (py::handle name : bnd_names)
184✔
934
        {
935
          auto sname = name.cast<std::string>();
112✔
936
          if (coord_sys == CoordinateSystemType::CYLINDRICAL && dim == 2)
112✔
937
          {
938
            if (sname == "xmin" || sname == "xmax" || sname == "ymin" || sname == "ymax")
24✔
939
              throw std::runtime_error("ComputeLeakage: Boundary name '" + sname +
×
940
                                       "' is invalid for cylindrical orthogonal meshes. "
941
                                       "Use rmin, rmax, zmin, zmax.");
×
942

943
            if (mesh_type == MeshType::ORTHOGONAL)
24✔
944
            {
945
              if (sname == "rmin") sname = "xmin";
24✔
946
              else if (sname == "rmax") sname = "xmax";
24✔
947
              else if (sname == "zmin") sname = "ymin";
16✔
948
              else if (sname == "zmax") sname = "ymax";
8✔
949
            }
950
          }
951
          bndry_ids.push_back(allowed_bd_names.at(sname));
112✔
952
        }
112✔
953
      }
954
      else
955
      {
956
        bndry_ids = self.GetGrid()->GetUniqueBoundaryIDs();
9✔
957
      }
958
      // compute the leakage
959
      std::map<std::uint64_t, std::vector<double>> leakage = ComputeLeakage(self, bndry_ids);
39✔
960
      const bool rz_ortho = (coord_sys == CoordinateSystemType::CYLINDRICAL &&
78✔
961
                             mesh_type == MeshType::ORTHOGONAL && dim == 2);
39✔
962

963
      auto to_rz_name = [](const std::string& name)
63✔
964
      {
965
        if (name == "xmin") return std::string("rmin");
24✔
966
        if (name == "xmax") return std::string("rmax");
24✔
967
        if (name == "ymin") return std::string("zmin");
16✔
968
        if (name == "ymax") return std::string("zmax");
8✔
969
        return name;
×
970
      };
971

972
      // convert result to native Python
973
      py::dict result;
39✔
974
      for (const auto& bndry_id : bndry_ids)
157✔
975
      {
976
        const auto it = leakage.find(bndry_id);
118✔
977
        if (it == leakage.end())
118✔
978
          continue;
×
979
        // construct numpy array and copy contents
980
        const auto& grp_wise_leakage = it->second;
118✔
981
        py::array_t<double> np_vector(py::ssize_t(grp_wise_leakage.size()));
118✔
982
        auto buffer = np_vector.request();
118✔
983
        auto *np_vector_data = static_cast<double*>(buffer.ptr);
118✔
984
        std::copy(grp_wise_leakage.begin(), grp_wise_leakage.end(), np_vector_data);
118✔
985
        std::string name = allowed_bd_ids.at(bndry_id);
118✔
986
        if (rz_ortho)
118✔
987
          name = to_rz_name(name);
24✔
988
        result[py::str(name)] = std::move(np_vector);
236✔
989
      }
118✔
990

991
      return result;
39✔
992
    },
78✔
993
    R"(
994
    Compute leakage for the problem.
995

996
    Parameters
997
    ----------
998
    bnd_names : List[str]
999
        A list of boundary names for which leakage should be computed.
1000

1001
    Returns
1002
    -------
1003
    Dict[str, numpy.ndarray]
1004
        A dictionary mapping boundary names to group-wise leakage vectors.
1005
        Each array contains the outgoing angular flux (per group) integrated over
1006
        the corresponding boundary surface.
1007

1008
    Raises
1009
    ------
1010
    RuntimeError
1011
        If `save_angular_flux` option was not enabled during problem setup.
1012

1013
    ValueError
1014
        If one or more boundary ids are not present on the current mesh.
1015
    )",
1016
    py::arg("bnd_names")
668✔
1017
  );
1018

1019
  // discrete ordinates curvilinear problem
1020
  auto do_curvilinear_problem = py::class_<DiscreteOrdinatesCurvilinearProblem,
668✔
1021
                                           std::shared_ptr<DiscreteOrdinatesCurvilinearProblem>,
1022
                                           DiscreteOrdinatesProblem>(
1023
    slv,
1024
    "DiscreteOrdinatesCurvilinearProblem",
1025
    R"(
1026
    Base class for discrete ordinates problems in curvilinear geometry.
1027

1028
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesCurvilinearProblem`.
1029
    )"
1030
  );
668✔
1031
  do_curvilinear_problem.def(
1,336✔
1032
    py::init(
668✔
1033
      [](py::kwargs& params)
80✔
1034
      {
1035
        return DiscreteOrdinatesCurvilinearProblem::Create(kwargs_to_param_block(params));
80✔
1036
      }
1037
    ),
1038
    R"(
1039
    Construct a discrete ordinates problem for curvilinear geometry.
1040

1041
    Warnings
1042
    --------
1043
       DiscreteOrdinatesCurvilinearProblem is **experimental** and should be used with caution!
1044

1045
    Parameters
1046
    ----------
1047
    mesh : MeshContinuum
1048
        The spatial mesh.
1049
    coord_system : int
1050
        Coordinate system to use. Must be set to 2 (cylindrical coordinates).
1051
    num_groups : int
1052
        The total number of energy groups.
1053
    groupsets : list of dict
1054
        A list of input parameter blocks, each block provides the iterative properties for a
1055
        groupset. Each dictionary supports:
1056
          - groups_from_to: List[int] (required)
1057
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
1058
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
1059
              Handle to an angular quadrature.
1060
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
1061
              Angle aggregation method to use during sweeping.
1062
          - angle_aggregation_num_subsets: int, default=1
1063
              Number of angle subsets used for aggregation.
1064
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
1065
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
1066
              Iterative method used for inner linear solves.
1067
          - l_abs_tol: float, default=1.0e-6
1068
              Inner linear solver absolute residual tolerance.
1069
          - l_max_its: int, default=200
1070
              Inner linear solver maximum iterations.
1071
          - gmres_restart_interval: int, default=30
1072
              GMRES restart interval, if GMRES is used.
1073
          - allow_cycles: bool, default=True
1074
              Whether cyclic dependencies are allowed in sweeps.
1075
          - apply_wgdsa: bool, default=False
1076
              Enable within-group DSA for this groupset.
1077
          - wgdsa_l_abs_tol: float, default=1.0e-4
1078
              WGDSA linear absolute tolerance.
1079
          - wgdsa_l_max_its: int, default=30
1080
              WGDSA maximum iterations.
1081
          - wgdsa_verbose: bool, default=False
1082
              Verbose WGDSA output.
1083
          - wgdsa_petsc_options: str, default=''
1084
              PETSc options string for the WGDSA solver.
1085
          - apply_tgdsa: bool, default=False
1086
              Enable two-grid DSA for this groupset.
1087
          - tgdsa_l_abs_tol: float, default=1.0e-4
1088
              TGDSA linear absolute tolerance.
1089
          - tgdsa_l_max_its: int, default=30
1090
              TGDSA maximum iterations.
1091
          - tgdsa_verbose: bool, default=False
1092
              Verbose TGDSA output.
1093
          - tgdsa_petsc_options: str, default=''
1094
    xs_map : list of dict
1095
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
1096
          - block_ids: List[int] (required)
1097
              Mesh block IDs to associate with the cross section.
1098
          - xs: pyopensn.xs.MultiGroupXS (required)
1099
              Cross-section object to assign to the specified blocks.
1100
    boundary_conditions: List[Dict], default=[]
1101
        A list containing tables for each boundary specification. Each dictionary supports:
1102
          - name: str (required)
1103
              Boundary name that identifies the specific boundary.
1104
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
1105
              Boundary type specification.
1106
          - group_strength: List[float], optional
1107
              Required when ``type='isotropic'``. Isotropic strength per group.
1108
          - function: AngularFluxFunction, optional
1109
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
1110
    point_sources: List[pyopensn.source.PointSource], default=[]
1111
        A list of point sources.
1112
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
1113
        A list of volumetric sources.
1114
    options : dict, optional
1115
        A block of optional configuration parameters. Each dictionary supports the same keys as
1116
        :meth:`LBSProblem.SetOptions`, including:
1117
          - max_mpi_message_size: int, default=32768
1118
          - restart_writes_enabled: bool, default=False
1119
          - write_delayed_psi_to_restart: bool, default=True
1120
          - read_restart_path: str, default=''
1121
          - write_restart_path: str, default=''
1122
          - write_restart_time_interval: int, default=0
1123
          - use_precursors: bool, default=False
1124
          - use_source_moments: bool, default=False
1125
          - save_angular_flux: bool, default=False
1126
          - verbose_inner_iterations: bool, default=True
1127
          - verbose_outer_iterations: bool, default=True
1128
          - max_ags_iterations: int, default=100
1129
          - ags_tolerance: float, default=1.0e-6
1130
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
1131
          - verbose_ags_iterations: bool, default=True
1132
          - power_field_function_on: bool, default=False
1133
          - power_default_kappa: float, default=3.20435e-11
1134
          - power_normalization: float, default=-1.0
1135
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
1136
          - field_function_prefix: str, default=''
1137
    sweep_type : str, optional
1138
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
1139
    )"
1140
  );
1141
}
668✔
1142

1143
// Wrap steady-state solver
1144
void
1145
WrapSteadyState(py::module& slv)
668✔
1146
{
1147
  // clang-format off
1148
  // steady state solver
1149
  auto steady_state_solver = py::class_<SteadyStateSourceSolver, std::shared_ptr<SteadyStateSourceSolver>,
668✔
1150
                                        Solver>(
1151
    slv,
1152
    "SteadyStateSourceSolver",
1153
    R"(
1154
    Steady state solver.
1155

1156
    Wrapper of :cpp:class:`opensn::SteadyStateSourceSolver`.
1157
    )"
1158
  );
668✔
1159
  steady_state_solver.def(
1,336✔
1160
    py::init(
668✔
1161
      [](py::kwargs& params)
353✔
1162
      {
1163
        return SteadyStateSourceSolver::Create(kwargs_to_param_block(params));
353✔
1164
      }
1165
    ),
1166
    R"(
1167
    Construct a steady state solver.
1168

1169
    Parameters
1170
    ----------
1171
    pyopensn.solver.LBSProblem : LBSProblem
1172
        Existing LBSProblem instance.
1173
    )"
1174
  );
1175
  // clang-format on
1176
}
668✔
1177

1178
// Wrap transient solver
1179
void
1180
WrapTransient(py::module& slv)
668✔
1181
{
1182
  // clang-format off
1183
  auto transient_solver =
668✔
1184
    py::class_<TransientSolver, std::shared_ptr<TransientSolver>, Solver>(
1185
      slv,
1186
      "TransientSolver",
1187
      R"(
1188
      Transient solver.
1189

1190
      Wrapper of :cpp:class:`opensn::TransientSolver`.
1191
      )"
1192
    );
668✔
1193
  transient_solver.def(
1,336✔
1194
    py::init(
668✔
1195
      [](py::kwargs& params)
156✔
1196
      {
1197
        return TransientSolver::Create(kwargs_to_param_block(params));
156✔
1198
      }
1199
    ),
1200
    R"(
1201
    Construct a transient solver.
1202

1203
    Parameters
1204
    ----------
1205
    pyopensn.solver.DiscreteOrdinatesProblem : DiscreteOrdinatesProblem
1206
        Existing discrete ordinates problem instance.
1207
    dt : float, optional, default=2.0e-3
1208
        Time step size used during the simulation.
1209
    stop_time : float, optional, default=0.1
1210
        Simulation end time.
1211
    theta : float, optional, default=0.5
1212
        Time differencing scheme parameter.
1213
    initial_state : str, optional, default="existing"
1214
        Initial state for the transient solve. Allowed values: existing, zero.
1215
        In "zero" mode, the solver may initialize the problem internally if needed.
1216
    verbose : bool, optional, default=True
1217
        Enable verbose logging.
1218
    )"
1219
  );
1220
  transient_solver.def(
668✔
1221
    "SetTimeStep",
1222
    &TransientSolver::SetTimeStep,
668✔
1223
    R"(
1224
    Set the timestep size used by :meth:`Advance`.
1225

1226
    Parameters
1227
    ----------
1228
    dt : float
1229
        New timestep size.
1230
    )");
1231
  transient_solver.def(
668✔
1232
    "SetTheta",
1233
    &TransientSolver::SetTheta,
668✔
1234
    R"(
1235
    Set the theta parameter used by :meth:`Advance`.
1236

1237
    Parameters
1238
    ----------
1239
    theta : float
1240
        Theta value between 1.0e-16 and 1.
1241
    )");
1242
  transient_solver.def(
668✔
1243
    "Advance",
1244
    &TransientSolver::Advance,
668✔
1245
    R"(
1246
    Advance the solver by a single timestep.
1247

1248
    Notes
1249
    -----
1250
    You must call :meth:`Initialize` before calling :meth:`Advance` or
1251
    :meth:`Execute`.
1252
    )");
1253
  transient_solver.def(
668✔
1254
    "SetPreAdvanceCallback",
1255
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
668✔
1256
      &TransientSolver::SetPreAdvanceCallback),
1257
    R"(
1258
    Register a callback that runs before each advance within :meth:`Execute`.
1259

1260
    Parameters
1261
    ----------
1262
    callback : Optional[Callable[[], None]]
1263
        Function invoked before the solver advances a timestep. Pass None to clear.
1264
        If the callback modifies the timestep, the new value is used for the
1265
        upcoming step.
1266
    )");
1267
  transient_solver.def(
668✔
1268
    "SetPreAdvanceCallback",
1269
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
668✔
1270
      &TransientSolver::SetPreAdvanceCallback),
1271
    "Clear the PreAdvance callback by passing None.");
1272
  transient_solver.def(
668✔
1273
    "SetPostAdvanceCallback",
1274
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
668✔
1275
      &TransientSolver::SetPostAdvanceCallback),
1276
    R"(
1277
    Register a callback that runs after each advance within :meth:`Execute`.
1278

1279
    Parameters
1280
    ----------
1281
    callback : Optional[Callable[[], None]]
1282
        Function invoked after the solver advances a timestep. Pass None to clear.
1283
    )");
1284
  transient_solver.def(
668✔
1285
    "SetPostAdvanceCallback",
1286
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
668✔
1287
      &TransientSolver::SetPostAdvanceCallback),
1288
    "Clear the PostAdvance callback by passing None.");
1289
  slv.attr("BackwardEuler") = 1.0;
668✔
1290
  slv.attr("CrankNicolson") = 0.5;
1,336✔
1291
  // clang-format on
1292
}
668✔
1293

1294
// Wrap non-linear k-eigen solver
1295
void
1296
WrapNLKEigen(py::module& slv)
668✔
1297
{
1298
  // clang-format off
1299
  // non-linear k-eigen solver
1300
  auto non_linear_k_eigen_solver = py::class_<NonLinearKEigenSolver, std::shared_ptr<NonLinearKEigenSolver>,
668✔
1301
                                              Solver>(
1302
    slv,
1303
    "NonLinearKEigenSolver",
1304
    R"(
1305
    Non-linear k-eigenvalue solver.
1306

1307
    Wrapper of :cpp:class:`opensn::NonLinearKEigenSolver`.
1308
    )"
1309
  );
668✔
1310
  non_linear_k_eigen_solver.def(
1,336✔
1311
    py::init(
668✔
1312
      [](py::kwargs& params)
28✔
1313
      {
1314
        return NonLinearKEigenSolver::Create(kwargs_to_param_block(params));
28✔
1315
      }
1316
        ),
1317
    R"(
1318
    Construct a non-linear k-eigenvalue solver.
1319

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

1360
// Wrap power iteration solvers
1361
void
1362
WrapPIteration(py::module& slv)
668✔
1363
{
1364
  // clang-format off
1365
  // power iteration k-eigen solver
1366
  auto pi_k_eigen_solver = py::class_<PowerIterationKEigenSolver, std::shared_ptr<PowerIterationKEigenSolver>,
668✔
1367
                                      Solver>(
1368
    slv,
1369
    "PowerIterationKEigenSolver",
1370
    R"(
1371
    Power iteration k-eigenvalue solver.
1372

1373
    Wrapper of :cpp:class:`opensn::PowerIterationKEigenSolver`.
1374
    )"
1375
  );
668✔
1376
  pi_k_eigen_solver.def(
1,336✔
1377
    py::init(
668✔
1378
      [](py::kwargs& params)
147✔
1379
      {
1380
        return PowerIterationKEigenSolver::Create(kwargs_to_param_block(params));
147✔
1381
      }
1382
    ),
1383
    R"(
1384
    Construct a power iteration k-eigen solver.
1385

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

1412
// Wrap LBS solver
1413
void
1414
WrapDiscreteOrdinatesKEigenAcceleration(py::module& slv)
668✔
1415
{
1416
  // clang-format off
1417
  // discrete ordinates k-eigen acceleration base
1418
  auto acceleration = py::class_<DiscreteOrdinatesKEigenAcceleration,
668✔
1419
                                 std::shared_ptr<DiscreteOrdinatesKEigenAcceleration>>(
1420
    slv,
1421
    "DiscreteOrdinatesKEigenAcceleration",
1422
    R"(
1423
    Base class for discrete ordinates k-eigenvalue acceleration methods.
1424

1425
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesKEigenAcceleration`.
1426
    )"
1427
  );
668✔
1428
  // SCDSA acceleration
1429
  auto scdsa_acceleration = py::class_<SCDSAAcceleration,
668✔
1430
                                       std::shared_ptr<SCDSAAcceleration>,
1431
                                       DiscreteOrdinatesKEigenAcceleration>(
1432
    slv,
1433
    "SCDSAAcceleration",
1434
    R"(
1435
    Construct an SCDSA accelerator for the power iteration k-eigenvalue solver.
1436

1437
    Wrapper of :cpp:class:`opensn::SCDSAAcceleration`.
1438
    )"
1439
  );
668✔
1440
  scdsa_acceleration.def(
1,336✔
1441
    py::init(
668✔
1442
      [](py::kwargs& params)
8✔
1443
      {
1444
        return SCDSAAcceleration::Create(kwargs_to_param_block(params));
8✔
1445
      }
1446
    ),
1447
    R"(
1448
    SCDSA acceleration for the power iteration k-eigenvalue solver.
1449

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

1481
    Wrapper of :cpp:class:`opensn::SMMAcceleration`.
1482
    )"
1483
  );
668✔
1484
  smm_acceleration.def(
1,336✔
1485
    py::init(
668✔
1486
      [](py::kwargs& params)
4✔
1487
      {
1488
        return SMMAcceleration::Create(kwargs_to_param_block(params));
4✔
1489
      }
1490
    ),
1491
    R"(
1492
    SMM acceleration for the power iteration k-eigenvalue solver.
1493

1494
    Warnings
1495
    --------
1496
       SMM acceleration is **experimental** and should be used with caution!
1497
       SMM accleration only supports problems with isotropic scattering.
1498

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

1524
// Wrap the solver components of OpenSn
1525
void
1526
py_solver(py::module& pyopensn)
63✔
1527
{
1528
  py::module slv = pyopensn.def_submodule("solver", "Solver module.");
63✔
1529
  WrapProblem(slv);
63✔
1530
  WrapSolver(slv);
63✔
1531
  WrapLBS(slv);
63✔
1532
  WrapSteadyState(slv);
63✔
1533
  WrapTransient(slv);
63✔
1534
  WrapNLKEigen(slv);
63✔
1535
  WrapDiscreteOrdinatesKEigenAcceleration(slv);
63✔
1536
  WrapPIteration(slv);
63✔
1537
}
63✔
1538

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