{
  "cells": [
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "import pyvista\npyvista.OFF_SCREEN = True\npyvista.set_jupyter_backend('static')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "\n# Irons constant-strain patch test\n\nThe canonical FE consistency test: a 2\u00d72\u00d72 hex patch with an\ninterior node displaced from its regular position, subjected to\nboundary displacements $u = \\varepsilon_0\\, x$ consistent\nwith a uniform axial strain field, must recover\n$\\varepsilon = \\varepsilon_0$ everywhere to **machine\nprecision**.\n\nReference: Irons, B. M. and Razzaque, A. (1972) \"Experience with\nthe patch test for convergence of finite elements,\" *The\nMathematical Foundations of the Finite Element Method*, pp.\n557-587.\n\nA discretisation that fails this test cannot pass the inf-sup /\nconsistency conditions and therefore cannot converge at the\noptimal rate.  A pass at machine precision says the element\nkernel is wiring $B$, $D$, and the isoparametric\nmapping correctly under non-trivial geometry.\n\n## Vendor cross-references\n\n======================================  =====================  ============================================\nSource                                   Expected               Problem ID / location\n======================================  =====================  ============================================\nIrons-Razzaque 1972 (original)           identity (exact)       Math Foundations of FEM, pp. 557-587\nTaylor-Beresford-Wilson 1976             identity (exact)       IJNME 10 (1976), pp. 1211-1219\nHughes, The Finite Element Method        identity (exact)       Dover (2000), \u00a74.4.3\nNAFEMS Background to Benchmarks          identity (exact)       BtB-4.2 (patch-test completeness)\nAbaqus Verification Manual               identity (exact)       AVM 1.4.4 (C3D8 patch test)\n======================================  =====================  ============================================\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "from __future__ import annotations\n\nimport numpy as np\nimport pyvista as pv\n\nfrom femorph_solver.validation.problems import IronsPatchTest"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Problem statement\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "problem = IronsPatchTest()\npv_ = problem.published_values[0]\nprint(f\"{problem.name}: {problem.description}\")\nprint(f\"\\n  source : {pv_.source}\")\nprint(f\"  formula: {pv_.formula}\")\nprint(f\"  target : {pv_.value} (tolerance {pv_.tolerance:.0e})\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Run the check \u2014 single-shot ``validate()``\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "results = problem.validate()\nr = results[0]\nprint(f\"\\n  computed max_strain_error: {r.computed:.3e}\")\nprint(f\"  passes tolerance          : {r.passed}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Figure \u2014 the distorted patch\nThe validation's ``build_model`` offsets the single interior\nnode by 10 % of the edge length so the isoparametric mapping\nis non-trivial; rendering the resulting mesh is a nice sanity\ncheck that the geometry actually has the promised distortion.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "m = problem.build_model()\nplotter = pv.Plotter(off_screen=True)\nplotter.add_mesh(m.grid, show_edges=True, opacity=0.7, color=\"#b8cde3\")\nplotter.add_points(\n    np.asarray(m.grid.points),\n    render_points_as_spheres=True,\n    point_size=12,\n    color=\"red\",\n)\nplotter.view_isometric()\nplotter.camera.zoom(1.1)\nplotter.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Acceptance\nThe 1e-9 tolerance is conservative \u2014 we measure ~1e-19 on\nthis build.  A regression above 1e-12 would indicate a\nprecision-dropping change in the assembly / solve pipeline.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "assert r.passed, f\"patch test failed: {r.computed:.3e} > {pv_.tolerance:.1e}\""
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.12.3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}