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

Open-Sn / opensn / 25359736025

04 May 2026 04:34PM UTC coverage: 75.416% (-0.001%) from 75.417%
25359736025

push

github

web-flow
Merge pull request #1038 from quocdang1998/sycl-mod

Extend SYCL implementation for caribou and OpenSn

21817 of 28929 relevant lines covered (75.42%)

65645385.67 hits per line

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

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

32
namespace opensn
33
{
34

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

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

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

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

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

98
    Wrapper of :cpp:class:`opensn::LBSProblem`.
99
    )"
100
  );
674✔
101
  lbs_problem.def(
674✔
102
    "GetScalarFluxFieldFunction",
103
    [](LBSProblem& self, bool only_scalar_flux)
674✔
104
    {
105
      py::list field_function_list_per_group;
574✔
106
      for (unsigned int group = 0; group < self.GetNumGroups(); group++)
22,315✔
107
      {
108
        if (only_scalar_flux)
21,741✔
109
        {
110
          field_function_list_per_group.append(self.CreateScalarFluxFieldFunction(group, 0));
25,520✔
111
        }
112
        else
113
        {
114
          py::list field_function_list_per_moment;
8,981✔
115
          for (unsigned int moment = 0; moment < self.GetNumMoments(); moment++)
37,168✔
116
          {
117
            field_function_list_per_moment.append(self.CreateScalarFluxFieldFunction(group, moment));
56,374✔
118
          }
119
          field_function_list_per_group.append(field_function_list_per_moment);
8,981✔
120
        }
8,981✔
121
      }
122
      return field_function_list_per_group;
574✔
123
    },
×
124
    R"(
125
    Return scalar-flux or flux-moment field functions grouped by energy group.
126

127
    Parameters
128
    ----------
129
    only_scalar_flux : bool, default=True
130
        If True, returns only the zeroth moment (scalar flux) field function for each group.
131
        If False, returns all moment field functions for each group.
132

133
    Returns
134
    -------
135
    Union[List[pyopensn.fieldfunc.FieldFunctionGridBased], List[List[pyopensn.fieldfunc.FieldFunctionGridBased]]]
136
        If ``only_scalar_flux=True``:
137
        ``result[g]`` is the scalar-flux field function for group ``g``.
138

139
        If ``only_scalar_flux=False``:
140
        ``result[g][m]`` is the field function for group ``g`` and moment ``m``.
141

142
    Notes
143
    -----
144
    Field functions are created on demand from the current solver state.
145

146
    The returned field functions are snapshots of the solver state at creation time; they are not
147
    refreshed automatically if the solver state changes.
148

149
    They support ``CanUpdate()`` and ``Update()`` while their owning problem is still alive.
150
    Calling ``Update()`` explicitly refreshes the same field-function object from the solver's
151
    latest state.
152

153
    Calling ``GetScalarFluxFieldFunction(only_scalar_flux=False)`` creates all requested
154
    moments from the current ``phi`` iterate at the time of the call.
155

156
    In the nested form (``only_scalar_flux=False``), the moment index varies fastest
157
    within each group (inner index = moment, outer index = group).
158
    )",
159
    py::arg("only_scalar_flux") = true
674✔
160
  );
161
  lbs_problem.def(
1,348✔
162
    "CreateFieldFunction",
163
    static_cast<std::shared_ptr<FieldFunctionGridBased> (LBSProblem::*)(
1,348✔
164
      const std::string&, const std::string&, double)>(&LBSProblem::CreateFieldFunction),
165
    R"(
166
    Create a named scalar field function derived from a 1D XS or ``power``.
167

168
    Parameters
169
    ----------
170
    name : str
171
        Name to assign to the returned field function.
172
    xs_name : str
173
        Built-in 1D XS name, custom XS name, or the special value ``power``.
174
    power_normalization_target : float, default=-1.0
175
        If positive, scale the derived field function so that the raw power field would
176
        integrate to this total power.
177

178
    Notes
179
    -----
180
    The returned field function is created on demand from the current scalar-flux iterate.
181
    For ordinary XS names this computes ``sum_g xs[g] * phi_g`` at each node.
182

183
    The returned field function is a snapshot of the solver state at creation time; it is not
184
    refreshed automatically if the solver state changes. It supports ``CanUpdate()`` and
185
    ``Update()`` while its owning problem is still alive. Calling ``Update()`` explicitly
186
    recomputes the same field function from the solver's latest state.
187

188
    If ``xs_name == "power"``, the same power-generation formula used elsewhere by the solver
189
    is applied on demand.
190

191
    If ``power_normalization_target > 0``, the returned field function is scaled using the power
192
    implied by the current scalar flux. This scaling affects only the returned field function;
193
    it does not mutate the solver's internal ``phi`` vectors.
194

195
    The returned field function is a fresh object created for this call. It is not
196
    automatically updated by later solves or timesteps.
197
    )",
198
    py::arg("name"),
1,348✔
199
    py::arg("xs_name"),
674✔
200
    py::arg("power_normalization_target") = -1.0
674✔
201
  );
202
  lbs_problem.def(
674✔
203
    "GetTime",
204
    &LBSProblem::GetTime,
674✔
205
    R"(
206
    Get the current simulation time in seconds.
207
    )"
208
  );
209
  lbs_problem.def(
674✔
210
    "GetTimeStep",
211
    &LBSProblem::GetTimeStep,
674✔
212
    R"(
213
    Get the current timestep size.
214
    )"
215
  );
216
  lbs_problem.def(
674✔
217
    "ComputeFissionRate",
218
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
674✔
219
    {
220
      const std::vector<double>* phi_ptr = nullptr;
×
221
      if (scalar_flux_iterate == "old")
×
222
      {
223
        phi_ptr = &self.GetPhiOldLocal();
×
224
      }
225
      else if (scalar_flux_iterate == "new")
×
226
      {
227
        phi_ptr = &self.GetPhiNewLocal();
×
228
      }
229
      else
230
      {
231
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
232
      }
233
      return ComputeFissionRate(self, *phi_ptr);
×
234
    },
235
    R"(
236
    Computes the total fission rate.
237

238
    Parameters
239
    ----------
240
    scalar_flux_iterate : {'old', 'new'}
241
        Specifies which scalar flux vector to use in the calculation.
242
            - 'old': Use the previous scalar flux iterate.
243
            - 'new': Use the current scalar flux iterate.
244

245
    Returns
246
    -------
247
    float
248
        The total fission rate.
249

250
    Raises
251
    ------
252
    ValueError
253
        If `scalar_flux_iterate` is not 'old' or 'new'.
254
    )",
255
    py::arg("scalar_flux_iterate")
674✔
256
  );
257
  lbs_problem.def(
674✔
258
    "ComputeFissionProduction",
259
    [](LBSProblem& self, const std::string& scalar_flux_iterate)
1,470✔
260
    {
261
      const std::vector<double>* phi_ptr = nullptr;
796✔
262
      if (scalar_flux_iterate == "old")
796✔
263
      {
264
        phi_ptr = &self.GetPhiOldLocal();
44✔
265
      }
266
      else if (scalar_flux_iterate == "new")
752✔
267
      {
268
        phi_ptr = &self.GetPhiNewLocal();
752✔
269
      }
270
      else
271
      {
272
        throw std::invalid_argument("Unknown scalar_flux_iterate value: \"" + scalar_flux_iterate + "\".");
×
273
      }
274
      return ComputeFissionProduction(self, *phi_ptr);
796✔
275
    },
276
    R"(
277
    Computes the total fission production (nu*fission).
278

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

286
    Returns
287
    -------
288
    float
289
        The total fission production.
290

291
    Raises
292
    ------
293
    ValueError
294
        If `scalar_flux_iterate` is not 'old' or 'new'.
295
    )",
296
    py::arg("scalar_flux_iterate")
674✔
297
  );
298
  lbs_problem.def(
674✔
299
    "GetPhiOldLocal",
300
    [](LBSProblem& self)
722✔
301
    {
302
      return convert_vector(self.GetPhiOldLocal());
48✔
303
    },
304
    R"(
305
    Get the previous scalar flux iterate (local vector).
306

307
    Returns
308
    -------
309
    memoryview
310
        Memory view of the local old scalar flux vector.
311
    )"
312
  );
313
  lbs_problem.def(
674✔
314
    "GetPhiNewLocal",
315
    [](LBSProblem& self)
738✔
316
    {
317
      return convert_vector(self.GetPhiNewLocal());
64✔
318
    },
319
    R"(
320
    Get the current scalar flux iterate (local vector).
321

322
    Returns
323
    -------
324
    memoryview
325
        Memory view of the local new scalar flux vector.
326
    )"
327
  );
328
  lbs_problem.def(
674✔
329
    "WriteFluxMoments",
330
    [](LBSProblem& self, const std::string& file_base)
706✔
331
    {
332
      LBSSolverIO::WriteFluxMoments(self, file_base);
32✔
333
    },
334
    R"(
335
    Write flux moments to file.
336

337
    Parameters
338
    ----------
339
    file_base: str
340
        File basename.
341
    )",
342
    py::arg("file_base")
674✔
343
  );
