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

openmc-dev / openmc / 22694846817

04 Mar 2026 11:42PM UTC coverage: 81.537% (-0.2%) from 81.69%
22694846817

Pull #3808

github

web-flow
Merge 41317cfa2 into 2bd06660c
Pull Request #3808: Add properties to settings w/ documentation, c++ loading of filename, and python round-trip test

17549 of 25285 branches covered (69.4%)

Branch coverage included in aggregate %.

82 of 92 new or added lines in 8 files covered. (89.13%)

1498 existing lines in 55 files now uncovered.

57990 of 67359 relevant lines covered (86.09%)

44766605.57 hits per line

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

79.26
/src/tallies/derivative.cpp
1
#include "openmc/tallies/derivative.h"
2

3
#include "openmc/error.h"
4
#include "openmc/material.h"
5
#include "openmc/nuclide.h"
6
#include "openmc/settings.h"
7
#include "openmc/tallies/tally.h"
8
#include "openmc/xml_interface.h"
9

10
#include <fmt/core.h>
11

12
template class openmc::vector<openmc::TallyDerivative>;
13

14
namespace openmc {
15

16
//==============================================================================
17
// Global variables
18
//==============================================================================
19

20
namespace model {
21
std::unordered_map<int, int> tally_deriv_map;
22
vector<TallyDerivative> tally_derivs;
23
} // namespace model
24

25
//==============================================================================
26
// TallyDerivative implementation
27
//==============================================================================
28

29
TallyDerivative::TallyDerivative(pugi::xml_node node)
75✔
30
{
31
  if (check_for_node(node, "id")) {
75!
32
    id = std::stoi(get_node_value(node, "id"));
150✔
33
  } else {
34
    fatal_error("Must specify an ID for <derivative> elements in the tally "
×
35
                "XML file");
36
  }
37

38
  if (id <= 0)
75!
39
    fatal_error("<derivative> IDs must be an integer greater than zero");
×
40

41
  std::string variable_str = get_node_value(node, "variable");
75✔
42

43
  if (variable_str == "density") {
75✔
44
    variable = DerivativeVariable::DENSITY;
30✔
45
  } else if (variable_str == "nuclide_density") {
45✔
46
    variable = DerivativeVariable::NUCLIDE_DENSITY;
30✔
47

48
    std::string nuclide_name = get_node_value(node, "nuclide");
30✔
49
    bool found = false;
50
    for (auto i = 0; i < data::nuclides.size(); ++i) {
720✔
51
      if (data::nuclides[i]->name_ == nuclide_name) {
690✔
52
        found = true;
30✔
53
        diff_nuclide = i;
30✔
54
      }
55
    }
56
    if (!found) {
30!
57
      fatal_error(fmt::format("Could not find the nuclide \"{}\" specified in "
×
58
                              "derivative {} in any material.",
59
        nuclide_name, id));
×
60
    }
61
  } else if (variable_str == "temperature") {
45!
62
    variable = DerivativeVariable::TEMPERATURE;
15✔
63
  } else {
64
    fatal_error(fmt::format(
×
65
      "Unrecognized variable \"{}\" on derivative {}", variable_str, id));
×
66
  }
67

68
  diff_material = std::stoi(get_node_value(node, "material"));
150✔
69
}
75✔
70

71
//==============================================================================
72
// Non-method functions
73
//==============================================================================
74

75
void read_tally_derivatives(pugi::xml_node node)
4,380✔
76
{
77
  // Populate the derivatives array.
78
  for (auto deriv_node : node.children("derivative"))
4,455✔
79
    model::tally_derivs.emplace_back(deriv_node);
75✔
80

81
  // Fill the derivative map.
82
  for (auto i = 0; i < model::tally_derivs.size(); ++i) {
4,455✔
83
    auto id = model::tally_derivs[i].id;
75!
84
    auto search = model::tally_deriv_map.find(id);
75!
85
    if (search == model::tally_deriv_map.end()) {
75!
86
      model::tally_deriv_map[id] = i;
75✔
87
    } else {
88
      fatal_error("Two or more derivatives use the same unique ID: " +
×
89
                  std::to_string(id));
×
90
    }
91
  }
92

93
  // Make sure derivatives were not requested for an MG run.
94
  if (!settings::run_CE && !model::tally_derivs.empty())
4,380!
95
    fatal_error("Differential tallies not supported in multi-group mode");
×
96
}
4,380✔
97

98
void apply_derivative_to_score(const Particle& p, int i_tally, int i_nuclide,
2,810,566✔
99
  double atom_density, int score_bin, double& score)
100
{
101
  const Tally& tally {*model::tallies[i_tally]};
2,810,566✔
102

103
  if (score == 0.0)
2,810,566✔
104
    return;
105

106
  // If our score was previously c then the new score is
107
  // c * (1/f * d_f/d_p + 1/c * d_c/d_p)
108
  // where (1/f * d_f/d_p) is the (logarithmic) flux derivative and p is the
109
  // perturbated variable.
110

111
  const auto& deriv {model::tally_derivs[tally.deriv_]};
2,140,666✔
112
  const auto flux_deriv = p.flux_derivs(tally.deriv_);
2,140,666✔
113

114
  // Handle special cases where we know that d_c/d_p must be zero.
115
  if (score_bin == SCORE_FLUX) {
2,140,666✔
116
    score *= flux_deriv;
321,970✔
117
    return;
321,970✔
118
  } else if (p.material() == MATERIAL_VOID) {
1,818,696!
119
    score *= flux_deriv;
×
120
    return;
×
121
  }
122
  const Material& material {*model::materials[p.material()]};
1,818,696✔
123
  if (material.id_ != deriv.diff_material) {
1,818,696✔
124
    score *= flux_deriv;
806,025✔
125
    return;
806,025✔
126
  }
127

128
  switch (deriv.variable) {
1,012,671!
129

130
    //============================================================================
131
    // Density derivative:
132
    // c = Sigma_MT
133
    // c = sigma_MT * N
134
    // c = sigma_MT * rho * const
135
    // d_c / d_rho = sigma_MT * const
136
    // (1 / c) * (d_c / d_rho) = 1 / rho
137

138
  case DerivativeVariable::DENSITY:
402,831✔
139
    switch (tally.estimator_) {
402,831!
140

141
    case TallyEstimator::ANALOG:
402,831✔
142
    case TallyEstimator::COLLISION:
402,831✔
143
      switch (score_bin) {
402,831!
144

145
      case SCORE_TOTAL:
402,831✔
146
      case SCORE_SCATTER:
402,831✔
147
      case SCORE_ABSORPTION:
402,831✔
148
      case SCORE_FISSION:
402,831✔
149
      case SCORE_NU_FISSION:
402,831✔
150
        score *= flux_deriv + 1. / material.density_gpcc_;
402,831✔
151
        break;
402,831✔
152

153
      default:
×
154
        fatal_error("Tally derivative not defined for a score on tally " +
×
155
                    std::to_string(tally.id_));
×
156
      }
157
      break;
402,831✔
158

159
    default:
×
160
      fatal_error("Differential tallies are only implemented for analog and "
×
161
                  "collision estimators.");
162
    }
163
    break;
164

165
    //============================================================================
166
    // Nuclide density derivative:
167
    // If we are scoring a reaction rate for a single nuclide then
168
    // c = Sigma_MT_i
169
    // c = sigma_MT_i * N_i
170
    // d_c / d_N_i = sigma_MT_i
171
    // (1 / c) * (d_c / d_N_i) = 1 / N_i
172
    // If the score is for the total material (i_nuclide = -1)
173
    // c = Sum_i(Sigma_MT_i)
174
    // d_c / d_N_i = sigma_MT_i
175
    // (1 / c) * (d_c / d_N) = sigma_MT_i / Sigma_MT
176
    // where i is the perturbed nuclide.
177

178
  case DerivativeVariable::NUCLIDE_DENSITY:
406,560✔
179
    switch (tally.estimator_) {
406,560!
180

181
    case TallyEstimator::ANALOG:
11,880✔
182
      if (p.event_nuclide() != deriv.diff_nuclide) {
11,880✔
183
        score *= flux_deriv;
6,996✔
184
        return;
6,996✔
185
      }
186

187
      switch (score_bin) {
4,884!
188

189
      case SCORE_TOTAL:
190
      case SCORE_SCATTER:
191
      case SCORE_ABSORPTION:
192
      case SCORE_FISSION:
193
      case SCORE_NU_FISSION: {
194
        // Find the index of the perturbed nuclide.
195
        int i;
196
        for (i = 0; i < material.nuclide_.size(); ++i)
9,768!
197
          if (material.nuclide_[i] == deriv.diff_nuclide)
9,768✔
198
            break;
199
        score *= flux_deriv + 1. / material.atom_density_(i);
4,884✔
200
      } break;
4,884✔
201

202
      default:
×
203
        fatal_error("Tally derivative not defined for a score on tally " +
×
204
                    std::to_string(tally.id_));
×
205
      }
206
      break;
4,884✔
207

208
    case TallyEstimator::COLLISION:
394,680✔
209
      switch (score_bin) {
394,680!
210

211
      case SCORE_TOTAL:
78,936✔
212
        if (i_nuclide == -1 && p.macro_xs().total > 0.0) {
78,936!
213
          score *= flux_deriv +
39,468✔
214
                   p.neutron_xs(deriv.diff_nuclide).total / p.macro_xs().total;
39,468✔
215
        } else if (i_nuclide == deriv.diff_nuclide &&
39,468!
216
                   p.neutron_xs(i_nuclide).total) {
19,734!
217
          score *= flux_deriv + 1. / atom_density;
19,734✔
218
        } else {
219
          score *= flux_deriv;
19,734✔
220
        }
221
        break;
222

223
      case SCORE_SCATTER:
78,936✔
224
        if (i_nuclide == -1 &&
78,936✔
225
            (p.macro_xs().total - p.macro_xs().absorption) > 0.0) {
39,468!
226
          score *=
39,468✔
227
            flux_deriv + (p.neutron_xs(deriv.diff_nuclide).total -
39,468✔
228
                           p.neutron_xs(deriv.diff_nuclide).absorption) /
39,468✔
229
                           (p.macro_xs().total - p.macro_xs().absorption);
39,468✔
230
        } else if (i_nuclide == deriv.diff_nuclide) {
39,468✔
231
          score *= flux_deriv + 1. / atom_density;
19,734✔
232
        } else {
233
          score *= flux_deriv;
19,734✔
234
        }
235
        break;
236

237
      case SCORE_ABSORPTION:
78,936✔
238
        if (i_nuclide == -1 && p.macro_xs().absorption > 0.0) {
78,936!
239
          score *= flux_deriv + p.neutron_xs(deriv.diff_nuclide).absorption /
39,468✔
240
                                  p.macro_xs().absorption;
39,468✔
241
        } else if (i_nuclide == deriv.diff_nuclide &&
39,468!
242
                   p.neutron_xs(i_nuclide).absorption) {
19,734!
243
          score *= flux_deriv + 1. / atom_density;
19,734✔
244
        } else {
245
          score *= flux_deriv;
19,734✔
246
        }
247
        break;
248

249
      case SCORE_FISSION:
78,936✔
250
        if (i_nuclide == -1 && p.macro_xs().fission > 0.0) {
78,936!
251
          score *= flux_deriv + p.neutron_xs(deriv.diff_nuclide).fission /
39,468✔
252
                                  p.macro_xs().fission;
39,468✔
253
        } else if (i_nuclide == deriv.diff_nuclide &&
39,468!
254
                   p.neutron_xs(i_nuclide).fission) {
19,734!
255
          score *= flux_deriv + 1. / atom_density;
19,734✔
256
        } else {
257
          score *= flux_deriv;
19,734✔
258
        }
259
        break;
260

261
      case SCORE_NU_FISSION:
78,936✔
262
        if (i_nuclide == -1 && p.macro_xs().nu_fission > 0.0) {
78,936!
263
          score *= flux_deriv + p.neutron_xs(deriv.diff_nuclide).nu_fission /
39,468✔
264
                                  p.macro_xs().nu_fission;
39,468✔
265
        } else if (i_nuclide == deriv.diff_nuclide &&
39,468!
266
                   p.neutron_xs(i_nuclide).nu_fission) {
19,734!
267
          score *= flux_deriv + 1. / atom_density;
19,734✔
268
        } else {
269
          score *= flux_deriv;
19,734✔
270
        }
271
        break;
272

273
      default:
×
274
        fatal_error("Tally derivative not defined for a score on tally " +
×
275
                    std::to_string(tally.id_));
×
276
      }
277
      break;
278

279
    default:
×
280
      fatal_error("Differential tallies are only implemented for analog and "
×
281
                  "collision estimators.");
282
    }
283
    break;
284

285
    //============================================================================
286
    // Temperature derivative:
287
    // If we are scoring a reaction rate for a single nuclide then
288
    // c = Sigma_MT_i
289
    // c = sigma_MT_i * N_i
290
    // d_c / d_T = (d_sigma_Mt_i / d_T) * N_i
291
    // (1 / c) * (d_c / d_T) = (d_sigma_MT_i / d_T) / sigma_MT_i
292
    // If the score is for the total material (i_nuclide = -1)
293
    // (1 / c) * (d_c / d_T) = Sum_i((d_sigma_MT_i / d_T) * N_i) / Sigma_MT_i
294
    // where i is the perturbed nuclide.  The d_sigma_MT_i / d_T term is
295
    // computed by multipole_deriv_eval.  It only works for the resolved
296
    // resonance range and requires multipole data.
297

298
  case DerivativeVariable::TEMPERATURE:
203,280✔
299
    switch (tally.estimator_) {
203,280!
300

301
    case TallyEstimator::ANALOG: {
302
      // Find the index of the event nuclide.
303
      int i;
304
      for (i = 0; i < material.nuclide_.size(); ++i)
13,057!
305
        if (material.nuclide_[i] == p.event_nuclide())
13,057✔
306
          break;
307

308
      const auto& nuc {*data::nuclides[p.event_nuclide()]};
5,940✔
309
      if (!multipole_in_range(nuc, p.E_last())) {
5,940✔
310
        score *= flux_deriv;
363✔
311
        break;
363✔
312
      }
313

314
      switch (score_bin) {
5,577!
315

316
      case SCORE_TOTAL:
×
317
        if (p.neutron_xs(p.event_nuclide()).total) {
×
UNCOV
318
          double dsig_s, dsig_a, dsig_f;
×
319
          std::tie(dsig_s, dsig_a, dsig_f) =
×
320
            nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
×
321
          score *= flux_deriv + (dsig_s + dsig_a) * material.atom_density_(i) /
×
322
                                  p.macro_xs().total;
×
323
        } else {
324
          score *= flux_deriv;
×
325
        }
326
        break;
327

328
      case SCORE_SCATTER:
99✔
329
        if (p.neutron_xs(p.event_nuclide()).total -
99!
330
            p.neutron_xs(p.event_nuclide()).absorption) {
99!
331
          double dsig_s, dsig_a, dsig_f;
99✔
332
          std::tie(dsig_s, dsig_a, dsig_f) =
99✔
333
            nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
99✔
334
          score *=
99✔
335
            flux_deriv + dsig_s * material.atom_density_(i) /
99✔
336
                           (p.macro_xs().total - p.macro_xs().absorption);
99✔
337
        } else {
338
          score *= flux_deriv;
×
339
        }
340
        break;
341

342
      case SCORE_ABSORPTION:
2,277✔
343
        if (p.neutron_xs(p.event_nuclide()).absorption) {
2,277!
344
          double dsig_s, dsig_a, dsig_f;
2,277✔
345
          std::tie(dsig_s, dsig_a, dsig_f) =
2,277✔
346
            nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
2,277✔
347
          score *= flux_deriv +
2,277✔
348
                   dsig_a * material.atom_density_(i) / p.macro_xs().absorption;
2,277✔
349
        } else {
350
          score *= flux_deriv;
×
351
        }
352
        break;
353

354
      case SCORE_FISSION:
×
355
        if (p.neutron_xs(p.event_nuclide()).fission) {
×
UNCOV
356
          double dsig_s, dsig_a, dsig_f;
×
357
          std::tie(dsig_s, dsig_a, dsig_f) =
×
358
            nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
×
359
          score *= flux_deriv +
×
360
                   dsig_f * material.atom_density_(i) / p.macro_xs().fission;
×
361
        } else {
362
          score *= flux_deriv;
×
363
        }
364
        break;
365

366
      case SCORE_NU_FISSION:
3,201✔
367
        if (p.neutron_xs(p.event_nuclide()).fission) {
3,201!
368
          double nu = p.neutron_xs(p.event_nuclide()).nu_fission /
3,201✔
369
                      p.neutron_xs(p.event_nuclide()).fission;
3,201✔
370
          double dsig_s, dsig_a, dsig_f;
3,201✔
371
          std::tie(dsig_s, dsig_a, dsig_f) =
3,201✔
372
            nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
3,201✔
373
          score *= flux_deriv + nu * dsig_f * material.atom_density_(i) /
3,201✔
374
                                  p.macro_xs().nu_fission;
3,201✔
375
        } else {
376
          score *= flux_deriv;
×
377
        }
378
        break;
379

380
      default:
×
381
        fatal_error("Tally derivative not defined for a score on tally " +
×
382
                    std::to_string(tally.id_));
×
383
      }
384
    } break;
385

386
    case TallyEstimator::COLLISION:
197,340✔
387
      if (i_nuclide != -1) {
197,340✔
388
        const auto& nuc {data::nuclides[i_nuclide]};
98,670✔
389
        if (!multipole_in_range(*nuc, p.E_last())) {
98,670✔
390
          score *= flux_deriv;
52,415✔
391
          return;
52,415✔
392
        }
393
      }
394

395
      switch (score_bin) {
144,925!
396

397
      case SCORE_TOTAL:
28,985✔
398
        if (i_nuclide == -1 && p.macro_xs().total > 0.0) {
28,985!
399
          double cum_dsig = 0;
400
          for (auto i = 0; i < material.nuclide_.size(); ++i) {
118,404✔
401
            auto i_nuc = material.nuclide_[i];
98,670✔
402
            const auto& nuc {*data::nuclides[i_nuc]};
98,670✔
403
            if (multipole_in_range(nuc, p.E_last()) &&
98,670!
404
                p.neutron_xs(i_nuc).total) {
55,308!
405
              double dsig_s, dsig_a, dsig_f;
55,308✔
406
              std::tie(dsig_s, dsig_a, dsig_f) =
55,308✔
407
                nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
55,308✔
408
              cum_dsig += (dsig_s + dsig_a) * material.atom_density_(i);
55,308✔
409
            }
410
          }
411
          score *= flux_deriv + cum_dsig / p.macro_xs().total;
19,734✔
412
        } else if (p.neutron_xs(i_nuclide).total) {
9,251!
413
          const auto& nuc {*data::nuclides[i_nuclide]};
9,251✔
414
          double dsig_s, dsig_a, dsig_f;
9,251✔
415
          std::tie(dsig_s, dsig_a, dsig_f) =
9,251✔
416
            nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
9,251✔
417
          score *=
9,251✔
418
            flux_deriv + (dsig_s + dsig_a) / p.neutron_xs(i_nuclide).total;
9,251✔
419
        } else {
420
          score *= flux_deriv;
×
421
        }
422
        break;
423

424
      case SCORE_SCATTER:
28,985✔
425
        if (i_nuclide == -1 && (p.macro_xs().total - p.macro_xs().absorption)) {
28,985!
426
          double cum_dsig = 0;
427
          for (auto i = 0; i < material.nuclide_.size(); ++i) {
118,404✔
428
            auto i_nuc = material.nuclide_[i];
98,670✔
429
            const auto& nuc {*data::nuclides[i_nuc]};
98,670✔
430
            if (multipole_in_range(nuc, p.E_last()) &&
98,670!
431
                (p.neutron_xs(i_nuc).total - p.neutron_xs(i_nuc).absorption)) {
55,308!
432
              double dsig_s, dsig_a, dsig_f;
55,308✔
433
              std::tie(dsig_s, dsig_a, dsig_f) =
55,308✔
434
                nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
55,308✔
435
              cum_dsig += dsig_s * material.atom_density_(i);
55,308✔
436
            }
437
          }
438
          score *= flux_deriv +
19,734✔
439
                   cum_dsig / (p.macro_xs().total - p.macro_xs().absorption);
19,734✔
440
        } else if (p.neutron_xs(i_nuclide).total -
9,251!
441
                   p.neutron_xs(i_nuclide).absorption) {
9,251!
442
          const auto& nuc {*data::nuclides[i_nuclide]};
9,251✔
443
          double dsig_s, dsig_a, dsig_f;
9,251✔
444
          std::tie(dsig_s, dsig_a, dsig_f) =
9,251✔
445
            nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
9,251✔
446
          score *= flux_deriv + dsig_s / (p.neutron_xs(i_nuclide).total -
9,251✔
447
                                           p.neutron_xs(i_nuclide).absorption);
9,251✔
448
        } else {
449
          score *= flux_deriv;
×
450
        }
451
        break;
452

453
      case SCORE_ABSORPTION:
28,985✔
454
        if (i_nuclide == -1 && p.macro_xs().absorption > 0.0) {
28,985!
455
          double cum_dsig = 0;
456
          for (auto i = 0; i < material.nuclide_.size(); ++i) {
118,404✔
457
            auto i_nuc = material.nuclide_[i];
98,670✔
458
            const auto& nuc {*data::nuclides[i_nuc]};
98,670✔
459
            if (multipole_in_range(nuc, p.E_last()) &&
98,670!
460
                p.neutron_xs(i_nuc).absorption) {
55,308!
461
              double dsig_s, dsig_a, dsig_f;
55,308✔
462
              std::tie(dsig_s, dsig_a, dsig_f) =
55,308✔
463
                nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
55,308✔
464
              cum_dsig += dsig_a * material.atom_density_(i);
55,308✔
465
            }
466
          }
467
          score *= flux_deriv + cum_dsig / p.macro_xs().absorption;
19,734✔
468
        } else if (p.neutron_xs(i_nuclide).absorption) {
9,251!
469
          const auto& nuc {*data::nuclides[i_nuclide]};
9,251✔
470
          double dsig_s, dsig_a, dsig_f;
9,251✔
471
          std::tie(dsig_s, dsig_a, dsig_f) =
9,251✔
472
            nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
9,251✔
473
          score *= flux_deriv + dsig_a / p.neutron_xs(i_nuclide).absorption;
9,251✔
474
        } else {
475
          score *= flux_deriv;
×
476
        }
477
        break;
478

479
      case SCORE_FISSION:
28,985✔
480
        if (i_nuclide == -1 && p.macro_xs().fission > 0.0) {
28,985!
481
          double cum_dsig = 0;
482
          for (auto i = 0; i < material.nuclide_.size(); ++i) {
118,404✔
483
            auto i_nuc = material.nuclide_[i];
98,670✔
484
            const auto& nuc {*data::nuclides[i_nuc]};
98,670✔
485
            if (multipole_in_range(nuc, p.E_last()) &&
98,670✔
486
                p.neutron_xs(i_nuc).fission) {
55,308✔
487
              double dsig_s, dsig_a, dsig_f;
29,656✔
488
              std::tie(dsig_s, dsig_a, dsig_f) =
29,656✔
489
                nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
29,656✔
490
              cum_dsig += dsig_f * material.atom_density_(i);
29,656✔
491
            }
492
          }
493
          score *= flux_deriv + cum_dsig / p.macro_xs().fission;
19,734✔
494
        } else if (p.neutron_xs(i_nuclide).fission) {
9,251!
495
          const auto& nuc {*data::nuclides[i_nuclide]};
9,251✔
496
          double dsig_s, dsig_a, dsig_f;
9,251✔
497
          std::tie(dsig_s, dsig_a, dsig_f) =
9,251✔
498
            nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
9,251✔
499
          score *= flux_deriv + dsig_f / p.neutron_xs(i_nuclide).fission;
9,251✔
500
        } else {
501
          score *= flux_deriv;
×
502
        }
503
        break;
504

505
      case SCORE_NU_FISSION:
28,985✔
506
        if (i_nuclide == -1 && p.macro_xs().nu_fission > 0.0) {
28,985!
507
          double cum_dsig = 0;
508
          for (auto i = 0; i < material.nuclide_.size(); ++i) {
118,404✔
509
            auto i_nuc = material.nuclide_[i];
98,670✔
510
            const auto& nuc {*data::nuclides[i_nuc]};
98,670✔
511
            if (multipole_in_range(nuc, p.E_last()) &&
98,670✔
512
                p.neutron_xs(i_nuc).fission) {
55,308✔
513
              double nu =
29,656✔
514
                p.neutron_xs(i_nuc).nu_fission / p.neutron_xs(i_nuc).fission;
29,656✔
515
              double dsig_s, dsig_a, dsig_f;
29,656✔
516
              std::tie(dsig_s, dsig_a, dsig_f) =
29,656✔
517
                nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
29,656✔
518
              cum_dsig += nu * dsig_f * material.atom_density_(i);
29,656✔
519
            }
520
          }
521
          score *= flux_deriv + cum_dsig / p.macro_xs().nu_fission;
19,734✔
522
        } else if (p.neutron_xs(i_nuclide).fission) {
9,251!
523
          const auto& nuc {*data::nuclides[i_nuclide]};
9,251✔
524
          double dsig_s, dsig_a, dsig_f;
9,251✔
525
          std::tie(dsig_s, dsig_a, dsig_f) =
9,251✔
526
            nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
9,251✔
527
          score *= flux_deriv + dsig_f / p.neutron_xs(i_nuclide).fission;
9,251✔
528
        } else {
529
          score *= flux_deriv;
×
530
        }
531
        break;
532

533
      default:
534
        break;
535
      }
536
      break;
537

538
    default:
×
539
      fatal_error("Differential tallies are only implemented for analog and "
×
540
                  "collision estimators.");
541
    }
542
    break;
543
  }
544
}
545

546
void score_track_derivative(Particle& p, double distance)
1,893,421,216✔
547
{
548
  // A void material cannot be perturbed so it will not affect flux derivatives.
549
  if (p.material() == MATERIAL_VOID)
1,893,421,216✔
550
    return;
551

552
  const Material& material {*model::materials[p.material()]};
1,783,633,570✔
553

554
  for (auto idx = 0; idx < model::tally_derivs.size(); idx++) {
1,785,992,190✔
555
    const auto& deriv = model::tally_derivs[idx];
2,358,620✔
556
    auto& flux_deriv = p.flux_derivs(idx);
2,358,620✔
557
    if (deriv.diff_material != material.id_)
2,358,620✔
558
      continue;
1,914,319✔
559

560
    switch (deriv.variable) {
444,301!
561

562
    case DerivativeVariable::DENSITY:
225,841✔
563
      // phi is proportional to e^(-Sigma_tot * dist)
564
      // (1 / phi) * (d_phi / d_rho) = - (d_Sigma_tot / d_rho) * dist
565
      // (1 / phi) * (d_phi / d_rho) = - Sigma_tot / rho * dist
566
      flux_deriv -= distance * p.macro_xs().total / material.density_gpcc_;
225,841✔
567
      break;
225,841✔
568

569
    case DerivativeVariable::NUCLIDE_DENSITY:
145,640✔
570
      // phi is proportional to e^(-Sigma_tot * dist)
571
      // (1 / phi) * (d_phi / d_N) = - (d_Sigma_tot / d_N) * dist
572
      // (1 / phi) * (d_phi / d_N) = - sigma_tot * dist
573
      flux_deriv -= distance * p.neutron_xs(deriv.diff_nuclide).total;
145,640✔
574
      break;
145,640✔
575

576
    case DerivativeVariable::TEMPERATURE:
577
      for (auto i = 0; i < material.nuclide_.size(); ++i) {
436,920✔
578
        const auto& nuc {*data::nuclides[material.nuclide_[i]]};
364,100✔
579
        if (multipole_in_range(nuc, p.E_last())) {
364,100✔
580
          // phi is proportional to e^(-Sigma_tot * dist)
581
          // (1 / phi) * (d_phi / d_T) = - (d_Sigma_tot / d_T) * dist
582
          // (1 / phi) * (d_phi / d_T) = - N (d_sigma_tot / d_T) * dist
583
          double dsig_s, dsig_a, dsig_f;
176,330✔
584
          std::tie(dsig_s, dsig_a, dsig_f) =
176,330✔
585
            nuc.multipole_->evaluate_deriv(p.E(), p.sqrtkT());
176,330✔
586
          flux_deriv -=
176,330✔
587
            distance * (dsig_s + dsig_a) * material.atom_density_(i);
176,330✔
588
        }
589
      }
590
      break;
591
    }
592
  }
593
}
594

595
void score_collision_derivative(Particle& p)
937,952,215✔
596
{
597
  // A void material cannot be perturbed so it will not affect flux derivatives.
598
  if (p.material() == MATERIAL_VOID)
937,952,215!
599
    return;
600

601
  const Material& material {*model::materials[p.material()]};
937,952,215✔
602

603
  for (auto idx = 0; idx < model::tally_derivs.size(); idx++) {
938,461,405✔
604
    const auto& deriv = model::tally_derivs[idx];
509,190✔
605
    auto& flux_deriv = p.flux_derivs(idx);
509,190✔
606

607
    if (deriv.diff_material != material.id_)
509,190✔
608
      continue;
385,594✔
609

610
    switch (deriv.variable) {
123,596!
611

612
    case DerivativeVariable::DENSITY:
64,394✔
613
      // phi is proportional to Sigma_s
614
      // (1 / phi) * (d_phi / d_rho) = (d_Sigma_s / d_rho) / Sigma_s
615
      // (1 / phi) * (d_phi / d_rho) = 1 / rho
616
      flux_deriv += 1. / material.density_gpcc_;
64,394✔
617
      break;
64,394✔
618

619
    case DerivativeVariable::NUCLIDE_DENSITY:
39,468✔
620
      if (p.event_nuclide() != deriv.diff_nuclide)
39,468✔
621
        continue;
30,789✔
622
      // Find the index in this material for the diff_nuclide.
623
      int i;
624
      for (i = 0; i < material.nuclide_.size(); ++i)
38,577!
625
        if (material.nuclide_[i] == deriv.diff_nuclide)
38,577✔
626
          break;
627
      // Make sure we found the nuclide.
628
      if (material.nuclide_[i] != deriv.diff_nuclide) {
8,679!
629
        fatal_error(fmt::format(
×
630
          "Could not find nuclide {} in material {} for tally derivative {}",
631
          data::nuclides[deriv.diff_nuclide]->name_, material.id_, deriv.id));
×
632
      }
633
      // phi is proportional to Sigma_s
634
      // (1 / phi) * (d_phi / d_N) = (d_Sigma_s / d_N) / Sigma_s
635
      // (1 / phi) * (d_phi / d_N) = sigma_s / Sigma_s
636
      // (1 / phi) * (d_phi / d_N) = 1 / N
637
      flux_deriv += 1. / material.atom_density_(i);
8,679✔
638
      break;
8,679✔
639

640
    case DerivativeVariable::TEMPERATURE:
19,734✔
641
      // Loop over the material's nuclides until we find the event nuclide.
642
      for (auto i_nuc : material.nuclide_) {
118,404✔
643
        const auto& nuc {*data::nuclides[i_nuc]};
98,670✔
644
        if (i_nuc == p.event_nuclide() && multipole_in_range(nuc, p.E_last())) {
98,670✔
645
          // phi is proportional to Sigma_s
646
          // (1 / phi) * (d_phi / d_T) = (d_Sigma_s / d_T) / Sigma_s
647
          // (1 / phi) * (d_phi / d_T) = (d_sigma_s / d_T) / sigma_s
648
          const auto& micro_xs {p.neutron_xs(i_nuc)};
14,696✔
649
          double dsig_s, dsig_a, dsig_f;
14,696✔
650
          std::tie(dsig_s, dsig_a, dsig_f) =
14,696✔
651
            nuc.multipole_->evaluate_deriv(p.E_last(), p.sqrtkT());
14,696✔
652
          flux_deriv += dsig_s / (micro_xs.total - micro_xs.absorption);
14,696✔
653
          // Note that this is an approximation!  The real scattering cross
654
          // section is
655
          // Sigma_s(E'->E, u'->u) = Sigma_s(E') * P(E'->E, u'->u).
656
          // We are assuming that d_P(E'->E, u'->u) / d_T = 0 and only
657
          // computing d_S(E') / d_T.  Using this approximation in the vicinity
658
          // of low-energy resonances causes errors (~2-5% for PWR pincell
659
          // eigenvalue derivatives).
660
        }
661
      }
662
      break;
663
    }
664
  }
665
}
666

667
} // namespace openmc
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