Skip to content

Migrate depletion module to C++ part 1 of N - #3986

Open
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation
Open

Migrate depletion module to C++ part 1 of N#3986
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation

Conversation

@eepeterson

@eepetersoneepeterson commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Description

This PR is the first of a handful of steps towards moving the majority of the depletion module to C++. There are a number of reasons why we might want to do that some of which are listed below (there are tradeoffs as well obviously).

  • Obviate the need to use the Python multiprocessing module which can cause installation and runtime headaches particularly on HPC systems.
  • Faster CRAM solves without requiring SuperLU or Eigen libraries as additional dependencies.
  • Easier tight coupling with multiphysics apps that need transmutation using OpenMC.

The general outline of the migration plan is as follows:

  1. Port CRAM solves to C++ (This PR).
  2. Port depletion chain infrastructure to C++
  3. Port reaction rate calculation and matrix formation from tally result infrastructure to C++
  4. Port integration schemes to C++
  5. Refactor python side to simply orchestrate/manage the depletion calculation

Specifically this PR replaces CRAM16 and CRAM48 callables with analogous functions through openmc.lib and the C API. To make this work we need to replace scipy.sparse.linalg.spsolve calls with our own minimal sparse solver specific to CRAM. I debated trying to pull in SuperLU or Eigen for the sparse solve, but after digging into the details of both libraries and what is needed for CRAM specifically, I opted to roll our own solver which has many benefits. In particular, because the IPF form of CRAM has complex values on the diagonal whose imaginary parts are larger than 1, the pivots are unconditionally well-behaved and we don't need any of the partial (or complete) pivoting algorithms employed by SuperLU or Eigen.

The main additions of this PR are the CSCPattern and CSCMatrix classes for the SymbolicLUFactorization of matrices and data storage of burnup matrix elements. Then the BatemanSolver abstract base class and concrete implementation via the IPFCramSolver are added in C++ as mirrors of the current functionality on the Python side. The C++ CRAM solver is made accessible through openmc.lib and it is wired in to take the place of the scipy based solver for the CRAM16 and CRAM48 functions. The numeric linear algebra work specific to CRAM is kept in bateman_solvers.cpp since the numeric_factorize_cram function doesn't build the matrix $A dt - \theta_\ell I$ explicitly and isn't intended to be a generic LU factorization routine but rather does the numeric factorization on the fly specific to the CRAM matrix structure. The symbolic factorization of $A dt - \theta_\ell I$ is done once and reused by each numeric factorization for all the poles of the cram solve. Then triangular_solve_lu ingests the SymbolicLUFactorization and NumericLUFactorization and performs the linear solve for each pole which then get accumulated to compute the final nuclide density from the initial one.

Fixes # (issue)

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@yrrepy

yrrepy commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

May be interesting to compare this with:
https://djsn.dev/post/lessons-porting-c++-cram-python/

@paulromanopaulromano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

@shimwell

Copy link
Copy Markdown
Member

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

This repo has a nice set of benchmarks, perhaps the SS316 steel is a reasonably complex one
https://github.com/jbae11/openmc_activator

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@eepeterson@yrrepy@shimwell@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Migrate depletion module to C++ part 1 of N by eepeterson · Pull Request #3986 · openmc-dev/openmc · GitHub
Skip to content

Migrate depletion module to C++ part 1 of N - #3986

Open
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation
Open

Migrate depletion module to C++ part 1 of N#3986
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation

Conversation

@eepeterson

@eepetersoneepeterson commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Description

This PR is the first of a handful of steps towards moving the majority of the depletion module to C++. There are a number of reasons why we might want to do that some of which are listed below (there are tradeoffs as well obviously).

  • Obviate the need to use the Python multiprocessing module which can cause installation and runtime headaches particularly on HPC systems.
  • Faster CRAM solves without requiring SuperLU or Eigen libraries as additional dependencies.
  • Easier tight coupling with multiphysics apps that need transmutation using OpenMC.

The general outline of the migration plan is as follows:

  1. Port CRAM solves to C++ (This PR).
  2. Port depletion chain infrastructure to C++
  3. Port reaction rate calculation and matrix formation from tally result infrastructure to C++
  4. Port integration schemes to C++
  5. Refactor python side to simply orchestrate/manage the depletion calculation

Specifically this PR replaces CRAM16 and CRAM48 callables with analogous functions through openmc.lib and the C API. To make this work we need to replace scipy.sparse.linalg.spsolve calls with our own minimal sparse solver specific to CRAM. I debated trying to pull in SuperLU or Eigen for the sparse solve, but after digging into the details of both libraries and what is needed for CRAM specifically, I opted to roll our own solver which has many benefits. In particular, because the IPF form of CRAM has complex values on the diagonal whose imaginary parts are larger than 1, the pivots are unconditionally well-behaved and we don't need any of the partial (or complete) pivoting algorithms employed by SuperLU or Eigen.