344
  lbs_problem.def(
674✔
345
    "CreateAndWriteSourceMoments",
346
    [](LBSProblem& self, const std::string& file_base)
678✔
347
    {
348
      std::vector<double> source_moments = self.MakeSourceMomentsFromPhi();
4✔
349
      LBSSolverIO::WriteFluxMoments(self, file_base, source_moments);
4✔
350
    },
4✔
351
    R"(
352
    Write source moments from latest flux iterate to file.
353

354
    Parameters
355
    ----------
356
    file_base: str
357
        File basename.
358
    )",
359
    py::arg("file_base")
674✔
360
  );
361
  lbs_problem.def(
674✔
362
    "ReadFluxMomentsAndMakeSourceMoments",
363
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
674✔
364
    {
365
      auto ext_src_moments = self.GetExtSrcMomentsLocal();
×
366
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, ext_src_moments);
×
367
      self.SetExtSrcMomentsFrom(ext_src_moments);
×
368
      log.Log() << "Making source moments from flux file.";
×
369
      const auto temp_phi = self.GetPhiOldLocal();
×
370
      self.SetPhiOldFrom(self.GetExtSrcMomentsLocal());
×
371
      self.SetExtSrcMomentsFrom(self.MakeSourceMomentsFromPhi());
×
372
      self.SetPhiOldFrom(temp_phi);
×
373
    },
×
374
    R"(
375
    Read flux moments and compute corresponding source moments.
376

377
    Parameters
378
    ----------
379
    file_base: str
380
        File basename.
381
    single_file_flag: bool
382
        True if all flux moments are in a single file.
383
    )",
384
    py::arg("file_base"),
1,348✔
385
    py::arg("single_file_flag")
674✔
386
  );
387
  lbs_problem.def(
674✔
388
    "ReadSourceMoments",
389
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
678✔
390
    {
391
      auto ext_src_moments = self.GetExtSrcMomentsLocal();
4✔
392
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag, ext_src_moments);
4✔
393
      self.SetExtSrcMomentsFrom(ext_src_moments);
4✔
394
    },
4✔
395
    R"(
396
    Read source moments from file.
397

398
    Parameters
399
    ----------
400
    file_base: str
401
        File basename.
402
    single_file_flag: bool
403
        True if all source moments are in a single file.
404
    )",
405
    py::arg("file_base"),
1,348✔
406
    py::arg("single_file_flag")
674✔
407
  );
408
  lbs_problem.def(
674✔
409
    "ReadFluxMoments",
410
    [](LBSProblem& self, const std::string& file_base, bool single_file_flag)
674✔
411
    {
412
      LBSSolverIO::ReadFluxMoments(self, file_base, single_file_flag);
×
413
    },
414
    R"(
415
    Read flux moment data.
416

417
    Parameters
418
    ----------
419
    file_base: str
420
        File basename.
421
    single_file_flag: bool
422
        True if all flux moments are in a single file.
423
    )",
424
    py::arg("file_base"),
1,348✔
425
    py::arg("single_file_flag")
674✔
426
  );
427
  lbs_problem.def(
674✔
428
    "WriteAngularFluxes",
429
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
678✔
430
    {
431
      DiscreteOrdinatesProblemIO::WriteAngularFluxes(self, file_base);
4✔
432
    },
433
    R"(
434
    Write angular flux data to file.
435

436
    Parameters
437
    ----------
438
    file_base: str
439
        File basename.
440
    )",
441
    py::arg("file_base")
674✔
442
  );
443
  lbs_problem.def(
674✔
444
    "ReadAngularFluxes",
445
    [](DiscreteOrdinatesProblem& self, const std::string& file_base)
678✔
446
    {
447
      DiscreteOrdinatesProblemIO::ReadAngularFluxes(self, file_base);
4✔
448
    },
449
    R"(
450
    Read angular fluxes from file.
451

452
    Parameters
453
    ----------
454
    file_base: str
455
        File basename.
456
    )",
457
    py::arg("file_base")
674✔
458
  );
459
  lbs_problem.def(
674✔
460
    "SetPointSources",
461
    [](LBSProblem& self, py::kwargs& params)
674✔
462
    {
463
      for (auto [key, value] : params)
×
464
      {
465
        auto c_key = key.cast<std::string>();
×
466
        if (c_key == "clear_point_sources")
×
467
          self.ClearPointSources();
×
468
        else if (c_key == "point_sources")
×
469
        {
470
          auto sources = value.cast<py::list>();
×
471
          for (auto source : sources)
×
472
          {
473
            self.AddPointSource(source.cast<std::shared_ptr<PointSource>>());
×
474
          }
475
        }
×
476
        else
477
          throw std::runtime_error("Invalid argument provided to SetPointSources.\n");
×
478
      }
×
479
    },
×
480
    R"(
481
    Set or clear point sources.
482

483
    Parameters
484
    ----------
485
    clear_point_sources: bool, default=False
486
        If true, all current the point sources of the problem are deleted.
487
    point_sources: List[pyopensn.source.PointSource]
488
        List of new point sources to be added to the problem.
489
    )"
490
  );
491
  lbs_problem.def(
674✔
492
    "SetVolumetricSources",
493
    [](LBSProblem& self, py::kwargs& params)
710✔
494
    {
495
      for (auto [key, value] : params)
108✔
496
      {
497
        auto c_key = key.cast<std::string>();
36✔
498
        if (c_key == "clear_volumetric_sources")
36✔
499
          self.ClearVolumetricSources();
12✔
500
        else if (c_key == "volumetric_sources")
24✔
501
        {
502
          auto sources = value.cast<py::list>();
24✔
503
          for (auto source : sources)
72✔
504
          {
505
            self.AddVolumetricSource(source.cast<std::shared_ptr<VolumetricSource>>());
48✔
506
          }
507
        }
24✔
508
        else
509
          throw std::runtime_error("Invalid argument provided to SetVolumetricSources.\n");
×
510
      }
36✔
511
    },
36✔
512
    R"(
513
    Set or clear volumetric sources.
514

515
    Parameters
516
    ----------
517
    clear_volumetric_sources: bool, default=False
518
        If true, all current the volumetric sources of the problem are deleted.
519
    volumetric_sources: List[pyopensn.source.VolumetricSource]
520
        List of new volumetric sources to be added to the problem.
521
    )"
522
  );
523
  lbs_problem.def(
674✔
524
    "SetXSMap",
525
    [](LBSProblem& self, py::kwargs& params)
858✔
526
    {
527
      BlockID2XSMap xs_map;
184✔
528
      for (auto [key, value] : params)
552✔
529
      {
530
        auto c_key = key.cast<std::string>();
184✔
531
        if (c_key == "xs_map")
184✔
532
        {
533
          auto xs_entries = value.cast<py::list>();
184✔
534
          for (auto entry : xs_entries)
552✔
535
          {
536
            InputParameters xs_entry_pars = LBSProblem::GetXSMapEntryBlock();
184✔
537
            xs_entry_pars.AssignParameters(pyobj_to_param_block("", entry.cast<py::dict>()));
184✔
538
            const auto& block_ids =
184✔
539
              xs_entry_pars.GetParam("block_ids").GetVectorValue<unsigned int>();
184✔
540
            auto xs = xs_entry_pars.GetSharedPtrParam<MultiGroupXS>("xs");
184✔
541
            for (const auto& block_id : block_ids)
368✔
542
              xs_map[block_id] = xs;
184✔
543
          }
184✔
544
        }
184✔
545
        else
546
          throw std::runtime_error("Invalid argument provided to SetXSMap.\n");
×
547
      }
184✔
548
      self.SetBlockID2XSMap(xs_map);
184✔
549
    },
184✔
550
    R"(
551
    Replace the block-id to cross-section map.
552

553
    Parameters
554
    ----------
555
    xs_map: List[Dict]
556
        A list of block-id to cross-section mapping dictionaries. Each dictionary supports:
557
          - block_ids: List[int] (required)
558
              Mesh block ids to associate with the cross section.
559
          - xs: pyopensn.xs.MultiGroupXS (required)
560
              Cross section object.
561

562
    Notes
563
    -----
564
    The problem is refreshed immediately after replacing the map. Material metadata,
565
    precursor storage, GPU carriers, and derived solver state owned by the concrete
566
    problem are rebuilt for the new cross sections.
567

568
    If ``options.use_precursors=True``, this flag remains active across XS-map
569
    changes even when the current map has no precursor-bearing material. In that
570
    case delayed-neutron source terms are simply inactive until a later map provides
571
    precursor data again.
572

573
    Existing precursor concentrations are remapped by local cell and precursor-family
574
    index. When the new material for a cell has fewer precursor families, extra old
575
    families are dropped. When it has more families, newly introduced families are
576
    initialized to zero. If a cell changes through a material with zero precursors,
577
    the precursor history for that cell is discarded and any later reintroduced
578
    precursor families start from zero.
579

580
    If any fissionable material in the new map contains delayed-neutron precursor
581
    data and ``options.use_precursors=True``, all fissionable materials in the map
582
    must contain precursor data. Non-fissionable materials may have zero precursors.
583

584
    Forward/adjoint mode toggles via :meth:`LBSProblem.SetAdjoint` do not change this map.
585
    The ``MultiGroupXS`` objects themselves are mutable and shared by pointer. If the same
586
    ``MultiGroupXS`` handle is shared across multiple problems, toggling adjoint mode in one
587
    problem also changes the transport mode seen by the others.
588
    )"
