{ "cells": [ { "cell_type": "markdown", "id": "10e37bfc-efa4-4370-af28-a1288e15acc4", "metadata": {}, "source": [ "
\n", " \n", " \"RMS\n", " \n", "
\n", "\n", "
\n", "

Resource Classification Challenges

\n", "

A Companion Notebook to the RMS Blog

\n", "
\n", "\n", "

Sean D. Horan

\n", "

June 20, 2024

\n", "\n", "---\n", "\n", "# Outline\n", "\n", "The following notebook demonstrates some of the challenges related to Mineral Resource classification approaches. A variety of approaches, considerations, and calibrations related to classification flagging are shown. The final section demonstrates a method for calibrating gridded spacings from a drill hole spacing study to correspond with the commonly used 3-hole spacing available for classification flagging.\n", "\n", "1. **Calculate DH Spacing**\n", " - Use the 3-hole rule to calculate the DH spacing at grid locations\n", "2. **Flag Classification based on DH Spacing**\n", " - Directly assign classification based on predetermined classification criteria\n", "3. **Flag Classification based on Search Pass**\n", " - Directly use the classification criteria as radii for search passes and using the search pass number to flag classification\n", " - Adjust the search radii so that the classification approximates the DH spacing classification from Step (1)\n", "5. **Flag Classification based on Estimation Quality Measure**\n", " - Use kriging efficiency (KE) output to assign classification categories\n", " - Use DH spacing to find a KE thresholds that approximate the DH spacing from Step (1)\n", "5. **Assigning Spacing to DH**\n", " - Assign DH spacing calculated in Step (1) directly to composite locations\n", " - Based on spacing of underlying composites, assign indicators of classification criteria to each DH using majority rules\n", " - Estimate the indicators with simple kriging to generate interpolated probabilities\n", " - Plot spacing versus interpolated probabilities and use these thresholds to define the resource categories\n", " - Inspect results for isolated categories and change the DH classification flagging, redefine indicators and re-run.\n", "6. **Scaling Grid Spacings to Represent DH Spacings**\n", " - Sample the grid at various idealised grid configurations (similar to a re-simulation DH spacing workflow)\n", " - Calculate the DH spacing for each of these grids and compare to the DH spacing criteria\n", " - Calculate a scaling factor that is more appropriate than the common $\\sqrt2$ convention\n", " \n", "The presented approaches may be extended to represent other deposits, although no warranty is made regarding error-free implementation. Further, although many techniques are considered and compared, their application here should not be interpretted as recommendation for use.\n", "\n", "\n", "\n", "---\n", "\n", "Import required modules:" ] }, { "cell_type": "code", "execution_count": null, "id": "6dca108e-f870-4872-8e44-f0106cef5b9a", "metadata": { "tags": [] }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", "from copy import deepcopy\n", "import rmsp" ] }, { "cell_type": "markdown", "id": "a7e3b641-7a3d-4d81-803c-e0cac80b812c", "metadata": {}, "source": [ "Set a couple parameters of interest:" ] }, { "cell_type": "code", "execution_count": null, "id": "7e4578a6-93d2-4222-979f-82741237e1c9", "metadata": { "tags": [] }, "outputs": [], "source": [ "# Set to false for interactive viewer\n", "rmsp.GlobalParams[\"plotting.viewer3d.show_static\"] = True\n", "\n", "# Set to true for progressbars on intensive tasks\n", "rmsp.GlobalParams[\"core.log_progress\"] = False" ] }, { "cell_type": "markdown", "id": "9abe4aa7-b92d-4447-b3c9-e070363dd9af", "metadata": {}, "source": [ "Set additional plotting parameters:" ] }, { "cell_type": "code", "execution_count": null, "id": "8dc259a4-7500-42b0-a61d-8e3d4b51d5a7", "metadata": { "tags": [] }, "outputs": [], "source": [ "rmsp.GlobalParams[\"core.enable_beta\"] = True\n", "rmsp.GlobalParams[\"plotting.viewer3d.background\"] = \"white\"\n", "rmsp.GlobalParams['plotting.viewer3d.dpi'] = 500\n", "rmsp.GlobalParams[\"plotting.viewer3d.render_points_as_spheres\"] = True\n", "rmsp.GlobalParams[\"plotting.cmap\"] = \"Spectral_r\"\n", "camera = [[-522.65, -883.05, 79.82, 200.06, 100.13, 97.29, -0.01, -0.01, 1.00], 113.50]" ] }, { "cell_type": "markdown", "id": "df890280-5cc5-49f8-9100-a6ac0c8009a7", "metadata": { "tags": [] }, "source": [ "Colormaps that we'll use for classification, spacing and kriging efficiency:" ] }, { "cell_type": "code", "execution_count": null, "id": "3ffc0b8b-cb0a-4e18-bd36-db4dec3b54a8", "metadata": { "tags": [] }, "outputs": [], "source": [ "cmap_class={\"Measured\": \"C3\", \"Indicated\": \"C2\",\n", " \"Inferred\": \"C0\", \"Unclassified\": \".7\"}\n", "cmap_space= {\"bounds\":[5., 10., 15., 20., 25., 30., 40.],\n", " \"colors\":[\"#b2182b\", \"#ef8a62\", \"#fddbc7\",\n", " \"#ffffff\", \"#e0e0e0\", \"#999999\"]}\n", "cmap_ke = {\"bounds\":[0.1, 0.2, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6],\n", " \"colors\":[\"#878787\", \"#bababa\", \"#e0e0e0\", \"#ffffff\",\n", " \"#fddbc7\", \"#f4a582\", \"#d6604d\", \"#b2182b\"]}" ] }, { "cell_type": "markdown", "id": "90d26464-e128-4cea-aaa4-d6a4b874c266", "metadata": {}, "source": [ "---\n", "# Loading Example Data\n", "\n", "Load tabular vein data and solid:" ] }, { "cell_type": "code", "execution_count": null, "id": "79f406b5-6210-4a63-9a35-3973232edf7f", "metadata": { "tags": [] }, "outputs": [], "source": [ "dh, solid, topo = rmsp.load_example_data('single_vein')" ] }, { "cell_type": "markdown", "id": "086f7be7-368b-40b4-b0a7-773cc088f387", "metadata": {}, "source": [ "Reduce to data inside the vein solid:" ] }, { "cell_type": "code", "execution_count": null, "id": "ac24d538-2f39-43ea-9908-9983245e3c61", "metadata": { "tags": [] }, "outputs": [], "source": [ "dh = dh[solid.contains(dh)].reset_index(drop=True)\n", "dh.head(2)" ] }, { "cell_type": "markdown", "id": "bace6e33-9751-414b-aa59-3c4c7bebdc34", "metadata": { "tags": [] }, "source": [ "View:" ] }, { "cell_type": "code", "execution_count": null, "id": "c7e1f497-383b-4083-94e6-17d5905f5c69", "metadata": { "tags": [] }, "outputs": [], "source": [ "viewer = dh.view3d(var='Au', representation='Surface')\n", "solid.view3d(viewer=viewer, alpha=0.1, color='.5')\n", "topo.view3d(viewer=viewer)\n", "viewer.set_camera(*camera)" ] }, { "cell_type": "markdown", "id": "07e1443f-418b-4a28-bf81-eb3391487440", "metadata": { "tags": [] }, "source": [ "Create a grid inside the solid:" ] }, { "cell_type": "code", "execution_count": null, "id": "5ab3a6c9-ad2f-4820-970d-39e1a050f7c4", "metadata": { "tags": [] }, "outputs": [], "source": [ "grid = rmsp.GridData.from_solid(solid, usize=1, vsize=1, zsize=1)\n", "grid.griddef.to_table()" ] }, { "cell_type": "markdown", "id": "f932d592-5214-49e9-8f82-8745e373f0e7", "metadata": {}, "source": [ "---\n", "# Calculate the Drill Hole Spacing\n", "For this example the 3-hole rule is used. The workflow is as follows:\n", "1. Use a large isotropic ellipsoid\n", "2. Set search criteria to minimum composites of 3 and maximum of 3 with a limit of one composite per hole\n", "3. Run an estimator (inverse distance used here) and record the average distance to the nearest 3 composites meeting the criteria above.\n", "4. Convert the average distance to a drillhole spacing through multiplying by $\\sqrt2$. \n", "\n", "
\n", "It should be noted that there are various ways to calculate DH spacing, the 3-hole rule used here is just one methodology. Towards the end of the notebook is a demonstration of how to relate the DH spacing back to your drill hole spacing study results.\n", "
" ] }, { "cell_type": "code", "execution_count": null, "id": "d1297ffc-f2e8-4277-bd59-850db2f17cd0", "metadata": { "tags": [] }, "outputs": [], "source": [ "search = rmsp.Search(min_comps=3, max_comps=3, max_comps_per_dhid=1)\n", "grid['spacing'] = rmsp.IDWEstimator(search).estimate(\n", " grid, dh, 'Au', output=('mean_dist'), copy_meta=False) * 2**0.5\n", "grid.head()" ] }, { "cell_type": "markdown", "id": "cc163826-0770-4bbe-b8bc-ca8bee410ecd", "metadata": {}, "source": [ "Plot distribution and oblique view of spacing:" ] }, { "cell_type": "code", "execution_count": null, "id": "1f9ab057-b4a5-4d83-936e-4d86a570f643", "metadata": { "tags": [] }, "outputs": [], "source": [ "viewer=grid.view3d('spacing', cmap=cmap_space)\n", "viewer.set_camera(*camera)\n", "\n", "fig, axes = plt.subplots(1, 2, figsize=(10, 3.4))\n", "grid.histplot('spacing', ax=axes[0])\n", "viewer.show_static(ax=axes[1], cbar=True)\n", "fig.tight_layout()" ] }, { "cell_type": "markdown", "id": "5e21b688-f75d-4e34-859c-5109da827d6b", "metadata": {}, "source": [ "---\n", "# Flag Classification based on DH Spacing\n", "Apply the classification flags directly to blocks based on DH spacing alone. In this example a drill hole spacing criteria is set to the following:\n", "1. Measured: <15m spacing\n", "2. Indicated: 15-30m spacing\n", "3. Inferred: 30-60m spacing" ] }, { "cell_type": "code", "execution_count": null, "id": "f935fe0e-af8b-4050-8ed1-82157ada28b4", "metadata": { "tags": [] }, "outputs": [], "source": [ "spac_crit = {\"Measured\": 15.0,\n", " \"Indicated\": 30.0,\n", " \"Inferred\": 60.0}\n", "\n", "grid[\"class_space\"] = 'Unclassified'\n", "for rescat, space in reversed(spac_crit.items()):\n", " grid.loc[grid['spacing'] <= space, 'class_space'] = rescat\n", "\n", "grid[['spacing', 'class_space']].tail()" ] }, { "cell_type": "markdown", "id": "70d9bcc4-5e5e-4e17-b514-08fbe32c8746", "metadata": {}, "source": [ "Overall the result looks reasonable with some areas that may require post processing (spheres highlight examples where the practitioner may opt to downgrade the measured to indicated):" ] }, { "cell_type": "code", "execution_count": null, "id": "1e059b91-3653-480f-9b66-22e0845d0d24", "metadata": { "tags": [] }, "outputs": [], "source": [ "viewer = grid.view3d(\"class_space\", cmap_cat=cmap_class)\n", "viewer.set_camera(*camera)\n", "\n", "# Highlight three problem areas\n", "problem_areas = [[82, 132.93, 169.73], [74.92, 107.85, 129.55], [49.24, 129.23, 157.55]]\n", "rmsp.Solid.from_merged_meshes(\n", " [rmsp.Solid.icosphere(10, p) for p in problem_areas]\n", ").view3d(viewer=viewer, label=\"To Downgrade\", alpha=0.5, color=\"yellow\")\n", "viewer" ] }, { "cell_type": "markdown", "id": "8a19d861-4bbd-4942-926e-d011e54c7201", "metadata": {}, "source": [ "---\n", "# Flag Classification using a Search Pass Approach\n", "\n", "## Baseline pass radii for classification\n", "Perform multi-pass estimation. A typical search pass strategy is used where the max per hole is less than the minimum samples required, ensuring that at least two holes are used for an estimate." ] }, { "cell_type": "code", "execution_count": null, "id": "54cfcf7c-09fb-4cfb-8d0e-c956d6138083", "metadata": { "tags": [] }, "outputs": [], "source": [ "estimators = []\n", "spacings = list(spac_crit.values())\n", "for i, r in enumerate(spacings):\n", " search = rmsp.Search(\n", " ranges=[r] * 3, min_comps=3, max_comps=12, max_comps_per_dhid=2\n", " )\n", " estimators.append(rmsp.IDWEstimator(search))\n", "\n", "multipass = rmsp.MultipassEstimator(estimators)\n", "grid[\"class_pass\"] = multipass.estimate(grid, dh, \"Au\")[\"multipass_flag\"]\n", "\n", "print(\"unique pass numbers\", grid[\"class_pass\"].unique())" ] }, { "cell_type": "markdown", "id": "79c6373e-a3d3-4fbe-b7e9-01cbe7af85fc", "metadata": {}, "source": [ "Without much consideration, assign classification based on pass number. Note that this mapping assumes all blocks are estimated with a pass number." ] }, { "cell_type": "code", "execution_count": null, "id": "ba2d7b40-1d32-4c1a-9d66-00ff86984439", "metadata": { "tags": [] }, "outputs": [], "source": [ "pass_to_class = {0: \"Measured\", 1: \"Indicated\", 2: \"Inferred\"}\n", "grid[\"class_pass\"] = grid[\"class_pass\"].map(pass_to_class)\n", "\n", "print(\"unique class codes\", grid[\"class_pass\"].unique())" ] }, { "cell_type": "markdown", "id": "e4be3096-947a-423a-b5ae-f410f93385c0", "metadata": {}, "source": [ "Compare counts:" ] }, { "cell_type": "code", "execution_count": null, "id": "b530c9bf-499c-4dbe-8b3c-654c234f1e57", "metadata": { "tags": [] }, "outputs": [], "source": [ "def add_counts_to_dict(var, title, cellcount_dict):\n", " \"\"\"Function for counting cells by category, returning a table for visualization\"\"\"\n", " cellcount_dict[title] = grid.groupby([var])[var].count()\n", " return pd.DataFrame(cellcount_dict)\n", "\n", "cellcount_dict = {}\n", "_ = add_counts_to_dict(\"class_space\", \"Spacing Method\", cellcount_dict)\n", "add_counts_to_dict(\"class_pass\", \"Search Pass Method\", cellcount_dict)" ] }, { "cell_type": "markdown", "id": "1654ab4a-d4c3-4909-96a9-7344c3e31c6c", "metadata": {}, "source": [ "Compare the search pass classification to the spacing classification:" ] }, { "cell_type": "code", "execution_count": null, "id": "3bbefe7d-c562-4768-a4a9-614ba14e898f", "metadata": { "tags": [] }, "outputs": [], "source": [ "def comparison_plots(\n", " variables=[\"class_pass\", \"class_space\"],\n", " titles=[\"Search Pass Classification\", \"DH Spacing Classification\"],\n", " cmaps=[cmap_class, cmap_class]):\n", " \"\"\"Function for repeatedly comparing many classifications\"\"\"\n", " fig, axes = plt.subplots(1, 2, figsize=(10, 3.4))\n", " for i, (var, title, ax, cmap) in enumerate(zip(variables, titles, axes, cmaps)):\n", " vw = grid.view3d(var=var, cmap_cat=cmap)\n", " vw.set_camera(*camera)\n", " vw.show_static(ax=ax, cbar=i==1, title=title)\n", "\n", "comparison_plots()" ] }, { "cell_type": "markdown", "id": "0d803243-7e43-4a5c-9c09-5b8dc6a21ccd", "metadata": {}, "source": [ "## Calibrated pass radii for classification\n", "\n", "It's clear from the images and results above that using the classification directly from the search ranges results in vastly different classification volumes. The code block below will look for a factor `fact` on the search ranges that will approximate the spacing classification (based on the measured volume):" ] }, { "cell_type": "code", "execution_count": null, "id": "239ac391-5562-439e-82d4-a6dc8bf5ad32", "metadata": { "tags": [] }, "outputs": [], "source": [ "fact = 0.4\n", "grid['class_pass'] = 'Unclassified'\n", "while np.sum(grid['class_pass'] == 'Measured') < np.sum(grid['class_space'] == 'Measured') and fact<0.9:\n", " fact+=0.01\n", " estimators=[]\n", " for i, r in enumerate(spacings):\n", " search=rmsp.Search(ranges=[r*fact]*3, min_comps=3, max_comps=12, max_comps_per_dhid=2)\n", " estimators.append(rmsp.IDWEstimator(search))\n", " multipass = rmsp.MultipassEstimator(estimators)\n", " grid['class_pass'] = multipass.estimate(grid, dh, 'Au', progressbar=False)['multipass_flag']\n", " grid[\"class_pass\"] = grid[\"class_pass\"].map(pass_to_class)\n", "print(f\"Factor on ranges applied = {fact}\")" ] }, { "cell_type": "markdown", "id": "c9eedec8-4d1d-4b40-88bf-6ea1d910a69c", "metadata": {}, "source": [ "Cell count comparison:" ] }, { "cell_type": "code", "execution_count": null, "id": "53e9fa69-a9d1-46b0-bc37-1600fced3be5", "metadata": { "tags": [] }, "outputs": [], "source": [ "_ = add_counts_to_dict(\"class_space\", \"Spacing Method\", cellcount_dict)\n", "add_counts_to_dict(\"class_pass\", \"Search Pass Method\", cellcount_dict)" ] }, { "cell_type": "markdown", "id": "822580bf-52ca-4efb-91be-72e623ef9175", "metadata": {}, "source": [ "Radii required to match DH spacing:" ] }, { "cell_type": "code", "execution_count": null, "id": "aa9f82f7-85f6-4ff4-86a7-b37e399b7dfa", "metadata": { "tags": [] }, "outputs": [], "source": [ "print(\"Measured:\", np.round(fact*np.array(spacings)[0]))\n", "print(\"Indicated:\", np.round(fact*np.array(spacings)[1]))\n", "print(\"Inferred:\", np.round(fact*np.array(spacings)[2]))" ] }, { "cell_type": "markdown", "id": "192c6b47-1fd0-4962-b5cb-8d34015c60cd", "metadata": {}, "source": [ "Visually compare:" ] }, { "cell_type": "code", "execution_count": null, "id": "723bc1ea-a962-40a5-a8aa-659cc4d23163", "metadata": { "tags": [] }, "outputs": [], "source": [ "comparison_plots(\n", " [\"class_pass\", \"class_space\"],\n", " [\"Search Pass Classification\", \"DH Spacing Classification\"],\n", ")" ] }, { "cell_type": "markdown", "id": "d7e87b67-f78a-44a7-bd44-fd9e2bd4c94a", "metadata": {}, "source": [ "When optimising the search ellipse radii, we can approximate the DH spacing classification. While the results become quite similar, there are some notable artifacts in the search pass approach which will cause some issues during reserve estimation. Interesting to note that the radii required to match the DH spacing classification are approximately 70% of the nominal spacing criteria:\n", "\n", "1. Measured radius = 11m\n", "2. Indicated radius = 21m\n", "3. Inferred radius = 43m\n", "\n", "---\n", "# Flag Classification using Estimation Quality Measure\n", "\n", "It is common practice to using estimation measures as a proxy for determining classification categories. While it can provide similar results to drill hole spacing categorization, it does suffer from some weaknesses including:abs\n", "\n", "1. How to pick the correct kriging threshold that approximates criteria determined using, for example, a drill hole spacing study.\n", "2. Direct flagging of classification based on kriging efficiency can lead to some unwanted artifacts.\n", "\n", "Calculate kriging efficiency:" ] }, { "cell_type": "code", "execution_count": null, "id": "e78965cb-5316-4f1f-b7d3-19d5760a1db5", "metadata": { "tags": [] }, "outputs": [], "source": [ "search = rmsp.Search(\n", " ranges=[100, 100, 100], min_comps=3, max_comps=3, max_comps_per_dhid=1\n", ")\n", "vario = rmsp.VarioModel.from_params(\n", " 1, 0.3, \"spherical\", [0.7], angles=[0, 0, 0], ranges=[80] * 3\n", ")\n", "kriger = rmsp.KrigeEstimator(search, vario)\n", "grid['KE'] = kriger.estimate(grid, dh, \"Au\", output=[\"efficiency\"])[\"efficiency\"]" ] }, { "cell_type": "markdown", "id": "0f36eecd-0df1-488d-bab4-fe7bd0cbb4f8", "metadata": {}, "source": [ "View results:" ] }, { "cell_type": "code", "execution_count": null, "id": "806107b6-09c7-4a9f-a8d6-7824b7b9cb1a", "metadata": { "tags": [] }, "outputs": [], "source": [ "fig, axes = plt.subplots(1, 2, figsize=(10, 3.4))\n", "viewer = grid.view3d(var='KE', cmap=cmap_ke)\n", "viewer.set_camera(*camera)\n", "viewer.show_static(ax=axes[1], title=\"Kriging Efficiency\")\n", "grid.scatplot(\n", " ax=axes[0], var0='spacing',\n", " var1='KE',\n", " c='class_space',\n", " cbar=True,\n", " title=\"Spacing vs KE coloured by Spacing Criteria\",\n", " grid=True, s=5,\n", " cmap=cmap_class\n", ")\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "d50a38ea-8e7f-4c78-af33-14df781dc048", "metadata": {}, "source": [ "In the next step, the kriging effiency thresholds are selected by approximating the spacing criteria in the measured and indicated categories:" ] }, { "cell_type": "code", "execution_count": null, "id": "68609fe4-ebe4-4012-9db3-a7c16a36ab80", "metadata": {}, "outputs": [], "source": [ "ke = 1.0\n", "grid[\"class_ke\"] = \"Inferred\"\n", "ke_crit = {}\n", "\n", "while np.sum(grid['class_ke'] == \"Measured\") < np.sum(grid['class_space'] == \"Measured\"):\n", " ke -= 0.001\n", " grid.loc[grid['KE'] >= ke, \"class_ke\"] = \"Measured\"\n", "\n", "ke_crit[\"Measured\"] = [np.round(ke, 3)]\n", "\n", "while np.sum(grid['class_ke'] == \"Indicated\") < np.sum(grid['class_space'] == \"Indicated\"):\n", " ke -= 0.001\n", " grid.loc[(grid['KE'] >= ke) & (grid['class_ke'] != \"Measured\"), \"class_ke\"] = \"Indicated\"\n", "\n", "ke_crit[\"Indicated\"] = [np.round(ke, 3)]\n", "ke_crit[\"Inferred\"] = [0]\n", "\n", "print(\"KE Required to Match DH Spacing:\")\n", "print(\"\")\n", "pd.DataFrame(ke_crit)" ] }, { "cell_type": "markdown", "id": "b6cea43f-08da-4d18-92dd-dec1b5697e90", "metadata": {}, "source": [ "Compare classification counts to prior methods:" ] }, { "cell_type": "code", "execution_count": null, "id": "2b15972a-6b36-460e-9098-4aa2538c62ba", "metadata": { "tags": [] }, "outputs": [], "source": [ "add_counts_to_dict(\"class_ke\", \"Kriging Efficiency Method\", cellcount_dict)" ] }, { "cell_type": "markdown", "id": "79cc0823-bc93-4918-ae77-dba453def71c", "metadata": {}, "source": [ "Visually compare against spacing based classification:" ] }, { "cell_type": "code", "execution_count": null, "id": "f9b59e26-da4d-462d-910f-10a65d05fe58", "metadata": {}, "outputs": [], "source": [ "comparison_plots(\n", " [\"class_ke\", \"class_space\"],\n", " [\"Kriging Efficiency Classification\", \"DH Spacing Classification\"],\n", ")" ] }, { "cell_type": "markdown", "id": "ea100c8d-fa08-4520-8e5c-0af00c1e327b", "metadata": {}, "source": [ "Similar to the search pass approach, it is possible to reasonably approximate volumes and patterns with the downside that some artifacts are present. A more reasonable approach might be to use kriging efficiency as a guide or validation tool.\n", "\n", "---\n", "# Assigning Spacing to DH Before Interpolating Spacing\n", "For the last classification approach example, the pre-calculated DH spacing value will be assigned to the composites themselves. Then using the classificaiton criteria, we can define a indicators for each class. The indicators can then be interpolated and the resulting indicator probabilities can be inspected relative to the original data spacing to determine reasonable thresholds for defining each category.\n", "Once the base thresholds are determined, further post processing can done to clean up the categories.\n", "\n", "## Determine representative spacing and classification for each DH" ] }, { "cell_type": "markdown", "id": "c9da0f49-8c65-4781-ada4-aa64717c649d", "metadata": { "tags": [] }, "source": [ "Begin by back-flagging the gridded spacing onto the composite locations:" ] }, { "cell_type": "code", "execution_count": null, "id": "a4b84f03-a1e9-4421-9db6-c35e0ddd4006", "metadata": { "tags": [] }, "outputs": [], "source": [ "dh[\"spacing\"] = grid.get_values_at(dh, columns_to_flag=[\"spacing\"], copy_meta=False)\n", "for rescat, space in reversed(spac_crit.items()):\n", " dh[rescat] = np.where(dh['spacing'] <= space, 1.0, 0.0)\n", "\n", "dh[[\"DHID\", \"From\", \"To\", \"spacing\"] + list(spac_crit)].head()" ] }, { "cell_type": "markdown", "id": "7920b769-c625-40d7-938f-a2c6031b50ee", "metadata": {}, "source": [ "Calculate the proportion of each DHID that exceeds each spacing threshold, before assigning a classification to each drillhole based on majority rules. Note that this only applicable for narrow tabular type mineralization." ] }, { "cell_type": "code", "execution_count": null, "id": "293aeb13-e82b-4b9e-bdb9-257b21fb3c24", "metadata": { "tags": [] }, "outputs": [], "source": [ "dhid_grouped = dh.groupby([\"DHID\"], as_index=True).mean()[list(spac_crit)].reset_index()\n", "dhid_grouped[\"class_space\"] = \"Unclassified\"\n", "for rescat in reversed(spac_crit):\n", " dhid_grouped.loc[dhid_grouped[rescat] >= 0.5, \"class_space\"] = rescat\n", "\n", "dhid_grouped[[\"DHID\", \"class_space\"] + list(spac_crit)].head()" ] }, { "cell_type": "markdown", "id": "3b3a546a-621b-4d68-9c4b-9494c5276e41", "metadata": {}, "source": [ "Return the calculated classification to the composites by DHID before reseting indicators accordingly:" ] }, { "cell_type": "code", "execution_count": null, "id": "f58ffa7b-092c-410b-877f-b2848a9ae861", "metadata": { "tags": [] }, "outputs": [], "source": [ "dh = dh.merge(dhid_grouped[[\"DHID\", \"class_space\"]], on=\"DHID\")\n", "# resetting indicators\n", "for rescat in spac_crit:\n", " dh[rescat] = 0.0\n", "dh.loc[dh['class_space'].isin([\"Measured\"]), \"Measured\"] = 1.0\n", "dh.loc[dh['class_space'].isin([\"Measured\", \"Indicated\"]), \"Indicated\"] = 1.0\n", "dh.loc[dh['class_space'].isin([\"Measured\", \"Indicated\", \"Inferred\"]), \"Inferred\"] = 1.0\n", "\n", "dh[[\"DHID\", \"From\", \"To\", \"spacing\", \"class_space\"] + list(spac_crit)].head()" ] }, { "cell_type": "markdown", "id": "5d9206fb-fa33-4a63-b960-b20df349a23f", "metadata": {}, "source": [ "Check to see that the flagging matches the blocks in terms of spacing and classification:" ] }, { "cell_type": "code", "execution_count": null, "id": "c5b7e955-e715-42c2-ad78-25830ea352e1", "metadata": {}, "outputs": [], "source": [ "viewer1 = grid.view3d(var=\"spacing\", cmap=cmap_space, alpha=0.1)\n", "dh.view3d(viewer=viewer1, var=\"spacing\", cmap=cmap_space, point_size=30)\n", "viewer1.set_camera(*camera)\n", "\n", "viewer2 = grid.view3d(var=\"class_space\", cmap_cat=cmap_class, alpha=0.1)\n", "dh.view3d(viewer=viewer2, var=\"class_space\", cmap_cat=cmap_class, point_size=30)\n", "viewer2.set_camera(*camera)\n", "\n", "fig, axes = plt.subplots(1, 2, figsize=(10, 3.4))\n", "viewer1.show_static(ax=axes[0], title='Spacing Assigned to DH')\n", "viewer2.show_static(ax=axes[1], title='DH Spacing Classification and Classified DH')" ] }, { "cell_type": "markdown", "id": "ae99f20f-2cb1-4e63-b7bc-79ff94a8517e", "metadata": {}, "source": [ "## Simple kriging of indicators and probability thresholding for classification\n", "\n", "Simple kriging is used in the next step to interpolate the indicator of each classification category with the range set to the predetermined spacing criteria and simple kriging mean to zero. By setting the simple kriging mean to zero, the indicator will decay towards zero away from data avoiding overextrapolation in the absence of data along edges of the domains or in data gaps." ] }, { "cell_type": "code", "execution_count": null, "id": "642116b2-643f-4b85-8b93-98a10fc0629c", "metadata": {}, "outputs": [], "source": [ "def simple_krige_each_indicator(data):\n", " \"\"\"Defining this in a function as we'll iterate with/without control points\"\"\"\n", " for rescat, space in reversed(spac_crit.items()):\n", " print(f'Interpolating {rescat} with a variogram/search tied to {space} ranges')\n", " search = rmsp.Search(ranges=space * 2.0, min_comps=1, max_comps=12, max_comps_per_dhid=5)\n", " vario = rmsp.VarioModel.from_params(1, 0.01, 'spherical', [0.99], [0,0,0], [space]*3)\n", " grid[rescat] = rmsp.KrigeEstimator(search, vario, ktype='sk', sk_mean=0.0).estimate(\n", " grid, data, rescat, output=['estimate'], copy_meta=False)\n", "\n", "simple_krige_each_indicator(dh)" ] }, { "cell_type": "markdown", "id": "faea9e11-e707-49f2-839f-099114c0a798", "metadata": {}, "source": [ "We can now inspect spacing versus the estimated indicators to select a probability threshold that matches the DH spacing criteria:" ] }, { "cell_type": "code", "execution_count": null, "id": "129a2d0d-a325-42e2-b712-a8c9f2cb8442", "metadata": {}, "outputs": [], "source": [ "fig, axes = plt.subplots(1, 3, figsize=(10, 3))\n", "for i, (rescat, space) in enumerate(spac_crit.items()):\n", " grid.scatplot(ax=axes[i], var0='spacing', var1=rescat,\n", " ylabel=rescat + \" Probability\")\n", " axes[i].axvline(space, c=cmap_class[rescat], ls='--')\n", "fig.tight_layout()" ] }, { "cell_type": "markdown", "id": "c05a00cf-d558-4256-b01a-c93f14f8bdd0", "metadata": {}, "source": [ "The following probability thresholds are estimated based on the scatter plots above - regression may be better:" ] }, { "cell_type": "code", "execution_count": null, "id": "9f1d5965-1358-43d3-bafc-a71353608efe", "metadata": {}, "outputs": [], "source": [ "spac_dh_crit = {\"Measured\": 0.4, \"Indicated\": 0.3, \"Inferred\": 0}\n", "\n", "grid['class_space_dh'] = \"Unclassified\"\n", "for rescat, prob in reversed(spac_dh_crit.items()):\n", " grid.loc[grid[rescat] >= prob, \"class_space_dh\"] = rescat" ] }, { "cell_type": "markdown", "id": "62d31d22-de9a-46cf-abdb-de5bcc26a78e", "metadata": {}, "source": [ "Compare the resulting classification counts against prior methods:" ] }, { "cell_type": "code", "execution_count": null, "id": "a9754267-d7da-4e9e-8183-940185ed2d3d", "metadata": { "tags": [] }, "outputs": [], "source": [ "add_counts_to_dict(\"class_space_dh\", \"Spacing to DH Method\", cellcount_dict)" ] }, { "cell_type": "markdown", "id": "2d8c9b7c-3fbf-451b-8f23-9348d71234f8", "metadata": {}, "source": [ "Compare the SK and Spacing based classifications visually:" ] }, { "cell_type": "code", "execution_count": null, "id": "290b0970-3c10-4d65-bde8-fba21a7d2ca8", "metadata": {}, "outputs": [], "source": [ "comparison_plots(\n", " [\"class_space_dh\", \"class_space\"],\n", " [\"Spacing Assigned to DH Classification (using SK)\", \"DH Spacing Classification\"])" ] }, { "cell_type": "markdown", "id": "d5ffaab2-470c-41d7-a736-0067c1d297af", "metadata": {}, "source": [ "Compare the SK classification against the flagged DH classification:" ] }, { "cell_type": "code", "execution_count": null, "id": "cd63bf94-9ec9-4bc7-966b-4375b24f7749", "metadata": {}, "outputs": [], "source": [ "viewer = grid.view3d(var=\"class_space_dh\", cmap_cat=cmap_class, alpha=0.1)\n", "viewer = dh.view3d(viewer=viewer, var=[\"class_space\", \"DHID\"], cmap_cat=cmap_class, point_size=30)\n", "viewer.set_camera(*camera)\n", "viewer" ] }, { "cell_type": "markdown", "id": "59614eaf-12bf-4c4f-a263-3c1a8b8167cc", "metadata": { "tags": [] }, "source": [ "## Manual adjustment for classification cleaning\n", "\n", "Some manual adjustment can be made by querying dh and locations in the 3d scene above.\n", "\n", "Add a control point to upgrade some indicated material that is surrounded by measured:" ] }, { "cell_type": "code", "execution_count": null, "id": "a13c311e-231d-4ffb-a8ab-153592b8e83b", "metadata": { "tags": [] }, "outputs": [], "source": [ "control_point_data = {'Easting': 205.43, 'Northing': 78.82, 'Elevation': 126.18, 'DHID': 'dh_added1'}\n", "control_point = dh.iloc[0].to_frame().T.assign(**control_point_data)\n", "control_point_data['Measured'] = 1.0\n", "control_point_data['Status'] = 'Adjusted'\n", "\n", "# Append to data\n", "dh['Status'] = 'As-is'\n", "dh_adj = dh.append(control_point_data, ignore_index=True)\n", "dh_adj[['DHID', 'Easting', 'Northing', 'Elevation', 'Measured', 'Status']].tail(3)" ] }, { "cell_type": "markdown", "id": "f361f8fa-c608-4378-bb3c-ffb9a282747d", "metadata": { "tags": [] }, "source": [ "The following DH's will be adjusted from \"measured\" to \"indicated\":" ] }, { "cell_type": "code", "execution_count": null, "id": "c120e7cc-6374-42bc-a9c3-b09180dce962", "metadata": { "tags": [] }, "outputs": [], "source": [ "measured_to_indicated = [\"DDH315\", \"DDH349\", \"DDH201\", \"DDH201\", \"DDH030\"]\n", "dh_adj.loc[dh_adj['DHID'].isin(measured_to_indicated), \"Measured\"] = 0.0\n", "dh_adj.loc[dh_adj['DHID'].isin(measured_to_indicated), \"Status\"] = \"Adjusted\"" ] }, { "cell_type": "markdown", "id": "d1fa1281-64ae-4174-97a1-b18f2000b9e9", "metadata": {}, "source": [ "
\n", " Warning! The drill hole object `dh` was adjusted to `dh_adj` only for the purposes of classification. This adjusted object should not be used for any other purposes such as grade estimation\n", "
\n", "\n", "Re-interpolate the indicators before applying the same probability thresholds for classification:" ] }, { "cell_type": "code", "execution_count": null, "id": "1ea248fd-afe1-4623-aa98-f4d015d96cfb", "metadata": { "tags": [] }, "outputs": [], "source": [ "simple_krige_each_indicator(dh_adj)\n", "grid['class_space_dh_adj'] = \"Unclassified\"\n", "for rescat, prob in reversed(spac_dh_crit.items()):\n", " grid.loc[grid[rescat] >= prob, \"class_space_dh_adj\"] = rescat" ] }, { "cell_type": "markdown", "id": "7c419460-ff85-47b1-89a4-2dc8ddf1ff4e", "metadata": {}, "source": [ "Compare classification counts:" ] }, { "cell_type": "code", "execution_count": null, "id": "c0eece7a-197e-46d4-b611-1b60b3162a58", "metadata": { "tags": [] }, "outputs": [], "source": [ "add_counts_to_dict(\"class_space_dh_adj\", \"Spacing to DH Method Adjusted\", cellcount_dict)" ] }, { "cell_type": "markdown", "id": "45084b02-80ad-4838-aaae-9ab0f8e7fc15", "metadata": {}, "source": [ "Visually compare before and after adjustment:" ] }, { "cell_type": "code", "execution_count": null, "id": "452fe969-56d2-458c-950e-9222bec0fc93", "metadata": {}, "outputs": [], "source": [ "cmap_adj = {\"As-is\": \"white\", \"Adjusted\": \"black\"}\n", "fig, axes = plt.subplots(1, 2, figsize=(10, 3.4))\n", "for i, (var, title, cbarshow) in enumerate(\n", " zip(\n", " [\"class_space_dh_adj\", \"class_space_dh\"],\n", " [\n", " \"Spacing Assigned to DH Classification (adjusted)\",\n", " \"Spacing Assigned to DH Classification (original)\",\n", " ],\n", " [False, True],\n", " )\n", "):\n", " viewer = grid.view3d(var=var, cmap_cat=cmap_class)\n", " dh_adj.view3d(viewer=viewer, var=\"Status\", cmap_cat=cmap_adj, point_size=50)\n", " viewer.set_camera(*camera)\n", " viewer.show_static(ax=axes[i], cbar=cbarshow, title=title)" ] }, { "cell_type": "markdown", "id": "1ac205a3-1ddb-4ab6-810b-e2dfab2db9cb", "metadata": { "tags": [] }, "source": [ "---\n", "# Testing Synthetic Grid Configurations\n", "A common method for determining DH spacing criteria is resampling of simulated realizations and resimulation to observe the uncertainty. The resampling requires the user to specify a grid configuration consiting of the following attributes:\n", "1. Grid spacing (isotropic or wider in the given direction)\n", "2. Composite length\n", "3. Azimuth of holes\n", "4. Inclination of holes\n", " \n", "The limitation of this resampling approach is that in reality, one single drill hole oriention drilled on a regular grid is rarely achievable. Some factors influencing this include:\n", "1. Available drilling platforms\n", "2. Geometric complexity of the deposit\n", "3. Drill hole deviation\n", "4. The presence of historic/superseded drilling grid configurations\n", "\n", "For this reason, it is important to calculate the actual drill hole spacing using a defined technique (e.g. the 3-hole rule) and then apply this to the idealised sampled grid configuration. This may lead to the use of factors outside of the commonly used $ \\sqrt{2} $.\n", "\n", "## Generate gridded datasets\n", "\n", "To begin, lets create some synthetic samples based on the following grid configurations:\n", "1. Spacings 5m to 65m stepping by 5m (assuming isotropic grids)\n", "2. Composite length of 1m\n", "3. Azimuth = 22.3\n", "4. Inclination = 35.6\n" ] }, { "cell_type": "code", "execution_count": null, "id": "78af3a0b-4b1f-4b33-8d14-f33850799e6a", "metadata": {}, "outputs": [], "source": [ "spacings = np.arange(5, 65, 5)\n", "sampler = rmsp.ModelSampler(\n", " spacings=spacings,\n", " plane_orient=\"xy\",\n", " comp_len=1.0,\n", " locations=grid,\n", " azm=22.3,\n", " incl=-35.6\n", ")\n", "sampler.sample(grid, [\"spacing\"])" ] }, { "cell_type": "markdown", "id": "758c1a65-2501-46b3-ba75-a4ef743bfaea", "metadata": {}, "source": [ "Visualize the synthetically sampled composite locations:" ] }, { "cell_type": "code", "execution_count": null, "id": "167a3a5c-d991-4d19-91d8-3423785b30e8", "metadata": { "tags": [] }, "outputs": [], "source": [ "fig, axes = sampler.sectionplots(s=3)\n", "for ax in axes:\n", " solid.sectionplot_draw(ax, alpha=0.2, face_c='.5')" ] }, { "cell_type": "markdown", "id": "30c062aa-a8c5-4553-a61b-daffc6303f70", "metadata": {}, "source": [ "## Compare 3-hole and gridded spacings\n", "\n", "Calculate 3-hole spacing for each gridded dataset, using the commonly applied $\\sqrt2$ factor for scaling (consistent with 3-hole calculation on regular data above that yielded the `spacing` column)." ] }, { "cell_type": "code", "execution_count": null, "id": "1bf017ab-12f7-4606-b819-e81f5724dccd", "metadata": {}, "outputs": [], "source": [ "def calculate_3hole_spacing(factor=2**0.5):\n", " \"\"\"Defining in a function as will recalculate with a calibrated factor\"\"\"\n", " search = rmsp.Search(min_comps=3, max_comps=3, max_comps_per_dhid=1)\n", " cols = []\n", " for space in sampler.spacings:\n", " cols.append(f\"spacing_{int(space[0])}\")\n", " grid[cols[-1]] = rmsp.IDWEstimator(search).estimate(\n", " grid, sampler.get_data(space), \"spacing\", output=(\"mean_dist\")\n", " )[\"mean_dist\"] * factor\n", " return cols\n", "\n", "cols = calculate_3hole_spacing(factor=2**0.5)\n", "\n", "grid[['spacing'] + cols].head()" ] }, { "cell_type": "markdown", "id": "90b4b5dd-6b1f-4153-a134-3f264b7159e7", "metadata": {}, "source": [ "Compare the 3-hole spacing with the gridded spacing:" ] }, { "cell_type": "code", "execution_count": null, "id": "5d7c1b08-1365-47ce-b277-da6aded7bc6e", "metadata": {}, "outputs": [], "source": [ "def compare_3hole_with_grid_spacing():\n", " \"\"\"Defining in a function as we will re-call with altered globals\"\"\"\n", " fig, ax = plt.subplots(figsize=(6, 6))\n", " ax.plot(spacings, grid[cols].mean(),\n", " \"--ok\", label=\"grid vs spacing\")\n", " for rescat, space in spac_crit.items():\n", " ax.axhline(space, c=cmap_class[rescat], label=rescat)\n", " rmsp.format_plot(ax, \"Study Grid\", \"3-Hole Spacing\", grid=True,\n", " xlim=(0, 70), ylim=(0, 70))\n", " ax.legend(loc=2)\n", " \n", "compare_3hole_with_grid_spacing()" ] }, { "cell_type": "markdown", "id": "bdbbd007-2955-4ef8-983a-02005cd5a119", "metadata": {}, "source": [ "## Tune the 3-hole scaling factor\n", "\n", "Based on the results above, under the grid configurations provided, the 3-hole rule (using factor of $ \\sqrt{2} $) corresponds with 78% of the grid configuration (e.g. a 3-hole spacing of 15m is equivalent to a grid configuration of 20mx20m).\n", "\n", "We would need a factor closer to $ \\sqrt{2}/{0.78} = 1.81$: " ] }, { "cell_type": "code", "execution_count": null, "id": "86df2ffc-b0cc-4f25-a99e-feade8907e15", "metadata": {}, "outputs": [], "source": [ "_ = calculate_3hole_spacing(factor=1.81)" ] }, { "cell_type": "markdown", "id": "fb1e3b9c-b718-4ba6-89da-753c0e3cad7a", "metadata": {}, "source": [ "Re-compare:" ] }, { "cell_type": "code", "execution_count": null, "id": "9ae5b6fd-bb51-4423-aabc-eaa679c61861", "metadata": {}, "outputs": [], "source": [ "compare_3hole_with_grid_spacing()" ] }, { "cell_type": "markdown", "id": "4c3c464f-7de8-4b87-9623-1bc764e7b27e", "metadata": {}, "source": [ "Now that the relationship between the sample grid and drill hole spacing calculation is established, there are two choices:\n", "1. Adjust the 3-hole spacing calculation based on the new factor\n", "2. Adjust the grid spacings in the study to match the 3-hole spacing calculations (to achieve target spacings)\n", "\n", "The same principle can be extended to drill hole planning where a planned grid configuration should be tested based on the drill hole spacing calculation with adjustments in accordance with how the drill hole spacing has been calculated in the context of the classification criteria." ] } ], "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.10.8" }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": {}, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 5 }