The main additions of this PR are the CSCPattern and CSCMatrix classes for the SymbolicLUFactorization of matrices and data storage of burnup matrix elements. Then the BatemanSolver abstract base class and concrete implementation via the IPFCramSolver are added in C++ as mirrors of the current functionality on the Python side. The C++ CRAM solver is made accessible through openmc.lib and it is wired in to take the place of the scipy based solver for the CRAM16 and CRAM48 functions. The numeric linear algebra work specific to CRAM is kept in bateman_solvers.cpp since the numeric_factorize_cram function doesn't build the matrix $A dt - \theta_\ell I$ explicitly and isn't intended to be a generic LU factorization routine but rather does the numeric factorization on the fly specific to the CRAM matrix structure. The symbolic factorization of $A dt - \theta_\ell I$ is done once and reused by each numeric factorization for all the poles of the cram solve. Then triangular_solve_lu ingests the SymbolicLUFactorization and NumericLUFactorization and performs the linear solve for each pole which then get accumulated to compute the final nuclide density from the initial one.

Fixes # (issue)

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@yrrepy

yrrepy commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

May be interesting to compare this with:
https://djsn.dev/post/lessons-porting-c++-cram-python/

@paulromanopaulromano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

@shimwell

Copy link
Copy Markdown
Member

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

This repo has a nice set of benchmarks, perhaps the SS316 steel is a reasonably complex one
https://github.com/jbae11/openmc_activator

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@eepeterson@yrrepy@shimwell@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Migrate depletion module to C++ part 1 of N by eepeterson · Pull Request #3986 · openmc-dev/openmc · GitHub
Skip to content

Migrate depletion module to C++ part 1 of N - #3986

Open
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation
Open

Migrate depletion module to C++ part 1 of N#3986
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation

Conversation

@eepeterson

@eepetersoneepeterson commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Description

This PR is the first of a handful of steps towards moving the majority of the depletion module to C++. There are a number of reasons why we might want to do that some of which are listed below (there are tradeoffs as well obviously).

  • Obviate the need to use the Python multiprocessing module which can cause installation and runtime headaches particularly on HPC systems.
  • Faster CRAM solves without requiring SuperLU or Eigen libraries as additional dependencies.
  • Easier tight coupling with multiphysics apps that need transmutation using OpenMC.

The general outline of the migration plan is as follows:

  1. Port CRAM solves to C++ (This PR).
  2. Port depletion chain infrastructure to C++
  3. Port reaction rate calculation and matrix formation from tally result infrastructure to C++
  4. Port integration schemes to C++
  5. Refactor python side to simply orchestrate/manage the depletion calculation

Specifically this PR replaces CRAM16 and CRAM48 callables with analogous functions through openmc.lib and the C API. To make this work we need to replace scipy.sparse.linalg.spsolve calls with our own minimal sparse solver specific to CRAM. I debated trying to pull in SuperLU or Eigen for the sparse solve, but after digging into the details of both libraries and what is needed for CRAM specifically, I opted to roll our own solver which has many benefits. In particular, because the IPF form of CRAM has complex values on the diagonal whose imaginary parts are larger than 1, the pivots are unconditionally well-behaved and we don't need any of the partial (or complete) pivoting algorithms employed by SuperLU or Eigen.

The main additions of this PR are the CSCPattern and CSCMatrix classes for the SymbolicLUFactorization of matrices and data storage of burnup matrix elements. Then the BatemanSolver abstract base class and concrete implementation via the IPFCramSolver are added in C++ as mirrors of the current functionality on the Python side. The C++ CRAM solver is made accessible through openmc.lib and it is wired in to take the place of the scipy based solver for the CRAM16 and CRAM48 functions. The numeric linear algebra work specific to CRAM is kept in bateman_solvers.cpp since the numeric_factorize_cram function doesn't build the matrix $A dt - \theta_\ell I$ explicitly and isn't intended to be a generic LU factorization routine but rather does the numeric factorization on the fly specific to the CRAM matrix structure. The symbolic factorization of $A dt - \theta_\ell I$ is done once and reused by each numeric factorization for all the poles of the cram solve. Then triangular_solve_lu ingests the SymbolicLUFactorization and NumericLUFactorization and performs the linear solve for each pole which then get accumulated to compute the final nuclide density from the initial one.