589
  );
590
  lbs_problem.def(
674✔
591
    "ZeroPhi",
592
    [](LBSProblem& self)
674✔
593
    {
594
      self.ZeroPhi();
×
595
    },
596
    R"(
597
    Zero the scalar-flux vectors (``phi_old`` and ``phi_new``) in-place.
598
    )"
599
  );
600
  lbs_problem.def(
674✔
601
    "SetAdjoint",
602
    [](LBSProblem& self, bool adjoint)
674✔
603
    {
604
      self.SetAdjoint(adjoint);
24✔
605
    },
606
    py::arg("adjoint") = true,
674✔
607
    R"(
608
    Set forward/adjoint transport mode.
609

610
    Parameters
611
    ----------
612
    adjoint: bool, default=True
613
        ``True`` enables adjoint mode and ``False`` enables forward mode.
614

615
    Notes
616
    -----
617
    This is one of two supported mode-setting paths in Python:
618
      1. ``options={'adjoint': ...}`` in the problem constructor.
619
      2. ``SetAdjoint(...)`` (this method).
620

621
    If this changes mode, OpenSn performs a full mode-transition reset:
622
      - Materials are reinitialized in the selected mode.
623
      - Point and volumetric sources are cleared.
624
      - Boundary conditions are cleared.
625
      - Scalar and angular flux vectors are zeroed.
626

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

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

633
    After a mode change, reapply the desired driving terms before solving, typically:
634
      - :meth:`LBSProblem.SetPointSources`
635
      - :meth:`LBSProblem.SetVolumetricSources`
636
      - :meth:`DiscreteOrdinatesProblem.SetBoundaryOptions`
637

638
    This routine is intentionally destructive with respect to source/boundary/flux state
639
    to avoid hidden coupling between forward and adjoint setups.
640
    )"
641
  );
642
  lbs_problem.def(
674✔
643
    "SetForward",
644
    &LBSProblem::SetForward,
674✔
645
    R"(
646
    Set forward transport mode.
647

648
    Equivalent to ``SetAdjoint(False)``.
649
    )"
650
  );
651
  lbs_problem.def(
674✔
652
    "IsAdjoint",
653
    &LBSProblem::IsAdjoint,
674✔
654
    R"(
655
    Return ``True`` if the problem is in adjoint mode, otherwise ``False``.
656
    )"
657
  );
658
  lbs_problem.def(
674✔
659
    "IsTimeDependent",
660
    &LBSProblem::IsTimeDependent,
674✔
661
    R"(
662
    Return ``True`` if the problem is in time-dependent mode, otherwise ``False``.
663
    )"
664
  );
665

666
  // discrete ordinate solver
667
  auto do_problem = py::class_<DiscreteOrdinatesProblem, std::shared_ptr<DiscreteOrdinatesProblem>,
