Ladder fit silently keeps the initial potential's rcut and ignores bonds::rcut from input.yaml #105

Description

@yury-lysogorskiy

When initial_potential is combined with a potential specification whose bonds::rcut differs from the initial potential's, the initial value silently wins. The fitted potential then has a cutoff the user never asked for, and nothing warns or raises.

This was found by reading the code — the test suite was not run — so please sanity-check the reproduction before acting on it.

Mechanism

extend_basis_block() in src/pyace/multispecies_basisextension.py:934 starts from the initial block:

extended_block=init_block.copy()

Every block that exists in the initial potential therefore keeps all of the initial block's radial parameters — rcutij, dcutij, radbase, radparameters, r_in, delta_in — and only receives the target's function specifications grafted on. The target's radial parameters are never applied. The only assertion in that function (line 909) compares block_name; nothing compares the radial basis.

ACEBBasisSet then builds map_bond_specifications exclusively from blocks with number_of_species <= 2 (lib/ace/src/ace/ace_b_basis.cpp:975) and sets

radial_functions->cut(ele_i, ele_j) = bondSpecification.rcut; // ace_b_basis.cpp:1023

so the pair block's value — inherited from the initial potential — is what the evaluator actually uses.

Why nothing catches it

  • Mixed rcut within one configuration. Blocks present in the initial potential keep its value (:934); blocks that exist only in the target are created fresh at multispecies_basisextension.py:1122 from f_block.copy() and keep the target's. So an extended configuration can hold Al Al at 7 Å and Al Al Al at 5 Å. The higher-rank block's rcutij is skipped for bond specifications by the continue at ace_b_basis.cpp:975, but still counts toward cutoffmax at :890, which is unfiltered.
  • Input::check_radial_basis_consistency — the one function that compares these fields — is called from a single place, lib/ace/src/ace/ace_yaml_input.cpp:331, during YAML file parsing, and only between a pair block and its reversed dual (Al Ni vs Ni Al). It never runs on configurations assembled in memory by the ladder.
  • BBasisConfiguration::validate (ace_b_basis.cpp:1312) only tries to construct an ACEBBasisSet; that constructor's consistency check (:1009-1017) compares (i,j) against (j,i), both inherited from the same block, so it always passes.

The same override happens outside ladder fitting through the overwrite_blocks_from_initial_bbasis branch at multispecies_basisextension.py:352-358, which replaces generated blocks with the initial ones wholesale.

Reproduction

  1. Fit any potential with bonds: {ALL: {rcut: 7, ...}} and keep output_potential.yaml.
  2. Write a new input.yaml with bonds: {ALL: {rcut: 5, ...}}, potential::initial_potential: output_potential.yaml and a fit::ladder_step.
  3. Run pacemaker. The resulting potential has rcutij: 7, not 5.

A third symptom is worth noting: generalfit.py:256 writes target_potential.yaml carrying rcut: 5, while the fit and the final output potential use 7. The two files on disk disagree and neither says so.

Suggested fix

docs/pacemaker/faq.md:165 already states the rule:

If you change cutoff, i.e. from rcut: 7 to rcut: 6.5, then potential should be refitted from the scratch.

That is the right behaviour — an initial potential's coefficients are meaningless at a different cutoff, so this is not recoverable by picking one of the two values. The documentation states the rule; the code just does not enforce it. A check next to the existing block_name assertion at multispecies_basisextension.py:909, comparing init_block against final_block and raising with the block name and both values, would cover it.

One trap for whoever implements it: do not reuse check_radial_basis_consistency's full field list. It also compares lmaxi, nradmaxi, nradbaseij and radcoefficients, and those are supposed to grow from initial to target — that is the point of the ladder (validate_radial_shape_from_funcs at :944 recomputes them, merge_crad_matrix at :949 merges the coefficients). Only the shape-independent parameters must match: rcutij, dcutij, radbase, radparameters, r_in, delta_in, inner_cutoff_type.