Fixes # (issue)

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@yrrepy

yrrepy commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

May be interesting to compare this with:
https://djsn.dev/post/lessons-porting-c++-cram-python/

@paulromanopaulromano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

@shimwell

Copy link
Copy Markdown
Member

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

This repo has a nice set of benchmarks, perhaps the SS316 steel is a reasonably complex one
https://github.com/jbae11/openmc_activator

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@eepeterson@yrrepy@shimwell@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Migrate depletion module to C++ part 1 of N by eepeterson · Pull Request #3986 · openmc-dev/openmc · GitHub
Skip to content

Migrate depletion module to C++ part 1 of N - #3986

Open
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation
Open

Migrate depletion module to C++ part 1 of N#3986
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation

Conversation

@eepeterson

@eepetersoneepeterson commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Description

This PR is the first of a handful of steps towards moving the majority of the depletion module to C++. There are a number of reasons why we might want to do that some of which are listed below (there are tradeoffs as well obviously).

  • Obviate the need to use the Python multiprocessing module which can cause installation and runtime headaches particularly on HPC systems.
  • Faster CRAM solves without requiring SuperLU or Eigen libraries as additional dependencies.
  • Easier tight coupling with multiphysics apps that need transmutation using OpenMC.

The general outline of the migration plan is as follows:

  1. Port CRAM solves to C++ (This PR).
  2. Port depletion chain infrastructure to C++
  3. Port reaction rate calculation and matrix formation from tally result infrastructure to C++
  4. Port integration schemes to C++
  5. Refactor python side to simply orchestrate/manage the depletion calculation

Specifically this PR replaces CRAM16 and CRAM48 callables with analogous functions through openmc.lib and the C API. To make this work we need to replace scipy.sparse.linalg.spsolve calls with our own minimal sparse solver specific to CRAM. I debated trying to pull in SuperLU or Eigen for the sparse solve, but after digging into the details of both libraries and what is needed for CRAM specifically, I opted to roll our own solver which has many benefits. In particular, because the IPF form of CRAM has complex values on the diagonal whose imaginary parts are larger than 1, the pivots are unconditionally well-behaved and we don't need any of the partial (or complete) pivoting algorithms employed by SuperLU or Eigen.

The main additions of this PR are the CSCPattern and CSCMatrix classes for the SymbolicLUFactorization of matrices and data storage of burnup matrix elements. Then the BatemanSolver abstract base class and concrete implementation via the IPFCramSolver are added in C++ as mirrors of the current functionality on the Python side. The C++ CRAM solver is made accessible through openmc.lib and it is wired in to take the place of the scipy based solver for the CRAM16 and CRAM48 functions. The numeric linear algebra work specific to CRAM is kept in bateman_solvers.cpp since the numeric_factorize_cram function doesn't build the matrix $A dt - \theta_\ell I$ explicitly and isn't intended to be a generic LU factorization routine but rather does the numeric factorization on the fly specific to the CRAM matrix structure. The symbolic factorization of $A dt - \theta_\ell I$ is done once and reused by each numeric factorization for all the poles of the cram solve. Then triangular_solve_lu ingests the SymbolicLUFactorization and NumericLUFactorization and performs the linear solve for each pole which then get accumulated to compute the final nuclide density from the initial one.

Fixes # (issue)

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@yrrepy

yrrepy commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

May be interesting to compare this with:
https://djsn.dev/post/lessons-porting-c++-cram-python/

@paulromanopaulromano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

@shimwell

Copy link
Copy Markdown
Member

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

This repo has a nice set of benchmarks, perhaps the SS316 steel is a reasonably complex one
https://github.com/jbae11/openmc_activator

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@eepeterson@yrrepy@shimwell@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Migrate depletion module to C++ part 1 of N by eepeterson · Pull Request #3986 · openmc-dev/openmc · GitHub
Skip to content

Migrate depletion module to C++ part 1 of N - #3986

Open
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation
Open

Migrate depletion module to C++ part 1 of N#3986
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation

Conversation

@eepeterson

@eepetersoneepeterson commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Description

This PR is the first of a handful of steps towards moving the majority of the depletion module to C++. There are a number of reasons why we might want to do that some of which are listed below (there are tradeoffs as well obviously).

  • Obviate the need to use the Python multiprocessing module which can cause installation and runtime headaches particularly on HPC systems.
  • Faster CRAM solves without requiring SuperLU or Eigen libraries as additional dependencies.
  • Easier tight coupling with multiphysics apps that need transmutation using OpenMC.

