[BUG] StructuredGrid2D.get_element_gradient_for_location omits the chain-rule division by step_vector #289

Description

@Jimmy-KL

Version: 1.6.28 (also present in 1.6.27), Python 3.11.10, Windows

Summary

StructuredGrid2D.get_element_gradient_for_location returns the bilinear shape-function
derivatives with respect to local (normalised 0–1) cell coordinates and never converts them to
world units. StructuredGrid3D.get_element_gradient_for_location does the conversion:

# LoopStructural/interpolators/supports/_3d_structured_grid.py:440-442T[:, 0, :] /=self.step_vector[None, 0]
T[:, 1, :] /=self.step_vector[None, 1]
T[:, 2, :] /=self.step_vector[None, 2]

The 2D version (_2d_structured_grid.py:446-456) has no equivalent, so the returned operator is
inflated by a factor of step_vector per axis.

Impact

get_element_gradient_for_location is used in two places, so this affects both reading and solving:

  1. StructuredGrid2D.evaluate_gradient returns step_vector * ∇f instead of ∇f.
  2. FiniteDifferenceInterpolator.add_norm_constraints builds its least-squares rows from the
    same operator, so a norm constraint n effectively enforces ∇f = n / step_vector. The target
    therefore depends on nelements, which makes orientation data silently resolution-dependent.
    (add_gradient_constraints is unaffected, since scaling the operator does not change the zero
    set of an orthogonality constraint — but it is 3D-only, see the note below.)

For anisotropic cells the error is also directional, not just a magnitude scaling.

Reproducer

A field with a known unit gradient: f(x, y) = (x + y) / sqrt(2), so |∇f| = 1.

importnumpyasnpfromLoopStructural.datatypesimportBoundingBoxfromLoopStructural.interpolatorsimportInterpolatorFactorySQRT2=np.sqrt(2.0)
bbox=BoundingBox(dimensions=2, origin=np.array([0.0, 0.0]), maximum=np.array([100.0, 100.0]))
fornelementsin (5e3, 2e4, 8e4):
interp=InterpolatorFactory.create_interpolator(
interpolatortype="FDI", boundingbox=bbox, nelements=nelements
)
pts=np.random.default_rng(0).uniform(10, 90, size=(60, 2))
interp.set_value_constraints(
np.column_stack([pts, (pts[:, 0] +pts[:, 1]) /SQRT2])
)
interp.setup_interpolator()
interp.solve_system(solver="cg")
p=np.array([[50.0, 50.0]])
api=np.linalg.norm(interp.evaluate_gradient(p)[0])
h=1.0fd=np.hypot(
(interp.evaluate_value(p+ [[h, 0]]) -interp.evaluate_value(p- [[h, 0]]))[0] / (2*h),
(interp.evaluate_value(p+ [[0, h]]) -interp.evaluate_value(p- [[0, h]]))[0] / (2*h),
)
step=float(np.mean(interp.support.step_vector))
print(f"nelements={nelements:7.0f} step={step:.3f} api={api:.4f} finite-diff={fd:.4f}")

Observed — evaluate_gradient tracks step_vector to four decimal places, while a finite
difference of the same value field stays at the correct 1.0:

nelements= 5000 step=1.408 api=1.4088 finite-diff=1.0003
nelements= 20000 step=0.704 api=0.7040 finite-diff=0.9995
nelements= 80000 step=0.353 api=0.3509 finite-diff=0.9932

That is api / step = 1.0006, 1.0000, 0.9940 — the reported gradient is the true gradient
multiplied by the cell size.

Expected:evaluate_gradient returns ≈1.0 at every resolution.

Suggested fix

Mirror the 3D implementation at the end of get_element_gradient_for_location
(_2d_structured_grid.py, after line 454):

T[:, 0, :] /=self.step_vector[None, 0]
T[:, 1, :] /=self.step_vector[None, 1]
returnvertices, T, elements, inside

A regression test asserting |∇f| ≈ 1 for f = (x + y)/sqrt(2) across two or three nelements
values would pin this down; the existing tests/unit/interpolator/test_2d_discrete_support.py
looks like the natural home.

Related

FiniteDifferenceInterpolator.add_gradient_constraints cannot be used with 2D data. It calls
get_vectorsnormal_vector_to_strike_and_dip, which indexes normal_vector[:, 2]:

IndexError: index 2 is out of bounds for axis 1 with size 2

