{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "206a681f-a3b4-430d-8e88-d4b11a9f32d0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 36652 characters]",
      "text/plain": [
       "<Figure size 600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== Radial equal-area infill ===\n",
      "Slice @ z              :   0.00 mm\n",
      "Area fraction (target) :  0.300\n",
      "Filled area            :  183.617 mm²\n",
      "Centroid (x̄,ȳ)          : ( -0.00 ,  -0.00) mm\n",
      "Ix , Iy                : 7617.569 , 7617.569 mm⁴\n",
      "|Ix − Iy| / Ix          : 1.19e-16\n",
      "Ixy                    :   -0.000 mm⁴\n",
      "\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 36060 characters]",
      "text/plain": [
       "<Figure size 600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== Radial equal-area infill ===\n",
      "Slice @ z              :   2.50 mm\n",
      "Area fraction (target) :  0.300\n",
      "Filled area            :  183.637 mm²\n",
      "Centroid (x̄,ȳ)          : ( -0.00 ,  -0.00) mm\n",
      "Ix , Iy                : 7825.814 , 7825.814 mm⁴\n",
      "|Ix − Iy| / Ix          : 0.00e+00\n",
      "Ixy                    :   -0.000 mm⁴\n",
      "\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 37076 characters]",
      "text/plain": [
       "<Figure size 600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== Radial equal-area infill ===\n",
      "Slice @ z              :   5.00 mm\n",
      "Area fraction (target) :  0.300\n",
      "Filled area            :  183.637 mm²\n",
      "Centroid (x̄,ȳ)          : ( -0.00 ,  -0.00) mm\n",
      "Ix , Iy                : 8009.353 , 8009.353 mm⁴\n",
      "|Ix − Iy| / Ix          : 0.00e+00\n",
      "Ixy                    :    0.000 mm⁴\n",
      "\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 35868 characters]",
      "text/plain": [
       "<Figure size 600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== Radial equal-area infill ===\n",
      "Slice @ z              :   7.50 mm\n",
      "Area fraction (target) :  0.300\n",
      "Filled area            :  183.587 mm²\n",
      "Centroid (x̄,ȳ)          : ( -0.00 ,  -0.00) mm\n",
      "Ix , Iy                : 7516.301 , 7516.301 mm⁴\n",
      "|Ix − Iy| / Ix          : 0.00e+00\n",
      "Ixy                    :   -0.000 mm⁴\n",
      "\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 37084 characters]",
      "text/plain": [
       "<Figure size 600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=== Radial equal-area infill ===\n",
      "Slice @ z              :  10.00 mm\n",
      "Area fraction (target) :  0.300\n",
      "Filled area            :  183.647 mm²\n",
      "Centroid (x̄,ȳ)          : ( -0.00 ,  -0.00) mm\n",
      "Ix , Iy                : 8084.520 , 8084.520 mm⁴\n",
      "|Ix − Iy| / Ix          : 1.12e-16\n",
      "Ixy                    :    0.000 mm⁴\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# ----------------------------------------------------------------------\n",
    "#  NEW  –  isotropic, constant-area implicit surface\n",
    "# ----------------------------------------------------------------------\n",
    "\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "\n",
    "def compute_area_moments(XX, YY, mask):\n",
    "    dx = XX[0, 1] - XX[0, 0]\n",
    "    dy = YY[1, 0] - YY[0, 0]\n",
    "    dA = dx * dy\n",
    "\n",
    "    A = np.count_nonzero(mask) * dA\n",
    "    if A == 0:\n",
    "        return A, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0\n",
    "\n",
    "    x_vals = XX[mask]\n",
    "    y_vals = YY[mask]\n",
    "    x_c = np.sum(x_vals * dA) / A\n",
    "    y_c = np.sum(y_vals * dA) / A\n",
    "    x_shift = x_vals - x_c\n",
    "    y_shift = y_vals - y_c\n",
    "\n",
    "    I_x = np.sum((y_shift**2) * dA)\n",
    "    I_y = np.sum((x_shift**2) * dA)\n",
    "    I_xy = np.sum((x_shift * y_shift) * dA)\n",
    "    J = I_x + I_y\n",
    "\n",
    "    return A, x_c, y_c, I_x, I_y, I_xy, J\n",
    "\n",
    "\n",
    "\n",
    "def radial_equal_area_infill_rectangular(\n",
    "    *,\n",
    "    width,\n",
    "    height,\n",
    "    area_fraction,      # 0 … 1  of INNER rectangle that must be solid\n",
    "    z_height=0.0,\n",
    "    base_period=6.0,    # radial wavelength  [mm]\n",
    "    shell_layers=0,\n",
    "    layer_width=0.4,\n",
    "    grid_N=400,\n",
    "    plot=True,\n",
    "    tol=1e-4,\n",
    "    max_iter=40\n",
    "):\n",
    "    \"\"\"\n",
    "    Generates a mask for an r-dependent implicit surface\n",
    "        f(r,z) = sin( 2π r / base_period + ϕ(z) )\n",
    "    At each z the threshold is chosen so that:\n",
    "        • filled area  = area_fraction · inner_area   (constant)\n",
    "        • Ix ≈ Iy      (radial symmetry → automatic)\n",
    "    Optional perimeter shells match the style of your gyroid routine.\n",
    "    \"\"\"\n",
    "    # 1) Build XY grid -------------------------------------------------\n",
    "    xs = np.linspace(-width  / 2,  width  / 2,  grid_N)\n",
    "    ys = np.linspace(-height / 2,  height / 2,  grid_N)\n",
    "    XX, YY = np.meshgrid(xs, ys)\n",
    "\n",
    "    # 2) Implicit field f(r,z) ----------------------------------------\n",
    "    r   = np.hypot(XX, YY)\n",
    "    k   = 2.0 * np.pi / base_period\n",
    "    phi = 2.0 * np.pi * z_height / base_period      # simple upward phase shift\n",
    "    F   = np.sin(k * r + phi)                       # values in [-1 … 1]\n",
    "\n",
    "    # 3) Perimeter shells (optional) ----------------------------------\n",
    "    shell_thk   = shell_layers * layer_width\n",
    "    inner_w     = width  / 2 - shell_thk\n",
    "    inner_h     = height / 2 - shell_thk\n",
    "    inner_mask  = ((np.abs(XX) < inner_w) &\n",
    "                   (np.abs(YY) < inner_h))\n",
    "    shell_mask  = ~inner_mask if shell_layers else np.zeros_like(F, bool)\n",
    "\n",
    "    # 4) Desired filled area in INNER region --------------------------\n",
    "    A_inner      = inner_w * 2 * inner_h * 2\n",
    "    target_area  = area_fraction * A_inner\n",
    "\n",
    "    # 5) Bisection to find threshold t so A == target_area ------------\n",
    "    lo, hi = F.min(), F.max()\n",
    "    t = 0.0\n",
    "    for _ in range(max_iter):\n",
    "        t = 0.5 * (lo + hi)\n",
    "        mask_core = (F >= t) & inner_mask\n",
    "        A, *_ = compute_area_moments(XX, YY, mask_core)\n",
    "        if abs(A - target_area) < tol * A_inner:\n",
    "            break\n",
    "        if A > target_area:\n",
    "            lo = t\n",
    "        else:\n",
    "            hi = t\n",
    "\n",
    "    core_mask  = (F >= t) & inner_mask\n",
    "    final_mask = core_mask | shell_mask\n",
    "\n",
    "    # 6) Section properties ------------------------------------------\n",
    "    A_tot, x_c, y_c, Ix, Iy, Ixy, J = compute_area_moments(XX, YY, final_mask)\n",
    "\n",
    "    # 7) Optional plot ------------------------------------------------\n",
    "    if plot:\n",
    "        plt.figure(figsize=(6, 6))\n",
    "        plt.imshow(np.where(final_mask, 1.0, np.nan),\n",
    "                   origin='lower',\n",
    "                   extent=[-width / 2, width / 2, -height / 2, height / 2],\n",
    "                   interpolation='none')\n",
    "        plt.title(f\"Radial equal-area infill  (z = {z_height:.2f} mm)\\n\"\n",
    "                  f\"area = {area_fraction*100:.1f}% of inner;   Ix ≈ Iy\")\n",
    "        plt.xlabel(\"x  [mm]\")\n",
    "        plt.ylabel(\"y  [mm]\")\n",
    "        plt.gca().set_aspect('equal', 'box')\n",
    "        plt.grid(True)\n",
    "        plt.show()\n",
    "\n",
    "    # 8) Console report ----------------------------------------------\n",
    "    print(\"=== Radial equal-area infill ===\")\n",
    "    print(f\"Slice @ z              : {z_height:6.2f} mm\")\n",
    "    print(f\"Area fraction (target) : {area_fraction:6.3f}\")\n",
    "    print(f\"Filled area            : {A_tot:8.3f} mm²\")\n",
    "    print(f\"Centroid (x̄,ȳ)          : ({x_c:6.2f} , {y_c:6.2f}) mm\")\n",
    "    print(f\"Ix , Iy                : {Ix:8.3f} , {Iy:8.3f} mm⁴\")\n",
    "    print(f\"|Ix − Iy| / Ix          : {abs(Ix-Iy)/Ix:8.2e}\")\n",
    "    print(f\"Ixy                    : {Ixy:8.3f} mm⁴\")\n",
    "    print(\"\")\n",
    "    return final_mask\n",
    "\n",
    "\n",
    "# ----------------------------------------------------------------------\n",
    "#  DEMO: comment or delete after testing\n",
    "# ----------------------------------------------------------------------\n",
    "if __name__ == \"__main__\":\n",
    "    width         = 20.0          # square coupon 20 × 20 mm\n",
    "    shell_layers  = 3\n",
    "    layer_width   = 0.4\n",
    "    z_values      = np.linspace(0.0, 10.0, 5)     # 5 Z-slices\n",
    "    area_fraction = 0.30                          # 30 % of inner area\n",
    "\n",
    "    for z in z_values:\n",
    "        radial_equal_area_infill_rectangular(\n",
    "            width=width,\n",
    "            height=width,\n",
    "            area_fraction=area_fraction,\n",
    "            z_height=z,\n",
    "            shell_layers=shell_layers,\n",
    "            layer_width=layer_width,\n",
    "            plot=True\n",
    "        )\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "e7f0e3ec-7f4b-462d-8ab7-a9d123a5ec0d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 406008 characters]",
      "text/plain": [
       "<Figure size 1000x800 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from mpl_toolkits.mplot3d import Axes3D\n",
    "\n",
    "\n",
    "def generate_radial_equal_area_volume(\n",
    "    *,\n",
    "    width=20.0,\n",
    "    height=20.0,\n",
    "    thickness=10.0,\n",
    "    base_period=6.0,\n",
    "    grid_N=100,\n",
    "    layer_count=100,\n",
    "    area_fraction=0.30,\n",
    "    tol=1e-4,\n",
    "    max_iter=40\n",
    "):\n",
    "    \"\"\"\n",
    "    Generates a 3D binary volume of the radial infill pattern\n",
    "    without perimeter shells. Volume shape: (Z, Y, X)\n",
    "    \"\"\"\n",
    "    xs = np.linspace(-width  / 2, width  / 2, grid_N)\n",
    "    ys = np.linspace(-height / 2, height / 2, grid_N)\n",
    "    zs = np.linspace(0.0, thickness, layer_count)\n",
    "    XX, YY = np.meshgrid(xs, ys)\n",
    "    r = np.hypot(XX, YY)\n",
    "\n",
    "    inner_area = width * height\n",
    "    target_area = area_fraction * inner_area\n",
    "\n",
    "    filled_volume = []\n",
    "\n",
    "    for z in zs:\n",
    "        phi = 2.0 * np.pi * z / base_period\n",
    "        k = 2.0 * np.pi / base_period\n",
    "        F = np.sin(k * r + phi)\n",
    "\n",
    "        # binary search for threshold\n",
    "        lo, hi = F.min(), F.max()\n",
    "        for _ in range(max_iter):\n",
    "            t = 0.5 * (lo + hi)\n",
    "            mask = (F >= t)\n",
    "            A = np.count_nonzero(mask) * (width / grid_N) * (height / grid_N)\n",
    "            if abs(A - target_area) < tol * inner_area:\n",
    "                break\n",
    "            if A > target_area:\n",
    "                lo = t\n",
    "            else:\n",
    "                hi = t\n",
    "\n",
    "        filled_volume.append(mask.astype(np.uint8))\n",
    "\n",
    "    return np.stack(filled_volume, axis=0)  # shape: (Z, Y, X)\n",
    "\n",
    "\n",
    "def plot_3d_binary_volume(volume, threshold=0.5, step=2):\n",
    "    \"\"\"\n",
    "    Uses matplotlib to render a 3D surface of the filled voxels.\n",
    "    \"\"\"\n",
    "    z_dim, y_dim, x_dim = volume.shape\n",
    "    fig = plt.figure(figsize=(10, 8))\n",
    "    ax = fig.add_subplot(111, projection=\"3d\")\n",
    "\n",
    "    ax.set_title(\"Radial Equal-Area Infill – 3D View\")\n",
    "    ax.set_xlabel(\"X\")\n",
    "    ax.set_ylabel(\"Y\")\n",
    "    ax.set_zlabel(\"Z\")\n",
    "\n",
    "    ax.view_init(elev=30, azim=45)\n",
    "\n",
    "    for z in range(0, z_dim, step):\n",
    "        mask = volume[z]\n",
    "        y_idx, x_idx = np.nonzero(mask)\n",
    "        x = x_idx - x_dim / 2\n",
    "        y = y_idx - y_dim / 2\n",
    "        z_layer = np.full_like(x, z)\n",
    "        ax.scatter(x, y, z_layer, color='black', s=1, alpha=0.5)\n",
    "\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "\n",
    "# -------------------------------\n",
    "# Run the 3D visualization\n",
    "# -------------------------------\n",
    "if __name__ == \"__main__\":\n",
    "    volume = generate_radial_equal_area_volume(\n",
    "        width=20.0,\n",
    "        height=20.0,\n",
    "        thickness=10.0,\n",
    "        base_period=6.0,\n",
    "        area_fraction=0.60,\n",
    "        grid_N=100,\n",
    "        layer_count=100\n",
    "    )\n",
    "\n",
    "    plot_3d_binary_volume(volume)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "864d8b1e-6a0d-423b-9292-5deef913c351",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f39853ba-c914-4923-8e8c-263bcd88d933",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.13.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