The general outline of the migration plan is as follows:

  1. Port CRAM solves to C++ (This PR).
  2. Port depletion chain infrastructure to C++
  3. Port reaction rate calculation and matrix formation from tally result infrastructure to C++
  4. Port integration schemes to C++
  5. Refactor python side to simply orchestrate/manage the depletion calculation

Specifically this PR replaces CRAM16 and CRAM48 callables with analogous functions through openmc.lib and the C API. To make this work we need to replace scipy.sparse.linalg.spsolve calls with our own minimal sparse solver specific to CRAM. I debated trying to pull in SuperLU or Eigen for the sparse solve, but after digging into the details of both libraries and what is needed for CRAM specifically, I opted to roll our own solver which has many benefits. In particular, because the IPF form of CRAM has complex values on the diagonal whose imaginary parts are larger than 1, the pivots are unconditionally well-behaved and we don't need any of the partial (or complete) pivoting algorithms employed by SuperLU or Eigen.

The main additions of this PR are the CSCPattern and CSCMatrix classes for the SymbolicLUFactorization of matrices and data storage of burnup matrix elements. Then the BatemanSolver abstract base class and concrete implementation via the IPFCramSolver are added in C++ as mirrors of the current functionality on the Python side. The C++ CRAM solver is made accessible through openmc.lib and it is wired in to take the place of the scipy based solver for the CRAM16 and CRAM48 functions. The numeric linear algebra work specific to CRAM is kept in bateman_solvers.cpp since the numeric_factorize_cram function doesn't build the matrix $A dt - \theta_\ell I$ explicitly and isn't intended to be a generic LU factorization routine but rather does the numeric factorization on the fly specific to the CRAM matrix structure. The symbolic factorization of $A dt - \theta_\ell I$ is done once and reused by each numeric factorization for all the poles of the cram solve. Then triangular_solve_lu ingests the SymbolicLUFactorization and NumericLUFactorization and performs the linear solve for each pole which then get accumulated to compute the final nuclide density from the initial one.

Fixes # (issue)

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@yrrepy

yrrepy commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

May be interesting to compare this with:
https://djsn.dev/post/lessons-porting-c++-cram-python/

@paulromanopaulromano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

@shimwell

Copy link
Copy Markdown
Member

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

This repo has a nice set of benchmarks, perhaps the SS316 steel is a reasonably complex one
https://github.com/jbae11/openmc_activator

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@eepeterson@yrrepy@shimwell@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Migrate depletion module to C++ part 1 of N by eepeterson · Pull Request #3986 · openmc-dev/openmc · GitHub
Skip to content

Migrate depletion module to C++ part 1 of N - #3986

Open
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation
Open

Migrate depletion module to C++ part 1 of N#3986
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation

Conversation

@eepeterson

@eepetersoneepeterson commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Description

This PR is the first of a handful of steps towards moving the majority of the depletion module to C++. There are a number of reasons why we might want to do that some of which are listed below (there are tradeoffs as well obviously).

  • Obviate the need to use the Python multiprocessing module which can cause installation and runtime headaches particularly on HPC systems.
  • Faster CRAM solves without requiring SuperLU or Eigen libraries as additional dependencies.
  • Easier tight coupling with multiphysics apps that need transmutation using OpenMC.

The general outline of the migration plan is as follows:

  1. Port CRAM solves to C++ (This PR).
  2. Port depletion chain infrastructure to C++
  3. Port reaction rate calculation and matrix formation from tally result infrastructure to C++
  4. Port integration schemes to C++
  5. Refactor python side to simply orchestrate/manage the depletion calculation

Specifically this PR replaces CRAM16 and CRAM48 callables with analogous functions through openmc.lib and the C API. To make this work we need to replace scipy.sparse.linalg.spsolve calls with our own minimal sparse solver specific to CRAM. I debated trying to pull in SuperLU or Eigen for the sparse solve, but after digging into the details of both libraries and what is needed for CRAM specifically, I opted to roll our own solver which has many benefits. In particular, because the IPF form of CRAM has complex values on the diagonal whose imaginary parts are larger than 1, the pivots are unconditionally well-behaved and we don't need any of the partial (or complete) pivoting algorithms employed by SuperLU or Eigen.