674✔
668
                               LBSProblem>(
669
    slv,
670
    "DiscreteOrdinatesProblem",
671
    R"(
672
    Base class for discrete ordinates problems in Cartesian geometry.
673

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

678
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesProblem`.
679
    )"
680
  );
674✔
681
  do_problem.def(
1,348✔
682
    py::init(
674✔
683
      [](py::kwargs& params)
596✔
684
      {
685
        return DiscreteOrdinatesProblem::Create(kwargs_to_param_block(params));
596✔
686
      }
687
    ),
688
    R"(
689
    Construct a discrete ordinates problem with Cartesian geometry.
690

691
    Parameters
692
    ----------
693
    mesh : MeshContinuum
694
        The spatial mesh.
695
    num_groups : int
696
        The total number of energy groups.
697
    groupsets : List[Dict], default=[]
698
        A list of input parameter blocks, each block provides the iterative properties for a
699
        groupset. Each dictionary supports:
700
          - groups_from_to: List[int] (required)
701
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
702
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
703
              Handle to an angular quadrature.
704
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
705
              Angle aggregation method to use during sweeping.
706
          - angle_aggregation_num_subsets: int, default=1
707
              Number of angle subsets used for aggregation.
708
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
709
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
710
              Iterative method used for inner linear solves.
711
          - l_abs_tol: float, default=1.0e-6
712
              Inner linear solver absolute residual tolerance.
713
          - l_max_its: int, default=200
714
              Inner linear solver maximum iterations.
715
          - gmres_restart_interval: int, default=30
716
              GMRES restart interval, if GMRES is used.
717
          - allow_cycles: bool, default=True
718
              Whether cyclic dependencies are allowed in sweeps.
719
          - apply_wgdsa: bool, default=False
720
              Enable within-group DSA for this groupset.
721
          - wgdsa_l_abs_tol: float, default=1.0e-4
722
              WGDSA linear absolute tolerance.
723
          - wgdsa_l_max_its: int, default=30
724
              WGDSA maximum iterations.
725
          - wgdsa_verbose: bool, default=False
726
              Verbose WGDSA output.
727
          - wgdsa_petsc_options: str, default=''
728
              PETSc options string for the WGDSA solver.
729
          - apply_tgdsa: bool, default=False
730
              Enable two-grid DSA for this groupset.
731
          - tgdsa_l_abs_tol: float, default=1.0e-4
732
              TGDSA linear absolute tolerance.
733
          - tgdsa_l_max_its: int, default=30
734
              TGDSA maximum iterations.
735
          - tgdsa_verbose: bool, default=False
736
              Verbose TGDSA output.
737
          - tgdsa_petsc_options: str, default=''
738
              PETSc options string for the TGDSA solver.
739
    xs_map : List[Dict], default=[]
740
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
741
          - block_ids: List[int] (required)
742
              Mesh block IDs to associate with the cross section.
743
          - xs: pyopensn.xs.MultiGroupXS (required)
744
              Cross-section object to assign to the specified blocks.
745
    boundary_conditions: List[Dict], default=[]
746
        A list containing tables for each boundary specification. Each dictionary supports:
747
          - name: str (required)
748
              Boundary name that identifies the specific boundary.
749
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
750
              Boundary type specification.
751
          - group_strength: List[float], optional
752
              Required when ``type='isotropic'``. Isotropic strength per group.
753
          - function: AngularFluxFunction, optional
754
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
755
    point_sources: List[pyopensn.source.PointSource], default=[]
756
        A list of point sources.
757
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
758
        A list of volumetric sources.
759
    options : Dict, default={}
760
        A block of optional configuration parameters, including:
761
          - max_mpi_message_size: int, default=32768
762
          - restart_writes_enabled: bool, default=False
763
          - write_delayed_psi_to_restart: bool, default=True
764
          - read_restart_path: str, default=''
765
          - write_restart_path: str, default=''
766
          - write_restart_time_interval: int, default=0
767
            (must be 0 or >=30)
768
          - use_precursors: bool, default=True
769
            Enable delayed-neutron precursor treatment. This is treated as user intent and remains
770
            active across later ``SetXSMap`` calls, even if the current XS map temporarily contains
771
            no precursor-bearing material. When XS maps are swapped, existing precursor
772
            concentrations are remapped by cell and precursor-family index; new families start at
773
            zero and removed families are discarded. If any fissionable material in the active map
774
            has precursor data and ``use_precursors=True``, all fissionable materials in that map
775
            must have precursor data. Non-fissionable materials may have zero precursors.
776
          - use_source_moments: bool, default=False
777
          - save_angular_flux: bool, default=False
778
            Store angular flux state (`psi`) for transient mode, angular-flux
779
            field functions, and angular-flux I/O.
780
          - adjoint: bool, default=False
781
          - verbose_inner_iterations: bool, default=True
782
            Print inner iteration details, including WGS and AGS iterations.
783
          - verbose_outer_iterations: bool, default=True
784
            Print outer solver progress, including PI/NLKE iterations and transient steps.
785
          - max_ags_iterations: int, default=100
786
          - ags_tolerance: float, default=1.0e-6
787
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
788
          - power_default_kappa: float, default=3.20435e-11
789
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
790
          - field_function_prefix: str, default=''
791
        These options are applied at problem creation.
792
    sweep_type : str, default="AAH"
793
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
794
    time_dependent : bool, default=False
795
        If true, the problem starts in time-dependent mode. Otherwise it starts in
796
        steady-state mode. Requires ``options.save_angular_flux=True``.
797
    use_gpus : bool, default=False
798
        A flag specifying whether GPU acceleration is used for the sweep. Currently, only ``AAH`` is
799
        supported.
800
    )"
801
  );
802
  do_problem.def(
674✔
803
    "SetTimeDependentMode",
804
    &DiscreteOrdinatesProblem::SetTimeDependentMode,
674✔
805
    R"(
806
    Set the problem to time-dependent mode.
807

808
    Notes
809
    -----
810
    Switch problem from steady-state to time-dependent mode. This updates problem
811
    internals (sweep chunk mode and source-function) while preserving user boundary
812
    conditions and fixed sources.
813

814
    Requires ``options.save_angular_flux=True`` at problem creation.
815
    )"
816
  );
817
  do_problem.def(
674✔
818
    "SetSteadyStateMode",
819
    &DiscreteOrdinatesProblem::SetSteadyStateMode,
674✔
820
    R"(
821
    Set the problem to steady-state mode.
822

823
    Notes
824
    -----
825
    Switch problem from time-dependent to steady-state mode. This updates problem
826
    internals (sweep chunk mode and source-function) while preserving user boundary
827
    conditions and fixed sources.
828
    )"
829
  );
830
  do_problem.def(
674✔
831
    "IsTimeDependent",
832
    &DiscreteOrdinatesProblem::IsTimeDependent,
674✔
833
    R"(
834
    Return ``True`` if the problem is currently in time-dependent mode.
835
    )"
836
  );
837
  do_problem.def(
674✔
838
    "SetBoundaryOptions",
839
    [](DiscreteOrdinatesProblem& self, py::kwargs& params)
678✔
840
    {
841
      for (auto [key, value] : params)
12✔
842
      {
843
        auto c_key = key.cast<std::string>();
4✔
844
        if (c_key == "clear_boundary_conditions")
4✔
845
          self.ClearBoundaries();
×
846
        else if (c_key == "boundary_conditions")
4✔
847
        {
848
          auto boundaries = value.cast<py::list>();
4✔
849
          for (auto boundary : boundaries)
24✔
850
          {
851
            InputParameters input = DiscreteOrdinatesProblem::GetBoundaryOptionsBlock();
16✔
852
            input.AssignParameters(pyobj_to_param_block("", boundary.cast<py::dict>()));
16✔
853
            self.SetBoundaryOptions(input);
16✔
854
          }
16✔
855
        }
4✔
856
        else
857
          throw std::runtime_error("Invalid argument provided to SetBoundaryOptions.\n");
×
858
      }
4✔
859
    },
4✔
860
    R"(
861
    Set or clear boundary conditions.
862

863
    Parameters
864
    ----------
865
    clear_boundary_conditions: bool, default=False
866
        If true, all current boundary conditions are deleted.
867
    boundary_conditions: List[Dict]
868
        A list of boundary condition dictionaries. Each dictionary supports:
869
          - name: str (required)
870
              Boundary name that identifies the specific boundary.
871
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
872
              Boundary type specification.
873
          - group_strength: List[float], optional
874
              Required when ``type='isotropic'``. Isotropic strength per group.
875
          - function: AngularFluxFunction, optional
876
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
877

878
    Notes
879
    -----
880
    Mode transitions via :meth:`LBSProblem.SetAdjoint` clear all boundary conditions.
881
    Reapply boundaries with this method before solving in the new mode.
882
    )"
883
  );
884
  do_problem.def(
674✔
885
    "ZeroPsi",
886
    [](DiscreteOrdinatesProblem& self)
674✔
887
    {
888
      self.ZeroPsi();
×
889
    },
890
    R"(
891
    Zero the angular-flux storage.
892
    )"
893
  );
894
  do_problem.def(
674✔
895
    "GetPsi",
896
    [](DiscreteOrdinatesProblem& self)
682✔
897
    {
898
      const auto& psi = self.GetPsiNewLocal();
8✔
899
      py::list psi_list;
8✔
900
      for (const auto& vec : psi)
16✔
901
      {
902
        auto array = py::array_t<double>(static_cast<py::ssize_t>(vec.size()),
8✔
903
                                         vec.data(),
904
                                         py::cast(self));
8✔
905
        psi_list.append(array);
8✔
906
      }
8✔
907
      return psi_list;
8✔
908
    },
×
909
    R"(
910
    Return psi as a list of NumPy arrays (float64), using zero-copy views into the
911
    underlying data.
912
    )"
913
  );
914
  do_problem.def(
674✔
915
    "GetAngularFieldFunctionList",
916
    [](DiscreteOrdinatesProblem& self, py::list groups, py::list angles)
674✔
917
    {
918
      std::vector<unsigned int> group_ids;
×
919
      std::vector<size_t> angle_ids;
×
920
      group_ids.reserve(groups.size());
×
921
      angle_ids.reserve(angles.size());
×
922

923
      for (py::handle g : groups)
×
924
        group_ids.push_back(g.cast<unsigned int>());
×
925
      for (py::handle a : angles)
×
926
        angle_ids.push_back(a.cast<size_t>());
×
927

928
      auto ff_list = self.CreateAngularFluxFieldFunctionList(group_ids, angle_ids);
×
929
      py::list out;
×
930
      for (const auto& ff : ff_list)
×
931
        out.append(ff);
×
932
      return out;
×
933
    },
×
934
    R"(
935
    Create field functions for selected angular flux components.
936

937
    Note: You must enable angular flux storage (``save_angular_flux=True``) in
938
    the problem options. For transient problems this is required. Otherwise
939
    the returned field functions will remain zero.
940

941
    Example
942
    -------
943
    .. code-block:: python
944

945
       solver.Initialize()
946
       solver.Execute()
947
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
948

949
    For transient/time-dependent runs, each field function is still a snapshot. Either call
950
    this after each timestep to create a fresh object or keep the returned object and call
951
    ``Update()`` after each timestep before exporting or interpolating it. Two common patterns
952
    are:
953

954
    1) Use ``TransientSolver.Execute()`` with a post-advance callback:
955

956
    .. code-block:: python
957

958
       solver = TransientSolver(problem=phys)
959
       solver.Initialize()
960
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
961

962
       def post_advance():
963
           for ff in ang_ff:
964
               ff.Update()
965
           FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
966

967
       solver.SetPostAdvanceCallback(post_advance)
968
       solver.Execute()
969

970
    2) Use a custom Python loop with ``TransientSolver.Advance()``:
971

972
    .. code-block:: python
973

974
       solver = TransientSolver(problem=phys)
975
       solver.Initialize()
976
       ang_ff = phys.GetAngularFieldFunctionList(groups=[0], angles=[0])
977
       for _ in range(num_steps):
978
           solver.Advance()
979
           for ff in ang_ff:
980
               ff.Update()
981
           FieldFunctionGridBased.ExportMultipleToPVTU(ang_ff, "angular_flux_t")
982

983
    Parameters
984
    ----------
985
    groups : List[int]
986
        Global group indices to export.
987
    angles : List[int]
988
        Angle indices within the groupset quadrature for each group.
989

990
    Returns
991
    -------
992
    List[pyopensn.fieldfunc.FieldFunctionGridBased]
993
        Field functions for the requested ``(group, angle)`` pairs. Each returned field function
994
        is a snapshot, but supports ``CanUpdate()`` and ``Update()`` while its owning problem is
995
        alive.
996
    )",
997
    py::arg("groups"),
1,348✔
998
    py::arg("angles")
674✔
999
  );
1000
  do_problem.def(
674✔
1001
    "ComputeLeakage",
1002
    [](DiscreteOrdinatesProblem& self, py::list bnd_names)
713✔
1003
    {
1004
      auto grid = self.GetGrid();
39✔
1005
      // get the supported boundaries
1006
      std::map<std::string, std::uint64_t> allowed_bd_names = grid->GetBoundaryNameMap();
39✔
1007
      std::map<std::uint64_t, std::string> allowed_bd_ids = grid->GetBoundaryIDMap();
39✔
1008
      const auto coord_sys = grid->GetCoordinateSystem();
39✔
1009
      const auto mesh_type = grid->GetType();
39✔
1010
      const auto dim = grid->GetDimension();
39✔
1011
      // get the boundaries to parse, preserving user order
1012
      std::vector<std::uint64_t> bndry_ids;
39✔
1013
      if (bnd_names.size() > 1)
39✔
1014
      {
1015
        for (py::handle name : bnd_names)
184✔
1016
        {
1017
          auto sname = name.cast<std::string>();
112✔
1018
          if (coord_sys == CoordinateSystemType::CYLINDRICAL && dim == 2)
112✔
1019
          {
1020
            if (sname == "xmin" || sname == "xmax" || sname == "ymin" || sname == "ymax")
24✔
1021
              throw std::runtime_error("ComputeLeakage: Boundary name '" + sname +
×
1022
                                       "' is invalid for cylindrical orthogonal meshes. "
1023
                                       "Use rmin, rmax, zmin, zmax.");
×
1024

1025
            if (mesh_type == MeshType::ORTHOGONAL)
24✔
1026
            {
1027
              if (sname == "rmin") sname = "xmin";
24✔
1028
              else if (sname == "rmax") sname = "xmax";
24✔
1029
              else if (sname == "zmin") sname = "ymin";
16✔
1030
              else if (sname == "zmax") sname = "ymax";
8✔
1031
            }
1032
          }
1033
          bndry_ids.push_back(allowed_bd_names.at(sname));
112✔
1034
        }
112✔
1035
      }
1036
      else
1037
      {
1038
        bndry_ids = self.GetGrid()->GetUniqueBoundaryIDs();
9✔
1039
      }
1040
      // compute the leakage
1041
      std::map<std::uint64_t, std::vector<double>> leakage = ComputeLeakage(self, bndry_ids);
39✔
1042
      const bool rz_ortho = (coord_sys == CoordinateSystemType::CYLINDRICAL &&
78✔
1043
                             mesh_type == MeshType::ORTHOGONAL && dim == 2);
39✔
1044

1045
      auto to_rz_name = [](const std::string& name)
63✔
1046
      {
1047
        if (name == "xmin") return std::string("rmin");
24✔
1048
        if (name == "xmax") return std::string("rmax");
24✔
1049
        if (name == "ymin") return std::string("zmin");
16✔
1050
        if (name == "ymax") return std::string("zmax");
8✔
1051
        return name;
×
1052
      };
1053

1054
      // convert result to native Python
1055
      py::dict result;
39✔
1056
      for (const auto& bndry_id : bndry_ids)
157✔
1057
      {
1058
        const auto it = leakage.find(bndry_id);
118✔
1059
        if (it == leakage.end())
118✔
1060
          continue;
×
1061
        // construct numpy array and copy contents
1062
        const auto& grp_wise_leakage = it->second;
118✔
1063
        py::array_t<double> np_vector(py::ssize_t(grp_wise_leakage.size()));
118✔
1064
        auto buffer = np_vector.request();
118✔
1065
        auto *np_vector_data = static_cast<double*>(buffer.ptr);
118✔
1066
        std::copy(grp_wise_leakage.begin(), grp_wise_leakage.end(), np_vector_data);
118✔
1067
        std::string name = allowed_bd_ids.at(bndry_id);
118✔
1068
        if (rz_ortho)
118✔
1069
          name = to_rz_name(name);
24✔
1070
        result[py::str(name)] = std::move(np_vector);
236✔
1071
      }
118✔
1072

1073
      return result;
39✔
1074
    },
78✔
1075
    R"(
1076
    Compute leakage for the problem.
1077

1078
    Parameters
1079
    ----------
1080
    bnd_names : List[str]
1081
        A list of boundary names for which leakage should be computed.
1082

1083
    Returns
1084
    -------
1085
    Dict[str, numpy.ndarray]
1086
        A dictionary mapping boundary names to group-wise leakage vectors.
1087
        Each array contains the outgoing angular flux (per group) integrated over
1088
        the corresponding boundary surface.
1089

1090
    Raises
1091
    ------
1092
    RuntimeError
1093
        If `save_angular_flux` option was not enabled during problem setup.
1094

1095
    ValueError
1096
        If one or more boundary ids are not present on the current mesh.
1097
    )",
1098
    py::arg("bnd_names")
674✔
1099
  );
1100

1101
  // discrete ordinates curvilinear problem
1102
  auto do_curvilinear_problem = py::class_<DiscreteOrdinatesCurvilinearProblem,
674✔
1103
                                           std::shared_ptr<DiscreteOrdinatesCurvilinearProblem>,
1104
                                           DiscreteOrdinatesProblem>(
1105
    slv,
1106
    "DiscreteOrdinatesCurvilinearProblem",
1107
    R"(
1108
    Base class for discrete ordinates problems in curvilinear geometry.
1109

1110
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesCurvilinearProblem`.
1111
    )"