Found while investigating the top-level cutoff parameter (see #104), but independent of it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      Ladder fit silently keeps the initial potential's rcut and ignores bonds::rcut from input.yaml #105

      Description

      @yury-lysogorskiy

      When initial_potential is combined with a potential specification whose bonds::rcut differs from the initial potential's, the initial value silently wins. The fitted potential then has a cutoff the user never asked for, and nothing warns or raises.

      This was found by reading the code — the test suite was not run — so please sanity-check the reproduction before acting on it.

      Mechanism

      extend_basis_block() in src/pyace/multispecies_basisextension.py:934 starts from the initial block:

      extended_block=init_block.copy()

      Every block that exists in the initial potential therefore keeps all of the initial block's radial parameters — rcutij, dcutij, radbase, radparameters, r_in, delta_in — and only receives the target's function specifications grafted on. The target's radial parameters are never applied. The only assertion in that function (line 909) compares block_name; nothing compares the radial basis.

      ACEBBasisSet then builds map_bond_specifications exclusively from blocks with number_of_species <= 2 (lib/ace/src/ace/ace_b_basis.cpp:975) and sets

      radial_functions->cut(ele_i, ele_j) = bondSpecification.rcut; // ace_b_basis.cpp:1023

      so the pair block's value — inherited from the initial potential — is what the evaluator actually uses.

      Why nothing catches it

      • Mixed rcut within one configuration. Blocks present in the initial potential keep its value (:934); blocks that exist only in the target are created fresh at multispecies_basisextension.py:1122 from f_block.copy() and keep the target's. So an extended configuration can hold Al Al at 7 Å and Al Al Al at 5 Å. The higher-rank block's rcutij is skipped for bond specifications by the continue at ace_b_basis.cpp:975, but still counts toward cutoffmax at :890, which is unfiltered.
      • Input::check_radial_basis_consistency — the one function that compares these fields — is called from a single place, lib/ace/src/ace/ace_yaml_input.cpp:331, during YAML file parsing, and only between a pair block and its reversed dual (Al Ni vs Ni Al). It never runs on configurations assembled in memory by the ladder.
      • BBasisConfiguration::validate (ace_b_basis.cpp:1312) only tries to construct an ACEBBasisSet; that constructor's consistency check (:1009-1017) compares (i,j) against (j,i), both inherited from the same block, so it always passes.

      The same override happens outside ladder fitting through the overwrite_blocks_from_initial_bbasis branch at multispecies_basisextension.py:352-358, which replaces generated blocks with the initial ones wholesale.

      Reproduction

      1. Fit any potential with bonds: {ALL: {rcut: 7, ...}} and keep output_potential.yaml.
      2. Write a new input.yaml with bonds: {ALL: {rcut: 5, ...}}, potential::initial_potential: output_potential.yaml and a fit::ladder_step.
      3. Run pacemaker. The resulting potential has rcutij: 7, not 5.

      A third symptom is worth noting: generalfit.py:256 writes target_potential.yaml carrying rcut: 5, while the fit and the final output potential use 7. The two files on disk disagree and neither says so.

      Suggested fix

      docs/pacemaker/faq.md:165 already states the rule:

      If you change cutoff, i.e. from rcut: 7 to rcut: 6.5, then potential should be refitted from the scratch.

      That is the right behaviour — an initial potential's coefficients are meaningless at a different cutoff, so this is not recoverable by picking one of the two values. The documentation states the rule; the code just does not enforce it. A check next to the existing block_name assertion at multispecies_basisextension.py:909, comparing init_block against final_block and raising with the block name and both values, would cover it.

      One trap for whoever implements it: do not reuse check_radial_basis_consistency's full field list. It also compares lmaxi, nradmaxi, nradbaseij and radcoefficients, and those are supposed to grow from initial to target — that is the point of the ladder (validate_radial_shape_from_funcs at :944 recomputes them, merge_crad_matrix at :949 merges the coefficients). Only the shape-independent parameters must match: rcutij, dcutij, radbase, radparameters, r_in, delta_in, inner_cutoff_type.

      Found while investigating the top-level cutoff parameter (see #104), but independent of it.

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          Ladder fit silently keeps the initial potential's rcut and ignores bonds::rcut from input.yaml #105

          Description

          @yury-lysogorskiy

          When initial_potential is combined with a potential specification whose bonds::rcut differs from the initial potential's, the initial value silently wins. The fitted potential then has a cutoff the user never asked for, and nothing warns or raises.

          This was found by reading the code — the test suite was not run — so please sanity-check the reproduction before acting on it.

          Mechanism

          extend_basis_block() in src/pyace/multispecies_basisextension.py:934 starts from the initial block:

          extended_block=init_block.copy()

          Every block that exists in the initial potential therefore keeps all of the initial block's radial parameters — rcutij, dcutij, radbase, radparameters, r_in, delta_in — and only receives the target's function specifications grafted on. The target's radial parameters are never applied. The only assertion in that function (line 909) compares block_name; nothing compares the radial basis.

          ACEBBasisSet then builds map_bond_specifications exclusively from blocks with number_of_species <= 2 (lib/ace/src/ace/ace_b_basis.cpp:975) and sets

          radial_functions->cut(ele_i, ele_j) = bondSpecification.rcut; // ace_b_basis.cpp:1023

          so the pair block's value — inherited from the initial potential — is what the evaluator actually uses.

          Why nothing catches it

          • Mixed rcut within one configuration. Blocks present in the initial potential keep its value (:934); blocks that exist only in the target are created fresh at multispecies_basisextension.py:1122 from f_block.copy() and keep the target's. So an extended configuration can hold Al Al at 7 Å and Al Al Al at 5 Å. The higher-rank block's rcutij is skipped for bond specifications by the continue at ace_b_basis.cpp:975, but still counts toward cutoffmax at :890, which is unfiltered.
          • Input::check_radial_basis_consistency — the one function that compares these fields — is called from a single place, lib/ace/src/ace/ace_yaml_input.cpp:331, during YAML file parsing, and only between a pair block and its reversed dual (Al Ni vs Ni Al). It never runs on configurations assembled in memory by the ladder.
          • BBasisConfiguration::validate (ace_b_basis.cpp:1312) only tries to construct an ACEBBasisSet; that constructor's consistency check (:1009-1017) compares (i,j) against (j,i), both inherited from the same block, so it always passes.

          The same override happens outside ladder fitting through the overwrite_blocks_from_initial_bbasis branch at multispecies_basisextension.py:352-358, which replaces generated blocks with the initial ones wholesale.

          Reproduction

          1. Fit any potential with bonds: {ALL: {rcut: 7, ...}} and keep output_potential.yaml.
          2. Write a new input.yaml with bonds: {ALL: {rcut: 5, ...}}, potential::initial_potential: output_potential.yaml and a fit::ladder_step.
          3. Run pacemaker. The resulting potential has rcutij: 7, not 5.

          A third symptom is worth noting: generalfit.py:256 writes target_potential.yaml carrying rcut: 5, while the fit and the final output potential use 7. The two files on disk disagree and neither says so.

          Suggested fix

          docs/pacemaker/faq.md:165 already states the rule:

          If you change cutoff, i.e. from rcut: 7 to rcut: 6.5, then potential should be refitted from the scratch.

          That is the right behaviour — an initial potential's coefficients are meaningless at a different cutoff, so this is not recoverable by picking one of the two values. The documentation states the rule; the code just does not enforce it. A check next to the existing block_name assertion at multispecies_basisextension.py:909, comparing init_block against final_block and raising with the block name and both values, would cover it.

          One trap for whoever implements it: do not reuse check_radial_basis_consistency's full field list. It also compares lmaxi, nradmaxi, nradbaseij and radcoefficients, and those are supposed to grow from initial to target — that is the point of the ladder (validate_radial_shape_from_funcs at :944 recomputes them, merge_crad_matrix at :949 merges the coefficients). Only the shape-independent parameters must match: rcutij, dcutij, radbase, radparameters, r_in, delta_in, inner_cutoff_type.

          Found while investigating the top-level cutoff parameter (see #104), but independent of it.

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

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

              Ladder fit silently keeps the initial potential's rcut and ignores bonds::rcut from input.yaml #105

              Description

              @yury-lysogorskiy

              When initial_potential is combined with a potential specification whose bonds::rcut differs from the initial potential's, the initial value silently wins. The fitted potential then has a cutoff the user never asked for, and nothing warns or raises.

              This was found by reading the code — the test suite was not run — so please sanity-check the reproduction before acting on it.

              Mechanism

              extend_basis_block() in src/pyace/multispecies_basisextension.py:934 starts from the initial block:

              extended_block=init_block.copy()

              Every block that exists in the initial potential therefore keeps all of the initial block's radial parameters — rcutij, dcutij, radbase, radparameters, r_in, delta_in — and only receives the target's function specifications grafted on. The target's radial parameters are never applied. The only assertion in that function (line 909) compares block_name; nothing compares the radial basis.

              ACEBBasisSet then builds map_bond_specifications exclusively from blocks with number_of_species <= 2 (lib/ace/src/ace/ace_b_basis.cpp:975) and sets

              radial_functions->cut(ele_i, ele_j) = bondSpecification.rcut; // ace_b_basis.cpp:1023

              so the pair block's value — inherited from the initial potential — is what the evaluator actually uses.

              Why nothing catches it

              • Mixed rcut within one configuration. Blocks present in the initial potential keep its value (:934); blocks that exist only in the target are created fresh at multispecies_basisextension.py:1122 from f_block.copy() and keep the target's. So an extended configuration can hold Al Al at 7 Å and Al Al Al at 5 Å. The higher-rank block's rcutij is skipped for bond specifications by the continue at ace_b_basis.cpp:975, but still counts toward cutoffmax at :890, which is unfiltered.
              • Input::check_radial_basis_consistency — the one function that compares these fields — is called from a single place, lib/ace/src/ace/ace_yaml_input.cpp:331, during YAML file parsing, and only between a pair block and its reversed dual (Al Ni vs Ni Al). It never runs on configurations assembled in memory by the ladder.
              • BBasisConfiguration::validate (ace_b_basis.cpp:1312) only tries to construct an ACEBBasisSet; that constructor's consistency check (:1009-1017) compares (i,j) against (j,i), both inherited from the same block, so it always passes.

              The same override happens outside ladder fitting through the overwrite_blocks_from_initial_bbasis branch at multispecies_basisextension.py:352-358, which replaces generated blocks with the initial ones wholesale.

              Reproduction

              1. Fit any potential with bonds: {ALL: {rcut: 7, ...}} and keep output_potential.yaml.
              2. Write a new input.yaml with bonds: {ALL: {rcut: 5, ...}}, potential::initial_potential: output_potential.yaml and a fit::ladder_step.
              3. Run pacemaker. The resulting potential has rcutij: 7, not 5.

              A third symptom is worth noting: generalfit.py:256 writes target_potential.yaml carrying rcut: 5, while the fit and the final output potential use 7. The two files on disk disagree and neither says so.

              Suggested fix

              docs/pacemaker/faq.md:165 already states the rule:

              If you change cutoff, i.e. from rcut: 7 to rcut: 6.5, then potential should be refitted from the scratch.

              That is the right behaviour — an initial potential's coefficients are meaningless at a different cutoff, so this is not recoverable by picking one of the two values. The documentation states the rule; the code just does not enforce it. A check next to the existing block_name assertion at multispecies_basisextension.py:909, comparing init_block against final_block and raising with the block name and both values, would cover it.

              One trap for whoever implements it: do not reuse check_radial_basis_consistency's full field list. It also compares lmaxi, nradmaxi, nradbaseij and radcoefficients, and those are supposed to grow from initial to target — that is the point of the ladder (validate_radial_shape_from_funcs at :944 recomputes them, merge_crad_matrix at :949 merges the coefficients). Only the shape-independent parameters must match: rcutij, dcutij, radbase, radparameters, r_in, delta_in, inner_cutoff_type.

              Found while investigating the top-level cutoff parameter (see #104), but independent of it.

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

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

                  Ladder fit silently keeps the initial potential's rcut and ignores bonds::rcut from input.yaml #105

                  Description

                  @yury-lysogorskiy

                  When initial_potential is combined with a potential specification whose bonds::rcut differs from the initial potential's, the initial value silently wins. The fitted potential then has a cutoff the user never asked for, and nothing warns or raises.

                  This was found by reading the code — the test suite was not run — so please sanity-check the reproduction before acting on it.

                  Mechanism

                  extend_basis_block() in src/pyace/multispecies_basisextension.py:934 starts from the initial block:

                  extended_block=init_block.copy()

                  Every block that exists in the initial potential therefore keeps all of the initial block's radial parameters — rcutij, dcutij, radbase, radparameters, r_in, delta_in — and only receives the target's function specifications grafted on. The target's radial parameters are never applied. The only assertion in that function (line 909) compares block_name; nothing compares the radial basis.

                  ACEBBasisSet then builds map_bond_specifications exclusively from blocks with number_of_species <= 2 (lib/ace/src/ace/ace_b_basis.cpp:975) and sets

                  radial_functions->cut(ele_i, ele_j) = bondSpecification.rcut; // ace_b_basis.cpp:1023

                  so the pair block's value — inherited from the initial potential — is what the evaluator actually uses.

                  Why nothing catches it

                  • Mixed rcut within one configuration. Blocks present in the initial potential keep its value (:934); blocks that exist only in the target are created fresh at multispecies_basisextension.py:1122 from f_block.copy() and keep the target's. So an extended configuration can hold Al Al at 7 Å and Al Al Al at 5 Å. The higher-rank block's rcutij is skipped for bond specifications by the continue at ace_b_basis.cpp:975, but still counts toward cutoffmax at :890, which is unfiltered.
                  • Input::check_radial_basis_consistency — the one function that compares these fields — is called from a single place, lib/ace/src/ace/ace_yaml_input.cpp:331, during YAML file parsing, and only between a pair block and its reversed dual (Al Ni vs Ni Al). It never runs on configurations assembled in memory by the ladder.
                  • BBasisConfiguration::validate (ace_b_basis.cpp:1312) only tries to construct an ACEBBasisSet; that constructor's consistency check (:1009-1017) compares (i,j) against (j,i), both inherited from the same block, so it always passes.

                  The same override happens outside ladder fitting through the overwrite_blocks_from_initial_bbasis branch at multispecies_basisextension.py:352-358, which replaces generated blocks with the initial ones wholesale.

                  Reproduction

                  1. Fit any potential with bonds: {ALL: {rcut: 7, ...}} and keep output_potential.yaml.
                  2. Write a new input.yaml with bonds: {ALL: {rcut: 5, ...}}, potential::initial_potential: output_potential.yaml and a fit::ladder_step.
                  3. Run pacemaker. The resulting potential has rcutij: 7, not 5.

                  A third symptom is worth noting: generalfit.py:256 writes target_potential.yaml carrying rcut: 5, while the fit and the final output potential use 7. The two files on disk disagree and neither says so.

                  Suggested fix

                  docs/pacemaker/faq.md:165 already states the rule:

                  If you change cutoff, i.e. from rcut: 7 to rcut: 6.5, then potential should be refitted from the scratch.

                  That is the right behaviour — an initial potential's coefficients are meaningless at a different cutoff, so this is not recoverable by picking one of the two values. The documentation states the rule; the code just does not enforce it. A check next to the existing block_name assertion at multispecies_basisextension.py:909, comparing init_block against final_block and raising with the block name and both values, would cover it.

                  One trap for whoever implements it: do not reuse check_radial_basis_consistency's full field list. It also compares lmaxi, nradmaxi, nradbaseij and radcoefficients, and those are supposed to grow from initial to target — that is the point of the ladder (validate_radial_shape_from_funcs at :944 recomputes them, merge_crad_matrix at :949 merges the coefficients). Only the shape-independent parameters must match: rcutij, dcutij, radbase, radparameters, r_in, delta_in, inner_cutoff_type.

                  Found while investigating the top-level cutoff parameter (see #104), but independent of it.

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      Ladder fit silently keeps the initial potential's rcut and ignores bonds::rcut from input.yaml #105

                      Description

                      @yury-lysogorskiy

                      When initial_potential is combined with a potential specification whose bonds::rcut differs from the initial potential's, the initial value silently wins. The fitted potential then has a cutoff the user never asked for, and nothing warns or raises.

                      This was found by reading the code — the test suite was not run — so please sanity-check the reproduction before acting on it.

                      Mechanism

                      extend_basis_block() in src/pyace/multispecies_basisextension.py:934 starts from the initial block:

                      extended_block=init_block.copy()

                      Every block that exists in the initial potential therefore keeps all of the initial block's radial parameters — rcutij, dcutij, radbase, radparameters, r_in, delta_in — and only receives the target's function specifications grafted on. The target's radial parameters are never applied. The only assertion in that function (line 909) compares block_name; nothing compares the radial basis.

                      ACEBBasisSet then builds map_bond_specifications exclusively from blocks with number_of_species <= 2 (lib/ace/src/ace/ace_b_basis.cpp:975) and sets

                      radial_functions->cut(ele_i, ele_j) = bondSpecification.rcut; // ace_b_basis.cpp:1023

                      so the pair block's value — inherited from the initial potential — is what the evaluator actually uses.

                      Why nothing catches it

                      • Mixed rcut within one configuration. Blocks present in the initial potential keep its value (:934); blocks that exist only in the target are created fresh at multispecies_basisextension.py:1122 from f_block.copy() and keep the target's. So an extended configuration can hold Al Al at 7 Å and Al Al Al at 5 Å. The higher-rank block's rcutij is skipped for bond specifications by the continue at ace_b_basis.cpp:975, but still counts toward cutoffmax at :890, which is unfiltered.
                      • Input::check_radial_basis_consistency — the one function that compares these fields — is called from a single place, lib/ace/src/ace/ace_yaml_input.cpp:331, during YAML file parsing, and only between a pair block and its reversed dual (Al Ni vs Ni Al). It never runs on configurations assembled in memory by the ladder.
                      • BBasisConfiguration::validate (ace_b_basis.cpp:1312) only tries to construct an ACEBBasisSet; that constructor's consistency check (:1009-1017) compares (i,j) against (j,i), both inherited from the same block, so it always passes.

                      The same override happens outside ladder fitting through the overwrite_blocks_from_initial_bbasis branch at multispecies_basisextension.py:352-358, which replaces generated blocks with the initial ones wholesale.

                      Reproduction

                      1. Fit any potential with bonds: {ALL: {rcut: 7, ...}} and keep output_potential.yaml.
                      2. Write a new input.yaml with bonds: {ALL: {rcut: 5, ...}}, potential::initial_potential: output_potential.yaml and a fit::ladder_step.
                      3. Run pacemaker. The resulting potential has rcutij: 7, not 5.

                      A third symptom is worth noting: generalfit.py:256 writes target_potential.yaml carrying rcut: 5, while the fit and the final output potential use 7. The two files on disk disagree and neither says so.

                      Suggested fix

                      docs/pacemaker/faq.md:165 already states the rule:

                      If you change cutoff, i.e. from rcut: 7 to rcut: 6.5, then potential should be refitted from the scratch.

                      That is the right behaviour — an initial potential's coefficients are meaningless at a different cutoff, so this is not recoverable by picking one of the two values. The documentation states the rule; the code just does not enforce it. A check next to the existing block_name assertion at multispecies_basisextension.py:909, comparing init_block against final_block and raising with the block name and both values, would cover it.

                      One trap for whoever implements it: do not reuse check_radial_basis_consistency's full field list. It also compares lmaxi, nradmaxi, nradbaseij and radcoefficients, and those are supposed to grow from initial to target — that is the point of the ladder (validate_radial_shape_from_funcs at :944 recomputes them, merge_crad_matrix at :949 merges the coefficients). Only the shape-independent parameters must match: rcutij, dcutij, radbase, radparameters, r_in, delta_in, inner_cutoff_type.

                      Found while investigating the top-level cutoff parameter (see #104), but independent of it.

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          Ladder fit silently keeps the initial potential's rcut and ignores bonds::rcut from input.yaml #105

                          Description

                          @yury-lysogorskiy

                          When initial_potential is combined with a potential specification whose bonds::rcut differs from the initial potential's, the initial value silently wins. The fitted potential then has a cutoff the user never asked for, and nothing warns or raises.

                          This was found by reading the code — the test suite was not run — so please sanity-check the reproduction before acting on it.

                          Mechanism

                          extend_basis_block() in src/pyace/multispecies_basisextension.py:934 starts from the initial block:

                          extended_block=init_block.copy()

                          Every block that exists in the initial potential therefore keeps all of the initial block's radial parameters — rcutij, dcutij, radbase, radparameters, r_in, delta_in — and only receives the target's function specifications grafted on. The target's radial parameters are never applied. The only assertion in that function (line 909) compares block_name; nothing compares the radial basis.

                          ACEBBasisSet then builds map_bond_specifications exclusively from blocks with number_of_species <= 2 (lib/ace/src/ace/ace_b_basis.cpp:975) and sets

                          radial_functions->cut(ele_i, ele_j) = bondSpecification.rcut; // ace_b_basis.cpp:1023

                          so the pair block's value — inherited from the initial potential — is what the evaluator actually uses.

                          Why nothing catches it

                          • Mixed rcut within one configuration. Blocks present in the initial potential keep its value (:934); blocks that exist only in the target are created fresh at multispecies_basisextension.py:1122 from f_block.copy() and keep the target's. So an extended configuration can hold Al Al at 7 Å and Al Al Al at 5 Å. The higher-rank block's rcutij is skipped for bond specifications by the continue at ace_b_basis.cpp:975, but still counts toward cutoffmax at :890, which is unfiltered.
                          • Input::check_radial_basis_consistency — the one function that compares these fields — is called from a single place, lib/ace/src/ace/ace_yaml_input.cpp:331, during YAML file parsing, and only between a pair block and its reversed dual (Al Ni vs Ni Al). It never runs on configurations assembled in memory by the ladder.
                          • BBasisConfiguration::validate (ace_b_basis.cpp:1312) only tries to construct an ACEBBasisSet; that constructor's consistency check (:1009-1017) compares (i,j) against (j,i), both inherited from the same block, so it always passes.

                          The same override happens outside ladder fitting through the overwrite_blocks_from_initial_bbasis branch at multispecies_basisextension.py:352-358, which replaces generated blocks with the initial ones wholesale.

                          Reproduction

                          1. Fit any potential with bonds: {ALL: {rcut: 7, ...}} and keep output_potential.yaml.
                          2. Write a new input.yaml with bonds: {ALL: {rcut: 5, ...}}, potential::initial_potential: output_potential.yaml and a fit::ladder_step.
                          3. Run pacemaker. The resulting potential has rcutij: 7, not 5.

                          A third symptom is worth noting: generalfit.py:256 writes target_potential.yaml carrying rcut: 5, while the fit and the final output potential use 7. The two files on disk disagree and neither says so.

                          Suggested fix

                          docs/pacemaker/faq.md:165 already states the rule:

                          If you change cutoff, i.e. from rcut: 7 to rcut: 6.5, then potential should be refitted from the scratch.

                          That is the right behaviour — an initial potential's coefficients are meaningless at a different cutoff, so this is not recoverable by picking one of the two values. The documentation states the rule; the code just does not enforce it. A check next to the existing block_name assertion at multispecies_basisextension.py:909, comparing init_block against final_block and raising with the block name and both values, would cover it.

                          One trap for whoever implements it: do not reuse check_radial_basis_consistency's full field list. It also compares lmaxi, nradmaxi, nradbaseij and radcoefficients, and those are supposed to grow from initial to target — that is the point of the ladder (validate_radial_shape_from_funcs at :944 recomputes them, merge_crad_matrix at :949 merges the coefficients). Only the shape-independent parameters must match: rcutij, dcutij, radbase, radparameters, r_in, delta_in, inner_cutoff_type.

                          Found while investigating the top-level cutoff parameter (see #104), but independent of it.

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              Ladder fit silently keeps the initial potential's rcut and ignores bonds::rcut from input.yaml #105

                              Description

                              @yury-lysogorskiy

                              When initial_potential is combined with a potential specification whose bonds::rcut differs from the initial potential's, the initial value silently wins. The fitted potential then has a cutoff the user never asked for, and nothing warns or raises.

                              This was found by reading the code — the test suite was not run — so please sanity-check the reproduction before acting on it.

                              Mechanism

                              extend_basis_block() in src/pyace/multispecies_basisextension.py:934 starts from the initial block:

                              extended_block=init_block.copy()

                              Every block that exists in the initial potential therefore keeps all of the initial block's radial parameters — rcutij, dcutij, radbase, radparameters, r_in, delta_in — and only receives the target's function specifications grafted on. The target's radial parameters are never applied. The only assertion in that function (line 909) compares block_name; nothing compares the radial basis.

                              ACEBBasisSet then builds map_bond_specifications exclusively from blocks with number_of_species <= 2 (lib/ace/src/ace/ace_b_basis.cpp:975) and sets

                              radial_functions->cut(ele_i, ele_j) = bondSpecification.rcut; // ace_b_basis.cpp:1023

                              so the pair block's value — inherited from the initial potential — is what the evaluator actually uses.

                              Why nothing catches it

                              • Mixed rcut within one configuration. Blocks present in the initial potential keep its value (:934); blocks that exist only in the target are created fresh at multispecies_basisextension.py:1122 from f_block.copy() and keep the target's. So an extended configuration can hold Al Al at 7 Å and Al Al Al at 5 Å. The higher-rank block's rcutij is skipped for bond specifications by the continue at ace_b_basis.cpp:975, but still counts toward cutoffmax at :890, which is unfiltered.
                              • Input::check_radial_basis_consistency — the one function that compares these fields — is called from a single place, lib/ace/src/ace/ace_yaml_input.cpp:331, during YAML file parsing, and only between a pair block and its reversed dual (Al Ni vs Ni Al). It never runs on configurations assembled in memory by the ladder.
                              • BBasisConfiguration::validate (ace_b_basis.cpp:1312) only tries to construct an ACEBBasisSet; that constructor's consistency check (:1009-1017) compares (i,j) against (j,i), both inherited from the same block, so it always passes.

                              The same override happens outside ladder fitting through the overwrite_blocks_from_initial_bbasis branch at multispecies_basisextension.py:352-358, which replaces generated blocks with the initial ones wholesale.

                              Reproduction

                              1. Fit any potential with bonds: {ALL: {rcut: 7, ...}} and keep output_potential.yaml.
                              2. Write a new input.yaml with bonds: {ALL: {rcut: 5, ...}}, potential::initial_potential: output_potential.yaml and a fit::ladder_step.
                              3. Run pacemaker. The resulting potential has rcutij: 7, not 5.

                              A third symptom is worth noting: generalfit.py:256 writes target_potential.yaml carrying rcut: 5, while the fit and the final output potential use 7. The two files on disk disagree and neither says so.

                              Suggested fix

                              docs/pacemaker/faq.md:165 already states the rule:

                              If you change cutoff, i.e. from rcut: 7 to rcut: 6.5, then potential should be refitted from the scratch.

                              That is the right behaviour — an initial potential's coefficients are meaningless at a different cutoff, so this is not recoverable by picking one of the two values. The documentation states the rule; the code just does not enforce it. A check next to the existing block_name assertion at multispecies_basisextension.py:909, comparing init_block against final_block and raising with the block name and both values, would cover it.

                              One trap for whoever implements it: do not reuse check_radial_basis_consistency's full field list. It also compares lmaxi, nradmaxi, nradbaseij and radcoefficients, and those are supposed to grow from initial to target — that is the point of the ladder (validate_radial_shape_from_funcs at :944 recomputes them, merge_crad_matrix at :949 merges the coefficients). Only the shape-independent parameters must match: rcutij, dcutij, radbase, radparameters, r_in, delta_in, inner_cutoff_type.

                              Found while investigating the top-level cutoff parameter (see #104), but independent of it.

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions