{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Modeling Hexagonal Lattices\n",
    "In this example, we will create a hexagonal lattice and show how the orientation can be changed via the cell rotation property. Let's first just set up some materials and universes that we will use to fill the lattice."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "import openmc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "fuel = openmc.Material(name='fuel')\n",
    "fuel.add_nuclide('U235', 1.0)\n",
    "fuel.set_density('g/cm3', 10.0)\n",
    "\n",
    "fuel2 = openmc.Material(name='fuel2')\n",
    "fuel2.add_nuclide('U238', 1.0)\n",
    "fuel2.set_density('g/cm3', 10.0)\n",
    "\n",
    "water = openmc.Material(name='water')\n",
    "water.add_nuclide('H1', 2.0)\n",
    "water.add_nuclide('O16', 1.0)\n",
    "water.set_density('g/cm3', 1.0)\n",
    "\n",
    "materials = openmc.Materials((fuel, fuel2, water))\n",
    "materials.export_to_xml()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "With our three materials, we will set up two universes that represent pin-cells: one with a small pin and one with a big pin. Since we will be using these universes in a lattice, it's always a good idea to have an \"outer\" universe as well that is applied outside the defined lattice."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "r_pin = openmc.ZCylinder(r=0.25)\n",
    "fuel_cell = openmc.Cell(fill=fuel, region=-r_pin)\n",
    "water_cell = openmc.Cell(fill=water, region=+r_pin)\n",
    "pin_universe = openmc.Universe(cells=(fuel_cell, water_cell))\n",
    "\n",
    "r_big_pin = openmc.ZCylinder(r=0.5)\n",
    "fuel2_cell = openmc.Cell(fill=fuel2, region=-r_big_pin)\n",
    "water2_cell = openmc.Cell(fill=water, region=+r_big_pin)\n",
    "big_pin_universe = openmc.Universe(cells=(fuel2_cell, water2_cell))\n",
    "\n",
    "all_water_cell = openmc.Cell(fill=water)\n",
    "outer_universe = openmc.Universe(cells=(all_water_cell,))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's create a hexagonal lattice using the `HexLattice` class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "lattice = openmc.HexLattice()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We need to set the `center` of the lattice, the `pitch`, an `outer` universe (which is applied to all lattice elements outside of those that are defined), and a list of `universes`. Let's start with the easy ones first. Note that for a 2D lattice, we only need to specify a single number for the pitch."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "lattice.center = (0., 0.)\n",
    "lattice.pitch = (1.25,)\n",
    "lattice.outer = outer_universe"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we need to set the `universes` property on our lattice. It needs to be set to a list of lists of Universes, where each list of Universes corresponds to a ring of the lattice. The rings are ordered from outermost to innermost, and within each ring the indexing starts at the \"top\". To help visualize the proper indices, we can use the `show_indices()` helper method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                  (0, 0)\n",
      "            (0,17)      (0, 1)\n",
      "      (0,16)      (1, 0)      (0, 2)\n",
      "(0,15)      (1,11)      (1, 1)      (0, 3)\n",
      "      (1,10)      (2, 0)      (1, 2)\n",
      "(0,14)      (2, 5)      (2, 1)      (0, 4)\n",
      "      (1, 9)      (3, 0)      (1, 3)\n",
      "(0,13)      (2, 4)      (2, 2)      (0, 5)\n",
      "      (1, 8)      (2, 3)      (1, 4)\n",
      "(0,12)      (1, 7)      (1, 5)      (0, 6)\n",
      "      (0,11)      (1, 6)      (0, 7)\n",
      "            (0,10)      (0, 8)\n",
      "                  (0, 9)\n"
     ]
    }
   ],
   "source": [
    "print(lattice.show_indices(num_rings=4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's set up a lattice where the first element in each ring is the big pin universe and all other elements are regular pin universes. \n",
    "\n",
    "From the diagram above, we see that the outer ring has 18 elements, the first ring has 12, and the second ring has 6 elements. The innermost ring of any hexagonal lattice will have only a single element. \n",
    "\n",
    "We build these rings through 'list concatenation' as follows: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "outer_ring = [big_pin_universe] + [pin_universe]*17 # Adds up to 18\n",
    "\n",
    "ring_1 = [big_pin_universe] + [pin_universe]*11 # Adds up to 12\n",
    "\n",
    "ring_2 = [big_pin_universe] + [pin_universe]*5 # Adds up to 6\n",
    "\n",
    "inner_ring = [big_pin_universe]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can now assign the rings (and the universes they contain) to our lattice. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "HexLattice\n",
      "\tID             =\t4\n",
      "\tName           =\t\n",
      "\tOrientation    =\ty\n",
      "\t# Rings        =\t4\n",
      "\t# Axial        =\tNone\n",
      "\tCenter         =\t(0.0, 0.0)\n",
      "\tPitch          =\t(1.25,)\n",
      "\tOuter          =\t3\n",
      "\tUniverses      \n",
      "   2\n",
      "  1 1\n",
      " 1 2 1\n",
      "1 1 1 1\n",
      " 1 2 1\n",
      "1 1 1 1\n",
      " 1 2 1\n",
      "1 1 1 1\n",
      " 1 1 1\n",
      "1 1 1 1\n",
      " 1 1 1\n",
      "  1 1\n",
      "   1\n"
     ]
    }
   ],
   "source": [
    "lattice.universes = [outer_ring,\n",
    "                     ring_1,\n",
    "                     ring_2,\n",
    "                     inner_ring]\n",
    "print(lattice)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's put our lattice inside a circular cell that will serve as the top-level cell for our geometry."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "outer_surface = openmc.ZCylinder(r=5.0, boundary_type='vacuum')\n",
    "main_cell = openmc.Cell(fill=lattice, region=-outer_surface)\n",
    "geometry = openmc.Geometry([main_cell])\n",
    "geometry.export_to_xml()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's create a plot to see what our geometry looks like."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 7008 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "plot = openmc.Plot.from_geometry(geometry)\n",
    "plot.color_by = 'material'\n",
    "plot.colors = colors = {\n",
    "    water: 'blue',\n",
    "    fuel: 'olive',\n",
    "    fuel2: 'yellow'\n",
    "}\n",
    "plot.to_ipython_image()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "At this point, if we wanted to simulate the model, we would need to create an instance of `openmc.Settings`, export it to XML, and run."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lattice orientation\n",
    "\n",
    "Now let's say we want our hexagonal lattice orientated such that two sides of the lattice are parallel to the x-axis. This can be achieved by two means: either we can rotate the cell that contains the lattice, or we can can change the `HexLattice.orientation` attribute. By default, the `orientation` is set to \"y\", indicating that two sides of the lattice are parallel to the y-axis, but we can also change it to \"x\" to make them parallel to the x-axis."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 5836 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Change the orientation of the lattice and re-export the geometry\n",
    "lattice.orientation = 'x'\n",
    "geometry.export_to_xml()\n",
    "\n",
    "# Run OpenMC in plotting mode\n",
    "plot.to_ipython_image()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "When we change the orientation to 'x', you can see that the first universe in each ring starts to the right along the x-axis. As before, the universes are defined in a clockwise fashion around each ring. To see the proper indices for a hexagonal lattice in this orientation, we can again call `show_indices` but pass an extra orientation argument:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                  (0,12)      (0,13)      (0,14)      (0,15)\n",
      "\n",
      "            (0,11)      (1, 8)      (1, 9)      (1,10)      (0,16)\n",
      "\n",
      "      (0,10)      (1, 7)      (2, 4)      (2, 5)      (1,11)      (0,17)\n",
      "\n",
      "(0, 9)      (1, 6)      (2, 3)      (3, 0)      (2, 0)      (1, 0)      (0, 0)\n",
      "\n",
      "      (0, 8)      (1, 5)      (2, 2)      (2, 1)      (1, 1)      (0, 1)\n",
      "\n",
      "            (0, 7)      (1, 4)      (1, 3)      (1, 2)      (0, 2)\n",
      "\n",
      "                  (0, 6)      (0, 5)      (0, 4)      (0, 3)\n"
     ]
    }
   ],
   "source": [
    "print(lattice.show_indices(4, orientation='x'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Hexagonal prisms\n",
    "\n",
    "OpenMC also contains a convenience function that can create a hexagonal prism representing the interior region of six surfaces defining a hexagon. This can be useful as a bounding surface of a hexagonal lattice. For example, if we wanted the outer boundary of our geometry to be hexagonal, we could change the `region` of the main cell:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 7204 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "main_cell.region = -openmc.model.HexagonalPrism(\n",
    "    edge_length=4*lattice.pitch[0],\n",
    "    orientation='x',\n",
    "    boundary_type='vacuum'\n",
    ")\n",
    "geometry.export_to_xml()\n",
    "\n",
    "# Run OpenMC in plotting mode\n",
    "plot.color_by = 'cell'\n",
    "plot.to_ipython_image()"
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "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.12.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