1112
  );
674✔
1113
  do_curvilinear_problem.def(
1,348✔
1114
    py::init(
674✔
1115
      [](py::kwargs& params)
80✔
1116
      {
1117
        return DiscreteOrdinatesCurvilinearProblem::Create(kwargs_to_param_block(params));
80✔
1118
      }
1119
    ),
1120
    R"(
1121
    Construct a discrete ordinates problem for curvilinear geometry.
1122

1123
    Warnings
1124
    --------
1125
       DiscreteOrdinatesCurvilinearProblem is **experimental** and should be used with caution!
1126

1127
    Parameters
1128
    ----------
1129
    mesh : MeshContinuum
1130
        The spatial mesh.
1131
    coord_system : int
1132
        Coordinate system to use. Must be set to 2 (cylindrical coordinates).
1133
    num_groups : int
1134
        The total number of energy groups.
1135
    groupsets : list of dict
1136
        A list of input parameter blocks, each block provides the iterative properties for a
1137
        groupset. Each dictionary supports:
1138
          - groups_from_to: List[int] (required)
1139
              Two-entry list with the first and last group id for the groupset, e.g. ``[0, 3]``.
1140
          - angular_quadrature: pyopensn.aquad.AngularQuadrature, optional
1141
              Handle to an angular quadrature.
1142
          - angle_aggregation_type: {'polar', 'single', 'azimuthal'}, default='polar'
1143
              Angle aggregation method to use during sweeping.
1144
          - angle_aggregation_num_subsets: int, default=1
1145
              Number of angle subsets used for aggregation.
1146
          - inner_linear_method: {'classic_richardson', 'petsc_richardson',
1147
            'petsc_gmres', 'petsc_bicgstab'}, default='petsc_richardson'
1148
              Iterative method used for inner linear solves.
1149
          - l_abs_tol: float, default=1.0e-6
1150
              Inner linear solver absolute residual tolerance.
1151
          - l_max_its: int, default=200
1152
              Inner linear solver maximum iterations.
1153
          - gmres_restart_interval: int, default=30
1154
              GMRES restart interval, if GMRES is used.
1155
          - allow_cycles: bool, default=True
1156
              Whether cyclic dependencies are allowed in sweeps.
1157
          - apply_wgdsa: bool, default=False
1158
              Enable within-group DSA for this groupset.
1159
          - wgdsa_l_abs_tol: float, default=1.0e-4
1160
              WGDSA linear absolute tolerance.
1161
          - wgdsa_l_max_its: int, default=30
1162
              WGDSA maximum iterations.
1163
          - wgdsa_verbose: bool, default=False
1164
              Verbose WGDSA output.
1165
          - wgdsa_petsc_options: str, default=''
1166
              PETSc options string for the WGDSA solver.
1167
          - apply_tgdsa: bool, default=False
1168
              Enable two-grid DSA for this groupset.
1169
          - tgdsa_l_abs_tol: float, default=1.0e-4
1170
              TGDSA linear absolute tolerance.
1171
          - tgdsa_l_max_its: int, default=30
1172
              TGDSA maximum iterations.
1173
          - tgdsa_verbose: bool, default=False
1174
              Verbose TGDSA output.
1175
          - tgdsa_petsc_options: str, default=''
1176
    xs_map : list of dict
1177
        A list of mappings from block ids to cross-section definitions. Each dictionary supports:
1178
          - block_ids: List[int] (required)
1179
              Mesh block IDs to associate with the cross section.
1180
          - xs: pyopensn.xs.MultiGroupXS (required)
1181
              Cross-section object to assign to the specified blocks.
1182
    boundary_conditions: List[Dict], default=[]
1183
        A list containing tables for each boundary specification. Each dictionary supports:
1184
          - name: str (required)
1185
              Boundary name that identifies the specific boundary.
1186
          - type: {'vacuum', 'isotropic', 'reflecting', 'arbitrary'} (required)
1187
              Boundary type specification.
1188
          - group_strength: List[float], optional
1189
              Required when ``type='isotropic'``. Isotropic strength per group.
1190
          - function: AngularFluxFunction, optional
1191
              Required when ``type='arbitrary'``. Callable that returns incoming angular flux.
1192
    point_sources: List[pyopensn.source.PointSource], default=[]
1193
        A list of point sources.
1194
    volumetric_sources: List[pyopensn.source.VolumetricSource], default=[]
1195
        A list of volumetric sources.
1196
    options : dict, optional
1197
        A block of optional configuration parameters applied at problem creation, including:
1198
          - max_mpi_message_size: int, default=32768
1199
          - restart_writes_enabled: bool, default=False
1200
          - write_delayed_psi_to_restart: bool, default=True
1201
          - read_restart_path: str, default=''
1202
          - write_restart_path: str, default=''
1203
          - write_restart_time_interval: int, default=0
1204
          - use_precursors: bool, default=True
1205
            Enable delayed-neutron precursor treatment. This remains active across later
1206
            ``SetXSMap`` calls, even if the current map temporarily has no precursor-bearing
1207
            material. When XS maps change, precursor concentrations are remapped by cell and
1208
            precursor-family index; newly introduced families start at zero and removed families
1209
            are discarded.
1210
          - use_source_moments: bool, default=False
1211
          - save_angular_flux: bool, default=False
1212
            Store angular flux state (`psi`) for transient mode, angular-flux
1213
            field functions, and angular-flux I/O.
1214
          - verbose_inner_iterations: bool, default=True
1215
            Print inner iteration details, including WGS and AGS iterations.
1216
          - verbose_outer_iterations: bool, default=True
1217
            Print outer solver progress, including PI/NLKE iterations and transient steps.
1218
          - max_ags_iterations: int, default=100
1219
          - ags_tolerance: float, default=1.0e-6
1220
          - ags_convergence_check: {'l2', 'pointwise'}, default='l2'
1221
          - power_default_kappa: float, default=3.20435e-11
1222
          - field_function_prefix_option: {'prefix', 'solver_name'}, default='prefix'
1223
          - field_function_prefix: str, default=''
1224
    sweep_type : str, optional
1225
        The sweep type to use. Must be one of `AAH` or `CBC`. Defaults to `AAH`.
1226
        If ``time_dependent=True``, ``options.save_angular_flux=True`` is required.
1227
    )"