The main additions of this PR are the CSCPattern and CSCMatrix classes for the SymbolicLUFactorization of matrices and data storage of burnup matrix elements. Then the BatemanSolver abstract base class and concrete implementation via the IPFCramSolver are added in C++ as mirrors of the current functionality on the Python side. The C++ CRAM solver is made accessible through openmc.lib and it is wired in to take the place of the scipy based solver for the CRAM16 and CRAM48 functions. The numeric linear algebra work specific to CRAM is kept in bateman_solvers.cpp since the numeric_factorize_cram function doesn't build the matrix $A dt - \theta_\ell I$ explicitly and isn't intended to be a generic LU factorization routine but rather does the numeric factorization on the fly specific to the CRAM matrix structure. The symbolic factorization of $A dt - \theta_\ell I$ is done once and reused by each numeric factorization for all the poles of the cram solve. Then triangular_solve_lu ingests the SymbolicLUFactorization and NumericLUFactorization and performs the linear solve for each pole which then get accumulated to compute the final nuclide density from the initial one.

Fixes # (issue)

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@yrrepy

yrrepy commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

May be interesting to compare this with:
https://djsn.dev/post/lessons-porting-c++-cram-python/

@paulromanopaulromano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

@shimwell

Copy link
Copy Markdown
Member

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

This repo has a nice set of benchmarks, perhaps the SS316 steel is a reasonably complex one
https://github.com/jbae11/openmc_activator

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@eepeterson@yrrepy@shimwell@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Migrate depletion module to C++ part 1 of N by eepeterson · Pull Request #3986 · openmc-dev/openmc · GitHub
Skip to content

Migrate depletion module to C++ part 1 of N - #3986

Open
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation
Open

Migrate depletion module to C++ part 1 of N#3986
eepeterson wants to merge 18 commits into
openmc-dev:developfrom
eepeterson:initial_cpp_cram_implementation

Conversation

@eepeterson

@eepetersoneepeterson commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Description

This PR is the first of a handful of steps towards moving the majority of the depletion module to C++. There are a number of reasons why we might want to do that some of which are listed below (there are tradeoffs as well obviously).

  • Obviate the need to use the Python multiprocessing module which can cause installation and runtime headaches particularly on HPC systems.
  • Faster CRAM solves without requiring SuperLU or Eigen libraries as additional dependencies.
  • Easier tight coupling with multiphysics apps that need transmutation using OpenMC.

The general outline of the migration plan is as follows:

  1. Port CRAM solves to C++ (This PR).
  2. Port depletion chain infrastructure to C++
  3. Port reaction rate calculation and matrix formation from tally result infrastructure to C++
  4. Port integration schemes to C++
  5. Refactor python side to simply orchestrate/manage the depletion calculation

Specifically this PR replaces CRAM16 and CRAM48 callables with analogous functions through openmc.lib and the C API. To make this work we need to replace scipy.sparse.linalg.spsolve calls with our own minimal sparse solver specific to CRAM. I debated trying to pull in SuperLU or Eigen for the sparse solve, but after digging into the details of both libraries and what is needed for CRAM specifically, I opted to roll our own solver which has many benefits. In particular, because the IPF form of CRAM has complex values on the diagonal whose imaginary parts are larger than 1, the pivots are unconditionally well-behaved and we don't need any of the partial (or complete) pivoting algorithms employed by SuperLU or Eigen.

The main additions of this PR are the CSCPattern and CSCMatrix classes for the SymbolicLUFactorization of matrices and data storage of burnup matrix elements. Then the BatemanSolver abstract base class and concrete implementation via the IPFCramSolver are added in C++ as mirrors of the current functionality on the Python side. The C++ CRAM solver is made accessible through openmc.lib and it is wired in to take the place of the scipy based solver for the CRAM16 and CRAM48 functions. The numeric linear algebra work specific to CRAM is kept in bateman_solvers.cpp since the numeric_factorize_cram function doesn't build the matrix $A dt - \theta_\ell I$ explicitly and isn't intended to be a generic LU factorization routine but rather does the numeric factorization on the fly specific to the CRAM matrix structure. The symbolic factorization of $A dt - \theta_\ell I$ is done once and reused by each numeric factorization for all the poles of the cram solve. Then triangular_solve_lu ingests the SymbolicLUFactorization and NumericLUFactorization and performs the linear solve for each pole which then get accumulated to compute the final nuclide density from the initial one.

Fixes # (issue)

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@yrrepy

yrrepy commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

May be interesting to compare this with:
https://djsn.dev/post/lessons-porting-c++-cram-python/

@paulromanopaulromano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

@shimwell

Copy link
Copy Markdown
Member

@eepeterson Thanks a ton for the PR! I haven't had a chance to look through in full yet, but right off the bat it would be very useful if you could share if you've done any comparisons on the accuracy of CRAM solutions for realistic systems as well as performance testing versus the current develop branch.

This repo has a nice set of benchmarks, perhaps the SS316 steel is a reasonably complex one
https://github.com/jbae11/openmc_activator

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@eepeterson@yrrepy@shimwell@paulromano