That leaves norm constraints as the only orientation option in 2D, which is why the scaling bug
above is hard to work around.

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

      [BUG] StructuredGrid2D.get_element_gradient_for_location omits the chain-rule division by step_vector #289

      Description

      @Jimmy-KL

      Version: 1.6.28 (also present in 1.6.27), Python 3.11.10, Windows

      Summary

      StructuredGrid2D.get_element_gradient_for_location returns the bilinear shape-function
      derivatives with respect to local (normalised 0–1) cell coordinates and never converts them to
      world units. StructuredGrid3D.get_element_gradient_for_location does the conversion:

      # LoopStructural/interpolators/supports/_3d_structured_grid.py:440-442T[:, 0, :] /=self.step_vector[None, 0]
      T[:, 1, :] /=self.step_vector[None, 1]
      T[:, 2, :] /=self.step_vector[None, 2]

      The 2D version (_2d_structured_grid.py:446-456) has no equivalent, so the returned operator is
      inflated by a factor of step_vector per axis.

      Impact

      get_element_gradient_for_location is used in two places, so this affects both reading and solving:

      1. StructuredGrid2D.evaluate_gradient returns step_vector * ∇f instead of ∇f.
      2. FiniteDifferenceInterpolator.add_norm_constraints builds its least-squares rows from the
        same operator, so a norm constraint n effectively enforces ∇f = n / step_vector. The target
        therefore depends on nelements, which makes orientation data silently resolution-dependent.
        (add_gradient_constraints is unaffected, since scaling the operator does not change the zero
        set of an orthogonality constraint — but it is 3D-only, see the note below.)

      For anisotropic cells the error is also directional, not just a magnitude scaling.

      Reproducer

      A field with a known unit gradient: f(x, y) = (x + y) / sqrt(2), so |∇f| = 1.

      importnumpyasnpfromLoopStructural.datatypesimportBoundingBoxfromLoopStructural.interpolatorsimportInterpolatorFactorySQRT2=np.sqrt(2.0)
      bbox=BoundingBox(dimensions=2, origin=np.array([0.0, 0.0]), maximum=np.array([100.0, 100.0]))
      fornelementsin (5e3, 2e4, 8e4):
      interp=InterpolatorFactory.create_interpolator(
      interpolatortype="FDI", boundingbox=bbox, nelements=nelements
      )
      pts=np.random.default_rng(0).uniform(10, 90, size=(60, 2))
      interp.set_value_constraints(
      np.column_stack([pts, (pts[:, 0] +pts[:, 1]) /SQRT2])
      )
      interp.setup_interpolator()
      interp.solve_system(solver="cg")
      p=np.array([[50.0, 50.0]])
      api=np.linalg.norm(interp.evaluate_gradient(p)[0])
      h=1.0fd=np.hypot(
      (interp.evaluate_value(p+ [[h, 0]]) -interp.evaluate_value(p- [[h, 0]]))[0] / (2*h),
      (interp.evaluate_value(p+ [[0, h]]) -interp.evaluate_value(p- [[0, h]]))[0] / (2*h),
      )
      step=float(np.mean(interp.support.step_vector))
      print(f"nelements={nelements:7.0f} step={step:.3f} api={api:.4f} finite-diff={fd:.4f}")

      Observed — evaluate_gradient tracks step_vector to four decimal places, while a finite
      difference of the same value field stays at the correct 1.0:

      nelements= 5000 step=1.408 api=1.4088 finite-diff=1.0003
      nelements= 20000 step=0.704 api=0.7040 finite-diff=0.9995
      nelements= 80000 step=0.353 api=0.3509 finite-diff=0.9932
      

      That is api / step = 1.0006, 1.0000, 0.9940 — the reported gradient is the true gradient
      multiplied by the cell size.

      Expected:evaluate_gradient returns ≈1.0 at every resolution.

      Suggested fix

      Mirror the 3D implementation at the end of get_element_gradient_for_location
      (_2d_structured_grid.py, after line 454):

      T[:, 0, :] /=self.step_vector[None, 0]
      T[:, 1, :] /=self.step_vector[None, 1]
      returnvertices, T, elements, inside

      A regression test asserting |∇f| ≈ 1 for f = (x + y)/sqrt(2) across two or three nelements
      values would pin this down; the existing tests/unit/interpolator/test_2d_discrete_support.py
      looks like the natural home.

      Related

      FiniteDifferenceInterpolator.add_gradient_constraints cannot be used with 2D data. It calls
      get_vectorsnormal_vector_to_strike_and_dip, which indexes normal_vector[:, 2]:

      IndexError: index 2 is out of bounds for axis 1 with size 2
      

      That leaves norm constraints as the only orientation option in 2D, which is why the scaling bug
      above is hard to work around.

      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

          [BUG] StructuredGrid2D.get_element_gradient_for_location omits the chain-rule division by step_vector #289

          Description

          @Jimmy-KL

          Version: 1.6.28 (also present in 1.6.27), Python 3.11.10, Windows

          Summary

          StructuredGrid2D.get_element_gradient_for_location returns the bilinear shape-function
          derivatives with respect to local (normalised 0–1) cell coordinates and never converts them to
          world units. StructuredGrid3D.get_element_gradient_for_location does the conversion:

          # LoopStructural/interpolators/supports/_3d_structured_grid.py:440-442T[:, 0, :] /=self.step_vector[None, 0]
          T[:, 1, :] /=self.step_vector[None, 1]
          T[:, 2, :] /=self.step_vector[None, 2]

          The 2D version (_2d_structured_grid.py:446-456) has no equivalent, so the returned operator is
          inflated by a factor of step_vector per axis.

          Impact

          get_element_gradient_for_location is used in two places, so this affects both reading and solving:

          1. StructuredGrid2D.evaluate_gradient returns step_vector * ∇f instead of ∇f.
          2. FiniteDifferenceInterpolator.add_norm_constraints builds its least-squares rows from the
            same operator, so a norm constraint n effectively enforces ∇f = n / step_vector. The target
            therefore depends on nelements, which makes orientation data silently resolution-dependent.
            (add_gradient_constraints is unaffected, since scaling the operator does not change the zero
            set of an orthogonality constraint — but it is 3D-only, see the note below.)

          For anisotropic cells the error is also directional, not just a magnitude scaling.

          Reproducer

          A field with a known unit gradient: f(x, y) = (x + y) / sqrt(2), so |∇f| = 1.

          importnumpyasnpfromLoopStructural.datatypesimportBoundingBoxfromLoopStructural.interpolatorsimportInterpolatorFactorySQRT2=np.sqrt(2.0)
          bbox=BoundingBox(dimensions=2, origin=np.array([0.0, 0.0]), maximum=np.array([100.0, 100.0]))
          fornelementsin (5e3, 2e4, 8e4):
          interp=InterpolatorFactory.create_interpolator(
          interpolatortype="FDI", boundingbox=bbox, nelements=nelements
          )
          pts=np.random.default_rng(0).uniform(10, 90, size=(60, 2))
          interp.set_value_constraints(
          np.column_stack([pts, (pts[:, 0] +pts[:, 1]) /SQRT2])
          )
          interp.setup_interpolator()
          interp.solve_system(solver="cg")
          p=np.array([[50.0, 50.0]])
          api=np.linalg.norm(interp.evaluate_gradient(p)[0])
          h=1.0fd=np.hypot(
          (interp.evaluate_value(p+ [[h, 0]]) -interp.evaluate_value(p- [[h, 0]]))[0] / (2*h),
          (interp.evaluate_value(p+ [[0, h]]) -interp.evaluate_value(p- [[0, h]]))[0] / (2*h),
          )
          step=float(np.mean(interp.support.step_vector))
          print(f"nelements={nelements:7.0f} step={step:.3f} api={api:.4f} finite-diff={fd:.4f}")

          Observed — evaluate_gradient tracks step_vector to four decimal places, while a finite
          difference of the same value field stays at the correct 1.0:

          nelements= 5000 step=1.408 api=1.4088 finite-diff=1.0003
          nelements= 20000 step=0.704 api=0.7040 finite-diff=0.9995
          nelements= 80000 step=0.353 api=0.3509 finite-diff=0.9932
          

          That is api / step = 1.0006, 1.0000, 0.9940 — the reported gradient is the true gradient
          multiplied by the cell size.

          Expected:evaluate_gradient returns ≈1.0 at every resolution.

          Suggested fix

          Mirror the 3D implementation at the end of get_element_gradient_for_location
          (_2d_structured_grid.py, after line 454):

          T[:, 0, :] /=self.step_vector[None, 0]
          T[:, 1, :] /=self.step_vector[None, 1]
          returnvertices, T, elements, inside

          A regression test asserting |∇f| ≈ 1 for f = (x + y)/sqrt(2) across two or three nelements
          values would pin this down; the existing tests/unit/interpolator/test_2d_discrete_support.py
          looks like the natural home.

          Related

          FiniteDifferenceInterpolator.add_gradient_constraints cannot be used with 2D data. It calls
          get_vectorsnormal_vector_to_strike_and_dip, which indexes normal_vector[:, 2]:

          IndexError: index 2 is out of bounds for axis 1 with size 2
          

          That leaves norm constraints as the only orientation option in 2D, which is why the scaling bug
          above is hard to work around.

          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

              [BUG] StructuredGrid2D.get_element_gradient_for_location omits the chain-rule division by step_vector #289

              Description

              @Jimmy-KL

              Version: 1.6.28 (also present in 1.6.27), Python 3.11.10, Windows

              Summary

              StructuredGrid2D.get_element_gradient_for_location returns the bilinear shape-function
              derivatives with respect to local (normalised 0–1) cell coordinates and never converts them to
              world units. StructuredGrid3D.get_element_gradient_for_location does the conversion:

              # LoopStructural/interpolators/supports/_3d_structured_grid.py:440-442T[:, 0, :] /=self.step_vector[None, 0]
              T[:, 1, :] /=self.step_vector[None, 1]
              T[:, 2, :] /=self.step_vector[None, 2]

              The 2D version (_2d_structured_grid.py:446-456) has no equivalent, so the returned operator is
              inflated by a factor of step_vector per axis.

              Impact

              get_element_gradient_for_location is used in two places, so this affects both reading and solving:

              1. StructuredGrid2D.evaluate_gradient returns step_vector * ∇f instead of ∇f.
              2. FiniteDifferenceInterpolator.add_norm_constraints builds its least-squares rows from the
                same operator, so a norm constraint n effectively enforces ∇f = n / step_vector. The target
                therefore depends on nelements, which makes orientation data silently resolution-dependent.
                (add_gradient_constraints is unaffected, since scaling the operator does not change the zero
                set of an orthogonality constraint — but it is 3D-only, see the note below.)

              For anisotropic cells the error is also directional, not just a magnitude scaling.

              Reproducer

              A field with a known unit gradient: f(x, y) = (x + y) / sqrt(2), so |∇f| = 1.

              importnumpyasnpfromLoopStructural.datatypesimportBoundingBoxfromLoopStructural.interpolatorsimportInterpolatorFactorySQRT2=np.sqrt(2.0)
              bbox=BoundingBox(dimensions=2, origin=np.array([0.0, 0.0]), maximum=np.array([100.0, 100.0]))
              fornelementsin (5e3, 2e4, 8e4):
              interp=InterpolatorFactory.create_interpolator(
              interpolatortype="FDI", boundingbox=bbox, nelements=nelements
              )
              pts=np.random.default_rng(0).uniform(10, 90, size=(60, 2))
              interp.set_value_constraints(
              np.column_stack([pts, (pts[:, 0] +pts[:, 1]) /SQRT2])
              )
              interp.setup_interpolator()
              interp.solve_system(solver="cg")
              p=np.array([[50.0, 50.0]])
              api=np.linalg.norm(interp.evaluate_gradient(p)[0])
              h=1.0fd=np.hypot(
              (interp.evaluate_value(p+ [[h, 0]]) -interp.evaluate_value(p- [[h, 0]]))[0] / (2*h),
              (interp.evaluate_value(p+ [[0, h]]) -interp.evaluate_value(p- [[0, h]]))[0] / (2*h),
              )
              step=float(np.mean(interp.support.step_vector))
              print(f"nelements={nelements:7.0f} step={step:.3f} api={api:.4f} finite-diff={fd:.4f}")

              Observed — evaluate_gradient tracks step_vector to four decimal places, while a finite
              difference of the same value field stays at the correct 1.0:

              nelements= 5000 step=1.408 api=1.4088 finite-diff=1.0003
              nelements= 20000 step=0.704 api=0.7040 finite-diff=0.9995
              nelements= 80000 step=0.353 api=0.3509 finite-diff=0.9932
              

              That is api / step = 1.0006, 1.0000, 0.9940 — the reported gradient is the true gradient
              multiplied by the cell size.

              Expected:evaluate_gradient returns ≈1.0 at every resolution.

              Suggested fix

              Mirror the 3D implementation at the end of get_element_gradient_for_location
              (_2d_structured_grid.py, after line 454):

              T[:, 0, :] /=self.step_vector[None, 0]
              T[:, 1, :] /=self.step_vector[None, 1]
              returnvertices, T, elements, inside

              A regression test asserting |∇f| ≈ 1 for f = (x + y)/sqrt(2) across two or three nelements
              values would pin this down; the existing tests/unit/interpolator/test_2d_discrete_support.py
              looks like the natural home.

              Related

              FiniteDifferenceInterpolator.add_gradient_constraints cannot be used with 2D data. It calls
              get_vectorsnormal_vector_to_strike_and_dip, which indexes normal_vector[:, 2]:

              IndexError: index 2 is out of bounds for axis 1 with size 2
              

              That leaves norm constraints as the only orientation option in 2D, which is why the scaling bug
              above is hard to work around.

              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

                  [BUG] StructuredGrid2D.get_element_gradient_for_location omits the chain-rule division by step_vector #289

                  Description

                  @Jimmy-KL

                  Version: 1.6.28 (also present in 1.6.27), Python 3.11.10, Windows

                  Summary

                  StructuredGrid2D.get_element_gradient_for_location returns the bilinear shape-function
                  derivatives with respect to local (normalised 0–1) cell coordinates and never converts them to
                  world units. StructuredGrid3D.get_element_gradient_for_location does the conversion:

                  # LoopStructural/interpolators/supports/_3d_structured_grid.py:440-442T[:, 0, :] /=self.step_vector[None, 0]
                  T[:, 1, :] /=self.step_vector[None, 1]
                  T[:, 2, :] /=self.step_vector[None, 2]

                  The 2D version (_2d_structured_grid.py:446-456) has no equivalent, so the returned operator is
                  inflated by a factor of step_vector per axis.

                  Impact

                  get_element_gradient_for_location is used in two places, so this affects both reading and solving:

                  1. StructuredGrid2D.evaluate_gradient returns step_vector * ∇f instead of ∇f.
                  2. FiniteDifferenceInterpolator.add_norm_constraints builds its least-squares rows from the
                    same operator, so a norm constraint n effectively enforces ∇f = n / step_vector. The target
                    therefore depends on nelements, which makes orientation data silently resolution-dependent.
                    (add_gradient_constraints is unaffected, since scaling the operator does not change the zero
                    set of an orthogonality constraint — but it is 3D-only, see the note below.)

                  For anisotropic cells the error is also directional, not just a magnitude scaling.

                  Reproducer

                  A field with a known unit gradient: f(x, y) = (x + y) / sqrt(2), so |∇f| = 1.

                  importnumpyasnpfromLoopStructural.datatypesimportBoundingBoxfromLoopStructural.interpolatorsimportInterpolatorFactorySQRT2=np.sqrt(2.0)
                  bbox=BoundingBox(dimensions=2, origin=np.array([0.0, 0.0]), maximum=np.array([100.0, 100.0]))
                  fornelementsin (5e3, 2e4, 8e4):
                  interp=InterpolatorFactory.create_interpolator(
                  interpolatortype="FDI", boundingbox=bbox, nelements=nelements
                  )
                  pts=np.random.default_rng(0).uniform(10, 90, size=(60, 2))
                  interp.set_value_constraints(
                  np.column_stack([pts, (pts[:, 0] +pts[:, 1]) /SQRT2])
                  )
                  interp.setup_interpolator()
                  interp.solve_system(solver="cg")
                  p=np.array([[50.0, 50.0]])
                  api=np.linalg.norm(interp.evaluate_gradient(p)[0])
                  h=1.0fd=np.hypot(
                  (interp.evaluate_value(p+ [[h, 0]]) -interp.evaluate_value(p- [[h, 0]]))[0] / (2*h),
                  (interp.evaluate_value(p+ [[0, h]]) -interp.evaluate_value(p- [[0, h]]))[0] / (2*h),
                  )
                  step=float(np.mean(interp.support.step_vector))
                  print(f"nelements={nelements:7.0f} step={step:.3f} api={api:.4f} finite-diff={fd:.4f}")

                  Observed — evaluate_gradient tracks step_vector to four decimal places, while a finite
                  difference of the same value field stays at the correct 1.0:

                  nelements= 5000 step=1.408 api=1.4088 finite-diff=1.0003
                  nelements= 20000 step=0.704 api=0.7040 finite-diff=0.9995
                  nelements= 80000 step=0.353 api=0.3509 finite-diff=0.9932
                  

                  That is api / step = 1.0006, 1.0000, 0.9940 — the reported gradient is the true gradient
                  multiplied by the cell size.

                  Expected:evaluate_gradient returns ≈1.0 at every resolution.

                  Suggested fix

                  Mirror the 3D implementation at the end of get_element_gradient_for_location
                  (_2d_structured_grid.py, after line 454):

                  T[:, 0, :] /=self.step_vector[None, 0]
                  T[:, 1, :] /=self.step_vector[None, 1]
                  returnvertices, T, elements, inside

                  A regression test asserting |∇f| ≈ 1 for f = (x + y)/sqrt(2) across two or three nelements
                  values would pin this down; the existing tests/unit/interpolator/test_2d_discrete_support.py
                  looks like the natural home.

                  Related

                  FiniteDifferenceInterpolator.add_gradient_constraints cannot be used with 2D data. It calls
                  get_vectorsnormal_vector_to_strike_and_dip, which indexes normal_vector[:, 2]:

                  IndexError: index 2 is out of bounds for axis 1 with size 2
                  

                  That leaves norm constraints as the only orientation option in 2D, which is why the scaling bug
                  above is hard to work around.

                  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

                      [BUG] StructuredGrid2D.get_element_gradient_for_location omits the chain-rule division by step_vector #289

                      Description

                      @Jimmy-KL

                      Version: 1.6.28 (also present in 1.6.27), Python 3.11.10, Windows

                      Summary

                      StructuredGrid2D.get_element_gradient_for_location returns the bilinear shape-function
                      derivatives with respect to local (normalised 0–1) cell coordinates and never converts them to
                      world units. StructuredGrid3D.get_element_gradient_for_location does the conversion:

                      # LoopStructural/interpolators/supports/_3d_structured_grid.py:440-442T[:, 0, :] /=self.step_vector[None, 0]
                      T[:, 1, :] /=self.step_vector[None, 1]
                      T[:, 2, :] /=self.step_vector[None, 2]

                      The 2D version (_2d_structured_grid.py:446-456) has no equivalent, so the returned operator is
                      inflated by a factor of step_vector per axis.

                      Impact

                      get_element_gradient_for_location is used in two places, so this affects both reading and solving:

                      1. StructuredGrid2D.evaluate_gradient returns step_vector * ∇f instead of ∇f.
                      2. FiniteDifferenceInterpolator.add_norm_constraints builds its least-squares rows from the
                        same operator, so a norm constraint n effectively enforces ∇f = n / step_vector. The target
                        therefore depends on nelements, which makes orientation data silently resolution-dependent.
                        (add_gradient_constraints is unaffected, since scaling the operator does not change the zero
                        set of an orthogonality constraint — but it is 3D-only, see the note below.)

                      For anisotropic cells the error is also directional, not just a magnitude scaling.

                      Reproducer

                      A field with a known unit gradient: f(x, y) = (x + y) / sqrt(2), so |∇f| = 1.

                      importnumpyasnpfromLoopStructural.datatypesimportBoundingBoxfromLoopStructural.interpolatorsimportInterpolatorFactorySQRT2=np.sqrt(2.0)
                      bbox=BoundingBox(dimensions=2, origin=np.array([0.0, 0.0]), maximum=np.array([100.0, 100.0]))
                      fornelementsin (5e3, 2e4, 8e4):
                      interp=InterpolatorFactory.create_interpolator(
                      interpolatortype="FDI", boundingbox=bbox, nelements=nelements
                      )
                      pts=np.random.default_rng(0).uniform(10, 90, size=(60, 2))
                      interp.set_value_constraints(
                      np.column_stack([pts, (pts[:, 0] +pts[:, 1]) /SQRT2])
                      )
                      interp.setup_interpolator()
                      interp.solve_system(solver="cg")
                      p=np.array([[50.0, 50.0]])
                      api=np.linalg.norm(interp.evaluate_gradient(p)[0])
                      h=1.0fd=np.hypot(
                      (interp.evaluate_value(p+ [[h, 0]]) -interp.evaluate_value(p- [[h, 0]]))[0] / (2*h),
                      (interp.evaluate_value(p+ [[0, h]]) -interp.evaluate_value(p- [[0, h]]))[0] / (2*h),
                      )
                      step=float(np.mean(interp.support.step_vector))
                      print(f"nelements={nelements:7.0f} step={step:.3f} api={api:.4f} finite-diff={fd:.4f}")

                      Observed — evaluate_gradient tracks step_vector to four decimal places, while a finite
                      difference of the same value field stays at the correct 1.0:

                      nelements= 5000 step=1.408 api=1.4088 finite-diff=1.0003
                      nelements= 20000 step=0.704 api=0.7040 finite-diff=0.9995
                      nelements= 80000 step=0.353 api=0.3509 finite-diff=0.9932
                      

                      That is api / step = 1.0006, 1.0000, 0.9940 — the reported gradient is the true gradient
                      multiplied by the cell size.

                      Expected:evaluate_gradient returns ≈1.0 at every resolution.

                      Suggested fix

                      Mirror the 3D implementation at the end of get_element_gradient_for_location
                      (_2d_structured_grid.py, after line 454):

                      T[:, 0, :] /=self.step_vector[None, 0]
                      T[:, 1, :] /=self.step_vector[None, 1]
                      returnvertices, T, elements, inside

                      A regression test asserting |∇f| ≈ 1 for f = (x + y)/sqrt(2) across two or three nelements
                      values would pin this down; the existing tests/unit/interpolator/test_2d_discrete_support.py
                      looks like the natural home.

                      Related

                      FiniteDifferenceInterpolator.add_gradient_constraints cannot be used with 2D data. It calls
                      get_vectorsnormal_vector_to_strike_and_dip, which indexes normal_vector[:, 2]:

                      IndexError: index 2 is out of bounds for axis 1 with size 2
                      

                      That leaves norm constraints as the only orientation option in 2D, which is why the scaling bug
                      above is hard to work around.

                      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

                          [BUG] StructuredGrid2D.get_element_gradient_for_location omits the chain-rule division by step_vector #289

                          Description

                          @Jimmy-KL

                          Version: 1.6.28 (also present in 1.6.27), Python 3.11.10, Windows

                          Summary

                          StructuredGrid2D.get_element_gradient_for_location returns the bilinear shape-function
                          derivatives with respect to local (normalised 0–1) cell coordinates and never converts them to
                          world units. StructuredGrid3D.get_element_gradient_for_location does the conversion:

                          # LoopStructural/interpolators/supports/_3d_structured_grid.py:440-442T[:, 0, :] /=self.step_vector[None, 0]
                          T[:, 1, :] /=self.step_vector[None, 1]
                          T[:, 2, :] /=self.step_vector[None, 2]

                          The 2D version (_2d_structured_grid.py:446-456) has no equivalent, so the returned operator is
                          inflated by a factor of step_vector per axis.

                          Impact

                          get_element_gradient_for_location is used in two places, so this affects both reading and solving:

                          1. StructuredGrid2D.evaluate_gradient returns step_vector * ∇f instead of ∇f.
                          2. FiniteDifferenceInterpolator.add_norm_constraints builds its least-squares rows from the
                            same operator, so a norm constraint n effectively enforces ∇f = n / step_vector. The target
                            therefore depends on nelements, which makes orientation data silently resolution-dependent.
                            (add_gradient_constraints is unaffected, since scaling the operator does not change the zero
                            set of an orthogonality constraint — but it is 3D-only, see the note below.)

                          For anisotropic cells the error is also directional, not just a magnitude scaling.

                          Reproducer

                          A field with a known unit gradient: f(x, y) = (x + y) / sqrt(2), so |∇f| = 1.

                          importnumpyasnpfromLoopStructural.datatypesimportBoundingBoxfromLoopStructural.interpolatorsimportInterpolatorFactorySQRT2=np.sqrt(2.0)
                          bbox=BoundingBox(dimensions=2, origin=np.array([0.0, 0.0]), maximum=np.array([100.0, 100.0]))
                          fornelementsin (5e3, 2e4, 8e4):
                          interp=InterpolatorFactory.create_interpolator(
                          interpolatortype="FDI", boundingbox=bbox, nelements=nelements
                          )
                          pts=np.random.default_rng(0).uniform(10, 90, size=(60, 2))
                          interp.set_value_constraints(
                          np.column_stack([pts, (pts[:, 0] +pts[:, 1]) /SQRT2])
                          )
                          interp.setup_interpolator()
                          interp.solve_system(solver="cg")
                          p=np.array([[50.0, 50.0]])
                          api=np.linalg.norm(interp.evaluate_gradient(p)[0])
                          h=1.0fd=np.hypot(
                          (interp.evaluate_value(p+ [[h, 0]]) -interp.evaluate_value(p- [[h, 0]]))[0] / (2*h),
                          (interp.evaluate_value(p+ [[0, h]]) -interp.evaluate_value(p- [[0, h]]))[0] / (2*h),
                          )
                          step=float(np.mean(interp.support.step_vector))
                          print(f"nelements={nelements:7.0f} step={step:.3f} api={api:.4f} finite-diff={fd:.4f}")

                          Observed — evaluate_gradient tracks step_vector to four decimal places, while a finite
                          difference of the same value field stays at the correct 1.0:

                          nelements= 5000 step=1.408 api=1.4088 finite-diff=1.0003
                          nelements= 20000 step=0.704 api=0.7040 finite-diff=0.9995
                          nelements= 80000 step=0.353 api=0.3509 finite-diff=0.9932
                          

                          That is api / step = 1.0006, 1.0000, 0.9940 — the reported gradient is the true gradient
                          multiplied by the cell size.

                          Expected:evaluate_gradient returns ≈1.0 at every resolution.

                          Suggested fix

                          Mirror the 3D implementation at the end of get_element_gradient_for_location
                          (_2d_structured_grid.py, after line 454):

                          T[:, 0, :] /=self.step_vector[None, 0]
                          T[:, 1, :] /=self.step_vector[None, 1]
                          returnvertices, T, elements, inside

                          A regression test asserting |∇f| ≈ 1 for f = (x + y)/sqrt(2) across two or three nelements
                          values would pin this down; the existing tests/unit/interpolator/test_2d_discrete_support.py
                          looks like the natural home.

                          Related

                          FiniteDifferenceInterpolator.add_gradient_constraints cannot be used with 2D data. It calls
                          get_vectorsnormal_vector_to_strike_and_dip, which indexes normal_vector[:, 2]:

                          IndexError: index 2 is out of bounds for axis 1 with size 2
                          

                          That leaves norm constraints as the only orientation option in 2D, which is why the scaling bug
                          above is hard to work around.

                          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

                              [BUG] StructuredGrid2D.get_element_gradient_for_location omits the chain-rule division by step_vector #289

                              Description

                              @Jimmy-KL

                              Version: 1.6.28 (also present in 1.6.27), Python 3.11.10, Windows

                              Summary

                              StructuredGrid2D.get_element_gradient_for_location returns the bilinear shape-function
                              derivatives with respect to local (normalised 0–1) cell coordinates and never converts them to
                              world units. StructuredGrid3D.get_element_gradient_for_location does the conversion:

                              # LoopStructural/interpolators/supports/_3d_structured_grid.py:440-442T[:, 0, :] /=self.step_vector[None, 0]
                              T[:, 1, :] /=self.step_vector[None, 1]
                              T[:, 2, :] /=self.step_vector[None, 2]

                              The 2D version (_2d_structured_grid.py:446-456) has no equivalent, so the returned operator is
                              inflated by a factor of step_vector per axis.

                              Impact

                              get_element_gradient_for_location is used in two places, so this affects both reading and solving:

                              1. StructuredGrid2D.evaluate_gradient returns step_vector * ∇f instead of ∇f.
                              2. FiniteDifferenceInterpolator.add_norm_constraints builds its least-squares rows from the
                                same operator, so a norm constraint n effectively enforces ∇f = n / step_vector. The target
                                therefore depends on nelements, which makes orientation data silently resolution-dependent.
                                (add_gradient_constraints is unaffected, since scaling the operator does not change the zero
                                set of an orthogonality constraint — but it is 3D-only, see the note below.)

                              For anisotropic cells the error is also directional, not just a magnitude scaling.

                              Reproducer

                              A field with a known unit gradient: f(x, y) = (x + y) / sqrt(2), so |∇f| = 1.

                              importnumpyasnpfromLoopStructural.datatypesimportBoundingBoxfromLoopStructural.interpolatorsimportInterpolatorFactorySQRT2=np.sqrt(2.0)
                              bbox=BoundingBox(dimensions=2, origin=np.array([0.0, 0.0]), maximum=np.array([100.0, 100.0]))
                              fornelementsin (5e3, 2e4, 8e4):
                              interp=InterpolatorFactory.create_interpolator(
                              interpolatortype="FDI", boundingbox=bbox, nelements=nelements
                              )
                              pts=np.random.default_rng(0).uniform(10, 90, size=(60, 2))
                              interp.set_value_constraints(
                              np.column_stack([pts, (pts[:, 0] +pts[:, 1]) /SQRT2])
                              )
                              interp.setup_interpolator()
                              interp.solve_system(solver="cg")
                              p=np.array([[50.0, 50.0]])
                              api=np.linalg.norm(interp.evaluate_gradient(p)[0])
                              h=1.0fd=np.hypot(
                              (interp.evaluate_value(p+ [[h, 0]]) -interp.evaluate_value(p- [[h, 0]]))[0] / (2*h),
                              (interp.evaluate_value(p+ [[0, h]]) -interp.evaluate_value(p- [[0, h]]))[0] / (2*h),
                              )
                              step=float(np.mean(interp.support.step_vector))
                              print(f"nelements={nelements:7.0f} step={step:.3f} api={api:.4f} finite-diff={fd:.4f}")

                              Observed — evaluate_gradient tracks step_vector to four decimal places, while a finite
                              difference of the same value field stays at the correct 1.0:

                              nelements= 5000 step=1.408 api=1.4088 finite-diff=1.0003
                              nelements= 20000 step=0.704 api=0.7040 finite-diff=0.9995
                              nelements= 80000 step=0.353 api=0.3509 finite-diff=0.9932
                              

                              That is api / step = 1.0006, 1.0000, 0.9940 — the reported gradient is the true gradient
                              multiplied by the cell size.

                              Expected:evaluate_gradient returns ≈1.0 at every resolution.

                              Suggested fix

                              Mirror the 3D implementation at the end of get_element_gradient_for_location
                              (_2d_structured_grid.py, after line 454):

                              T[:, 0, :] /=self.step_vector[None, 0]
                              T[:, 1, :] /=self.step_vector[None, 1]
                              returnvertices, T, elements, inside

                              A regression test asserting |∇f| ≈ 1 for f = (x + y)/sqrt(2) across two or three nelements
                              values would pin this down; the existing tests/unit/interpolator/test_2d_discrete_support.py
                              looks like the natural home.

                              Related

                              FiniteDifferenceInterpolator.add_gradient_constraints cannot be used with 2D data. It calls
                              get_vectorsnormal_vector_to_strike_and_dip, which indexes normal_vector[:, 2]:

                              IndexError: index 2 is out of bounds for axis 1 with size 2
                              

                              That leaves norm constraints as the only orientation option in 2D, which is why the scaling bug
                              above is hard to work around.

                              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