1228
  );
1229
}
674✔
1230

1231
// Wrap steady-state solver
1232
void
1233
WrapSteadyState(py::module& slv)
674✔
1234
{
1235
  const auto balance_table_to_dict = [](const BalanceTable& table)
693✔
1236
  {
1237
    py::dict values;
19✔
1238
    values["absorption_rate"] = table.absorption_rate;
19✔
1239
    values["production_rate"] = table.production_rate;
19✔
1240
    values["inflow_rate"] = table.inflow_rate;
19✔
1241
    values["outflow_rate"] = table.outflow_rate;
19✔
1242
    values["balance"] = table.balance;
19✔
1243
    if (table.initial_inventory.has_value())
19✔
1244
      values["initial_inventory"] = table.initial_inventory.value();
×
1245
    if (table.final_inventory.has_value())
19✔
1246
      values["final_inventory"] = table.final_inventory.value();
×
1247
    if (table.predicted_inventory_change.has_value())
19✔
1248
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1249
    if (table.actual_inventory_change.has_value())
19✔
1250
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1251
    if (table.inventory_residual.has_value())
19✔
1252
      values["inventory_residual"] = table.inventory_residual.value();
×
1253
    return values;
19✔
1254
  };
×
1255

1256
  // clang-format off
1257
  // steady state solver
1258
  auto steady_state_solver = py::class_<SteadyStateSourceSolver, std::shared_ptr<SteadyStateSourceSolver>,
674✔
1259
                                        Solver>(
1260
    slv,
1261
    "SteadyStateSourceSolver",
1262
    R"(
1263
    Steady state solver.
1264

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

1278
    Parameters
1279
    ----------
1280
    problem : pyopensn.solver.LBSProblem
1281
        Existing LBSProblem instance.
1282

1283
    Notes
1284
    -----
1285
    If ``problem.options.read_restart_path`` is set, restart data is read
1286
    during :meth:`Initialize`. If ``problem.options.restart_writes_enabled`` is
1287
    true, a restart dump is written after :meth:`Execute` completes.
1288
    problem : pyopensn.solver.DiscreteOrdinatesProblem
1289
        Existing discrete ordinates problem instance.
1290
    )"
1291
  );
1292
  steady_state_solver.def(
674✔
1293
    "ComputeBalanceTable",
1294
    [balance_table_to_dict](const SteadyStateSourceSolver& self)
674✔
1295
    {
1296
      return balance_table_to_dict(self.ComputeBalanceTable());
19✔
1297
    },
1298
    R"(
1299
    Compute and return the global balance table using the solver's normalization.
1300
    This is a collective operation and must be called on all ranks.
1301

1302
    Returns
1303
    -------
1304
    dict
1305
        Dictionary with the following entries:
1306

1307
        - ``absorption_rate``:
1308
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1309
          groups and the full domain.
1310
        - ``production_rate``:
1311
          Global volumetric production/source rate used by the solver,
1312
          approximately ``integral Q dV`` summed over groups and the full domain.
1313
        - ``inflow_rate``:
1314
          Global incoming boundary contribution integrated over incoming
1315
          angular flux on boundaries.
1316
        - ``outflow_rate``:
1317
          Global outgoing boundary contribution accumulated from face outflow
1318
          tallies.
1319
        - ``balance``:
1320
          Rate balance,
1321
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1322

1323
    Notes
1324
    -----
1325
    This solver applies no extra normalization to the balance table.
1326
    )"
1327
  );
1328
  // clang-format on
1329
}
674✔
1330

1331
// Wrap transient solver
1332
void
1333
WrapTransient(py::module& slv)
674✔
1334
{
1335
  const auto balance_table_to_dict = [](const BalanceTable& table)
674✔
1336
  {
1337
    py::dict values;
×
1338
    values["absorption_rate"] = table.absorption_rate;
×
1339
    values["production_rate"] = table.production_rate;
×
1340
    values["inflow_rate"] = table.inflow_rate;
×
1341
    values["outflow_rate"] = table.outflow_rate;
×
1342
    values["balance"] = table.balance;
×
1343
    if (table.initial_inventory.has_value())
×
1344
      values["initial_inventory"] = table.initial_inventory.value();
×
1345
    if (table.final_inventory.has_value())
×
1346
      values["final_inventory"] = table.final_inventory.value();
×
1347
    if (table.predicted_inventory_change.has_value())
×
1348
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1349
    if (table.actual_inventory_change.has_value())
×
1350
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1351
    if (table.inventory_residual.has_value())
×
1352
      values["inventory_residual"] = table.inventory_residual.value();
×
1353
    return values;
×
1354
  };
×
1355
  // clang-format off
1356
  auto transient_solver =
674✔
1357
    py::class_<TransientSolver, std::shared_ptr<TransientSolver>, Solver>(
1358
      slv,
1359
      "TransientSolver",
1360
      R"(
1361
      Transient solver.
1362

1363
      Wrapper of :cpp:class:`opensn::TransientSolver`.
1364
      )"
1365
    );
674✔
1366
  transient_solver.def(
1,348✔
1367
    py::init(
674✔
1368
      [](py::kwargs& params)
180✔
1369
      {
1370
        return TransientSolver::Create(kwargs_to_param_block(params));
180✔
1371
      }
1372
    ),
1373
    R"(
1374
    Construct a transient solver.
1375

1376
    Parameters
1377
    ----------
1378
    pyopensn.solver.DiscreteOrdinatesProblem : DiscreteOrdinatesProblem
1379
        Existing discrete ordinates problem instance.
1380
    dt : float, optional, default=2.0e-3
1381
        Time step size used during the simulation.
1382
    stop_time : float, optional, default=0.1
1383
        Simulation end time.
1384
    theta : float, optional, default=0.5
1385
        Time differencing scheme parameter.
1386
    initial_state : str, optional, default="existing"
1387
        Initial state for the transient solve. Allowed values: existing, zero.
1388
        In "zero" mode, scalar flux vectors are reset to zero.
1389
    verbose : bool, optional, default=True
1390
        Enable verbose logging.
1391

1392
    Notes
1393
    -----
1394
    The associated problem must have ``save_angular_flux=True`` enabled. This
1395
    is required for transient problems.
1396

1397
    If ``problem.options.read_restart_path`` is set, restart data is read
1398
    during :meth:`Initialize`. If ``problem.options.restart_writes_enabled`` is
1399
    true, timed restart dumps may be written during :meth:`Execute` and a final
1400
    restart dump is written when execution completes.
1401
    )"
1402
  );
1403
  transient_solver.def(
674✔
1404
    "SetTimeStep",
1405
    &TransientSolver::SetTimeStep,
674✔
1406
    R"(
1407
    Set the timestep size used by :meth:`Advance`.
1408

1409
    Parameters
1410
    ----------
1411
    dt : float
1412
        New timestep size.
1413
    )");
1414
  transient_solver.def(
674✔
1415
    "SetTheta",
1416
    &TransientSolver::SetTheta,
674✔
1417
    R"(
1418
    Set the theta parameter used by :meth:`Advance`.
1419

1420
    Parameters
1421
    ----------
1422
    theta : float
1423
        Theta value between 1.0e-16 and 1.
1424
    )");
1425
  transient_solver.def(
674✔
1426
    "Advance",
1427
    &TransientSolver::Advance,
674✔
1428
    R"(
1429
    Advance the solver by a single timestep.
1430

1431
    Notes
1432
    -----
1433
    You must call :meth:`Initialize` before calling :meth:`Advance` or
1434
    :meth:`Execute`.
1435
    )");
1436
  transient_solver.def(
674✔
1437
    "SetPreAdvanceCallback",
1438
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
674✔
1439
      &TransientSolver::SetPreAdvanceCallback),
1440
    R"(
1441
    Register a callback that runs before each advance within :meth:`Execute`.
1442

1443
    Parameters
1444
    ----------
1445
    callback : Optional[Callable[[], None]]
1446
        Function invoked before the solver advances a timestep. Pass None to clear.
1447
        If the callback modifies the timestep, the new value is used for the
1448
        upcoming step.
1449
    )");
1450
  transient_solver.def(
674✔
1451
    "SetPreAdvanceCallback",
1452
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
674✔
1453
      &TransientSolver::SetPreAdvanceCallback),
1454
    "Clear the PreAdvance callback by passing None.");
1455
  transient_solver.def(
674✔
1456
    "SetPostAdvanceCallback",
1457
    static_cast<void (TransientSolver::*)(std::function<void()>)>(
674✔
1458
      &TransientSolver::SetPostAdvanceCallback),
1459
    R"(
1460
    Register a callback that runs after each advance within :meth:`Execute`.
1461

1462
    Parameters
1463
    ----------
1464
    callback : Optional[Callable[[], None]]
1465
        Function invoked after the solver advances a timestep. Pass None to clear.
1466
    )");
1467
  transient_solver.def(
674✔
1468
    "SetPostAdvanceCallback",
1469
    static_cast<void (TransientSolver::*)(std::nullptr_t)>(
674✔
1470
      &TransientSolver::SetPostAdvanceCallback),
1471
    "Clear the PostAdvance callback by passing None.");
1472
  transient_solver.def(
674✔
1473
    "ComputeBalanceTable",
1474
    [balance_table_to_dict](const TransientSolver& self)
674✔
1475
    {
1476
      return balance_table_to_dict(self.ComputeBalanceTable());
×
1477
    },
1478
    R"(
1479
    Compute and return the global balance table using the solver's normalization.
1480
    This is a collective operation and must be called on all ranks.
1481

1482
    Returns
1483
    -------
1484
    dict
1485
        Dictionary with the following entries:
1486

1487
        - ``absorption_rate``:
1488
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1489
          groups and the full domain.
1490
        - ``production_rate``:
1491
          Global volumetric production/source rate used by the solver,
1492
          approximately ``integral Q dV`` summed over groups and the full domain.
1493
        - ``inflow_rate``:
1494
          Global incoming boundary contribution integrated over incoming
1495
          angular flux on boundaries.
1496
        - ``outflow_rate``:
1497
          Global outgoing boundary contribution accumulated from face outflow
1498
          tallies.
1499
        - ``balance``:
1500
          Rate balance,
1501
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1502
        - ``initial_inventory``:
1503
          Total particle inventory at the start of the timestep, computed as
1504
          ``integral (1 / v_g) * phi_old dV`` summed over groups and the full domain.
1505
        - ``final_inventory``:
1506
          Total particle inventory at the end of the timestep, computed as
1507
          ``integral (1 / v_g) * phi_new dV`` summed over groups and the full domain.
1508
        - ``predicted_inventory_change``:
1509
          Inventory change predicted by the current timestep balance, computed as 
1510
          ``dt * balance``.
1511
        - ``actual_inventory_change``:
1512
          Measured change in total particle inventory over the timestep, computed as
1513
          ``final_inventory - initial_inventory``.
1514
        - ``inventory_residual``:
1515
          Mismatch between the measured and predicted timestep inventory
1516
          changes, computed as
1517
          ``actual_inventory_change - predicted_inventory_change``.
1518

1519
    Notes
1520
    -----
1521
    This solver applies no extra normalization to the balance table.
1522

1523
    The transient inventory terms currently use the end-of-step rate balance to
1524
    estimate the timestep inventory change.
1525
    )"
1526
  );
1527
  slv.attr("BackwardEuler") = 1.0;
674✔
1528
  slv.attr("CrankNicolson") = 0.5;
1,348✔
1529
  // clang-format on
1530
}
674✔
1531

1532
// Wrap non-linear k-eigen solver
1533
void
1534
WrapNLKEigen(py::module& slv)
674✔
1535
{
1536
  const auto balance_table_to_dict = [](const BalanceTable& table)
674✔
1537
  {
1538
    py::dict values;
×
1539
    values["absorption_rate"] = table.absorption_rate;
×
1540
    values["production_rate"] = table.production_rate;
×
1541
    values["inflow_rate"] = table.inflow_rate;
×
1542
    values["outflow_rate"] = table.outflow_rate;
×
1543
    values["balance"] = table.balance;
×
1544
    if (table.initial_inventory.has_value())
×
1545
      values["initial_inventory"] = table.initial_inventory.value();
×
1546
    if (table.final_inventory.has_value())
×
1547
      values["final_inventory"] = table.final_inventory.value();
×
1548
    if (table.predicted_inventory_change.has_value())
×
1549
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1550
    if (table.actual_inventory_change.has_value())
×
1551
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1552
    if (table.inventory_residual.has_value())
×
1553
      values["inventory_residual"] = table.inventory_residual.value();
×
1554
    return values;
×
1555
  };
×
1556
  // clang-format off
1557
  // non-linear k-eigen solver
1558
  auto non_linear_k_eigen_solver = py::class_<NonLinearKEigenSolver, std::shared_ptr<NonLinearKEigenSolver>,
674✔
1559
                                              Solver>(
1560
    slv,
1561
    "NonLinearKEigenSolver",
1562
    R"(
1563
    Non-linear k-eigenvalue solver.
1564

1565
    Wrapper of :cpp:class:`opensn::NonLinearKEigenSolver`.
1566
    )"
1567
  );
674✔
1568
  non_linear_k_eigen_solver.def(
1,348✔
1569
    py::init(
674✔
1570
      [](py::kwargs& params)
41✔
1571
      {
1572
        return NonLinearKEigenSolver::Create(kwargs_to_param_block(params));
41✔
1573
      }
1574
        ),
1575
    R"(
1576
    Construct a non-linear k-eigenvalue solver.
1577

1578
    Parameters
1579
    ----------
1580
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1581
        Existing discrete ordinates problem instance.
1582
    nl_abs_tol: float, default=1.0e-8
1583
        Non-linear absolute tolerance.
1584
    nl_rel_tol: float, default=1.0e-8
1585
        Non-linear relative tolerance.
1586
    nl_sol_tol: float, default=1.0e-50
1587
        Non-linear solution tolerance.
1588
    nl_max_its: int, default=50
1589
        Non-linear algorithm maximum iterations.
1590
    l_abs_tol: float, default=1.0e-8
1591
        Linear absolute tolerance.
1592
    l_rel_tol: float, default=1.0e-8
1593
        Linear relative tolerance.
1594
    l_div_tol: float, default=1.0e6
1595
        Linear divergence tolerance.
1596
    l_max_its: int, default=50
1597
        Linear algorithm maximum iterations.
1598
    l_gmres_restart_intvl: int, default=30
1599
        GMRES restart interval.
1600
    l_gmres_breakdown_tol: float, default=1.0e6
1601
        GMRES breakdown tolerance.
1602
    reset_phi0: bool, default=True
1603
        If true, reinitializes scalar fluxes to 1.0.
1604
    num_initial_power_iterations: int, default=0
1605
        Number of initial power iterations before the non-linear solve.
1606
    )"
1607
  );
1608
  non_linear_k_eigen_solver.def(
674✔
1609
    "GetEigenvalue",
1610
    &NonLinearKEigenSolver::GetEigenvalue,
674✔
1611
    R"(
1612
    Return the current k-eigenvalue.
1613
    )"
1614
  );
1615
  non_linear_k_eigen_solver.def(
674✔
1616
    "ComputeBalanceTable",
1617
    [balance_table_to_dict](const NonLinearKEigenSolver& self)
674✔
1618
    {
1619
      return balance_table_to_dict(self.ComputeBalanceTable());
×
1620
    },
1621
    R"(
1622
    Compute and return the global balance table using the solver's normalization.
1623
    This is a collective operation and must be called on all ranks.
1624

1625
    Returns
1626
    -------
1627
    dict
1628
        Dictionary with the following entries:
1629

1630
        - ``absorption_rate``:
1631
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1632
          groups and the full domain.
1633
        - ``production_rate``:
1634
          Global volumetric production/source rate used by the solver,
1635
          approximately ``integral Q dV`` summed over groups and the full domain.
1636
        - ``inflow_rate``:
1637
          Global incoming boundary contribution integrated over incoming
1638
          angular flux on boundaries.
1639
        - ``outflow_rate``:
1640
          Global outgoing boundary contribution accumulated from face outflow
1641
          tallies.
1642
        - ``balance``:
1643
          Rate balance,
1644
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1645

1646
    Notes
1647
    -----
1648
    For k-eigenvalue balance reporting, this solver scales the production term by
1649
    ``1 / k_eff`` before forming both ``production_rate`` and ``balance``.
1650
    )"
1651
  );
1652
  // clang-format on
1653
}
674✔
1654

1655
// Wrap power iteration solvers
1656
void
1657
WrapPIteration(py::module& slv)
674✔
1658
{
1659
  const auto balance_table_to_dict = [](const BalanceTable& table)
678✔
1660
  {
1661
    py::dict values;
4✔
1662
    values["absorption_rate"] = table.absorption_rate;
4✔
1663
    values["production_rate"] = table.production_rate;
4✔
1664
    values["inflow_rate"] = table.inflow_rate;
4✔
1665
    values["outflow_rate"] = table.outflow_rate;
4✔
1666
    values["balance"] = table.balance;
4✔
1667
    if (table.initial_inventory.has_value())
4✔
1668
      values["initial_inventory"] = table.initial_inventory.value();
×
1669
    if (table.final_inventory.has_value())
4✔
1670
      values["final_inventory"] = table.final_inventory.value();
×
1671
    if (table.predicted_inventory_change.has_value())
4✔
1672
      values["predicted_inventory_change"] = table.predicted_inventory_change.value();
×
1673
    if (table.actual_inventory_change.has_value())
4✔
1674
      values["actual_inventory_change"] = table.actual_inventory_change.value();
×
1675
    if (table.inventory_residual.has_value())
4✔
1676
      values["inventory_residual"] = table.inventory_residual.value();
×
1677
    return values;
4✔
1678
  };
×
1679
  // clang-format off
1680
  // power iteration k-eigen solver
1681
  auto pi_k_eigen_solver = py::class_<PowerIterationKEigenSolver, std::shared_ptr<PowerIterationKEigenSolver>,
674✔
1682
                                      Solver>(
1683
    slv,
1684
    "PowerIterationKEigenSolver",
1685
    R"(
1686
    Power iteration k-eigenvalue solver.
1687

1688
    Wrapper of :cpp:class:`opensn::PowerIterationKEigenSolver`.
1689
    )"
1690
  );
674✔
1691
  pi_k_eigen_solver.def(
1,348✔
1692
    py::init(
674✔
1693
      [](py::kwargs& params)
165✔
1694
      {
1695
        return PowerIterationKEigenSolver::Create(kwargs_to_param_block(params));
165✔
1696
      }
1697
    ),
1698
    R"(
1699
    Construct a power iteration k-eigen solver.
1700

1701
    Parameters
1702
    ----------
1703
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1704
        Existing DiscreteOrdinatesProblem instance.
1705
    acceleration: pyopensn.solver.DiscreteOrdinatesKEigenAcceleration
1706
        Optional DiscreteOrdinatesKEigenAcceleration instance for acceleration.
1707
    max_iters: int, default = 1000
1708
        Maximum power iterations allowed.
1709
    k_tol: float, default = 1.0e-10
1710
        Tolerance on the k-eigenvalue.
1711
    reset_solution: bool, default=True
1712
        If true, initialize flux moments to 1.0.
1713
    reset_phi0: bool, default=True
1714
        If true, reinitializes scalar fluxes to 1.0.
1715

1716
    Notes
1717
    -----
1718
    If ``problem.options.read_restart_path`` is set, restart data is read
1719
    during :meth:`Initialize`. If ``problem.options.restart_writes_enabled`` is
1720
    true, timed restart dumps may be written during the outer iteration loop
1721
    and a final restart dump is written when execution completes.
1722
    )"
1723
  );
1724
  pi_k_eigen_solver.def(
674✔
1725
    "GetEigenvalue",
1726
    &PowerIterationKEigenSolver::GetEigenvalue,
674✔
1727
    R"(
1728
    Return the current k-eigenvalue.
1729
    )"
1730
  );
1731
  pi_k_eigen_solver.def(
674✔
1732
    "ComputeBalanceTable",
1733
    [balance_table_to_dict](const PowerIterationKEigenSolver& self)
674✔
1734
    {
1735
      return balance_table_to_dict(self.ComputeBalanceTable());
4✔
1736
    },
1737
    R"(
1738
    Compute and return the global balance table using the solver's normalization.
1739
    This is a collective operation and must be called on all ranks.
1740

1741
    Returns
1742
    -------
1743
    dict
1744
        Dictionary with the following entries:
1745

1746
        - ``absorption_rate``:
1747
          Global absorption rate, approximately ``integral sigma_a * phi dV`` summed over
1748
          groups and the full domain.
1749
        - ``production_rate``:
1750
          Global volumetric production/source rate used by the solver,
1751
          approximately ``integral Q dV`` summed over groups and the full domain.
1752
        - ``inflow_rate``:
1753
          Global incoming boundary contribution integrated over incoming
1754
          angular flux on boundaries.
1755
        - ``outflow_rate``:
1756
          Global outgoing boundary contribution accumulated from face outflow
1757
          tallies.
1758
        - ``balance``:
1759
          Rate balance,
1760
          ``production_rate + inflow_rate - absorption_rate - outflow_rate``.
1761

1762
    Notes
1763
    -----
1764
    For k-eigenvalue balance reporting, this solver scales the production term by
1765
    ``1 / k_eff`` before forming both ``production_rate`` and ``balance``.
1766
    )"
1767
  );
1768
  // clang-format on
1769
}
674✔
1770

1771
// Wrap LBS solver
1772
void
1773
WrapDiscreteOrdinatesKEigenAcceleration(py::module& slv)
674✔
1774
{
1775
  // clang-format off
1776
  // discrete ordinates k-eigen acceleration base
1777
  auto acceleration = py::class_<DiscreteOrdinatesKEigenAcceleration,
674✔
1778
                                 std::shared_ptr<DiscreteOrdinatesKEigenAcceleration>>(
1779
    slv,
1780
    "DiscreteOrdinatesKEigenAcceleration",
1781
    R"(
1782
    Base class for discrete ordinates k-eigenvalue acceleration methods.
1783

1784
    Wrapper of :cpp:class:`opensn::DiscreteOrdinatesKEigenAcceleration`.
1785
    )"
1786
  );
674✔
1787
  // SCDSA acceleration
1788
  auto scdsa_acceleration = py::class_<SCDSAAcceleration,
674✔
1789
                                       std::shared_ptr<SCDSAAcceleration>,
1790
                                       DiscreteOrdinatesKEigenAcceleration>(
1791
    slv,
1792
    "SCDSAAcceleration",
1793
    R"(
1794
    Construct an SCDSA accelerator for the power iteration k-eigenvalue solver.
1795

1796
    Wrapper of :cpp:class:`opensn::SCDSAAcceleration`.
1797
    )"
1798
  );
674✔
1799
  scdsa_acceleration.def(
1,348✔
1800
    py::init(
674✔
1801
      [](py::kwargs& params)
8✔
1802
      {
1803
        return SCDSAAcceleration::Create(kwargs_to_param_block(params));
8✔
1804
      }
1805
    ),
1806
    R"(
1807
    SCDSA acceleration for the power iteration k-eigenvalue solver.
1808

1809
    Parameters
1810
    ----------
1811
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1812
        Existing DiscreteOrdinatesProblem instance.
1813
    l_abs_tol: float, defauilt=1.0e-10
1814
        Absolute residual tolerance.
1815
    max_iters: int, default=100
1816
        Maximum allowable iterations.
1817
    verbose: bool, default=False
1818
        If true, enables verbose output.
1819
    petsc_options: str, default="ssss"
1820
        Additional PETSc options.
1821
    pi_max_its: int, default=50
1822
        Maximum allowable iterations for inner power iterations.
1823
    pi_k_tol: float, default=1.0e-10
1824
        k-eigenvalue tolerance for the inner power iterations.
1825
    sdm: str, default="pwld"
1826
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1827
            - 'pwld' : Piecewise Linear Discontinuous
1828
            - 'pwlc' : Piecewise Linear Continuous
1829
    )"
1830
  );
1831
  // SMM acceleration
1832
  auto smm_acceleration = py::class_<SMMAcceleration,
674✔
1833
                                     std::shared_ptr<SMMAcceleration>,
1834
                                     DiscreteOrdinatesKEigenAcceleration>(
1835
    slv,
1836
    "SMMAcceleration",
1837
    R"(
1838
    Construct an SMM accelerator for the power iteration k-eigenvalue solver.
1839

1840
    Wrapper of :cpp:class:`opensn::SMMAcceleration`.
1841
    )"
1842
  );
674✔
1843
  smm_acceleration.def(
1,348✔
1844
    py::init(
674✔
1845
      [](py::kwargs& params)
4✔
1846
      {
1847
        return SMMAcceleration::Create(kwargs_to_param_block(params));
4✔
1848
      }
1849
    ),
1850
    R"(
1851
    SMM acceleration for the power iteration k-eigenvalue solver.
1852

1853
    Warnings
1854
    --------
1855
       SMM acceleration is **experimental** and should be used with caution!
1856
       SMM accleration only supports problems with isotropic scattering.
1857

1858
    Parameters
1859
    ----------
1860
    problem: pyopensn.solver.DiscreteOrdinatesProblem
1861
        Existing DiscreteOrdinatesProblem instance.
1862
    l_abs_tol: float, defauilt=1.0e-10
1863
        Absolute residual tolerance.
1864
    max_iters: int, default=100
1865
        Maximum allowable iterations.
1866
    verbose: bool, default=False
1867
        If true, enables verbose output.
1868
    petsc_options: str, default="ssss"
1869
        Additional PETSc options.
1870
    pi_max_its: int, default=50
1871
        Maximum allowable iterations for inner power iterations.
1872
    pi_k_tol: float, default=1.0e-10
1873
        k-eigenvalue tolerance for the inner power iterations.
1874
    sdm: str, default="pwld"
1875
        Spatial discretization method to use for the diffusion solver. Valid choices are:
1876
            - 'pwld' : Piecewise Linear Discontinuous
1877
            - 'pwlc' : Piecewise Linear Continuous
1878
    )"
1879
  );
1880
  // clang-format on
1881
}
674✔
1882

1883
// Wrap the solver components of OpenSn
1884
void
1885
py_solver(py::module& pyopensn)
72✔
1886
{
1887
  py::module slv = pyopensn.def_submodule("solver", "Solver module.");
72✔
1888
  WrapProblem(slv);
72✔
1889
  WrapSolver(slv);
72✔
1890
  WrapLBS(slv);
72✔
1891
  WrapSteadyState(slv);
72✔
1892
  WrapTransient(slv);
72✔
1893
  WrapNLKEigen(slv);
72✔
1894
  WrapDiscreteOrdinatesKEigenAcceleration(slv);
72✔
1895
  WrapPIteration(slv);
72✔
1896
}
72✔
1897

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