Drillholes

Drillholes represent information obtained from exploration drilling. Drillhole information is typically stored in a drillhole database. The SDK provides an interface to drilling information via the Drillhole and DrillholeDatabase classes.

Important
  • Drillholes and drillhole databases are only available in a script when connected to Maptek GeologyCore. Attempting to access these classes when connected to another application will result in an error.

  • This documentation covers the drillhole types available in GeologyCore. They are not compatible with blast holes in BlastLogic.

Drillhole storage

Drillholes store their data in fields, which are organised into tables. The following table shows the built-in drillhole table types, and the built-in types of fields that each of these tables support.

Table Field Types Required? Description
Collar table Northing Yes The “North” component of the collar The location where drilling started. location
Easting Yes The “East” component of the collar The location where drilling started. location
Elevation No The “Elevation” component of the collar The location where drilling started. location
Azimuth No The bearing of the drill at the collar The location where drilling started. location
Dip No The angle of the drill at the collar The location where drilling started. location
Survey table Depth Yes The depth the azimuth and dip measurements were taken at
Azimuth Yes The azimuth of the hole at each depth
Dip Yes The dip of the hole at each depth
Geology table To depth Yes The end depth of each interval in the table
From depth Yes The start depth of each interval in the table
Thickness No The thickness of each interval in the table
Rock type No The type of rock in each interval
Horizon No Stratigraphy layer for the interval
Assay table To depth Yes The end depth of each interval in the table
  From depth Yes The start depth of each interval in the table
  Thickness No The thickness of each interval in the table
Downhole table To depth Yes The end depth of each interval in the table
  From depth Yes The start depth of each interval in the table
  Thickness No The thickness of each interval in the table
Quality table To depth Yes The end depth of each interval in the table
  From depth Yes The start depth of each interval in the table
  Thickness No The thickness of each interval in the table

As an example, consider the drillhole represented in the image below:

Note the following:

  • The intervals are coloured by the rock type from the geology table.

  • The left hand labels are the values from the “From depth” field.

  • The right hand labels are the values from the “To depth” field.

This drillhole is described using these two tables:

Geology Table
From depth To depth Rock type
0.0 m 1.2 m Soil
1.2 m 11.5 m Rock
11.5 m 13.65 m Ore
13.65 m 15.0 m Rock
Collar Table
Northing Easting Elevation
12.5 m 6.76 m 1.12 m

The other tables (Survey, Assay, Downhole and Quality) do not exist in this drillhole.

Note the following:

  • The first row of the geology table indicates that from a depth of 0.0 m to 1.2 m the rock type is “Soil”.

  • The second row of the geology table indicates that from a depth of 1.2 m to 11.5 m the rock type is “Rock”.

  • The third row of the geology table indicates that from a depth of 11.5 m to 13.65 m the rock type is “Ore”.

  • The fourth row of the geology table indicates that from a depth of 13.65 m to 15.0 m the rock type is “Rock”.

This method of representation applies to all drillholes. Adding additional tables can enable more complex representations. For instance, incorporating a Survey table allows the drillhole’s path to be adjusted, providing a more accurate depiction of the drill’s trajectory.

The tables and fields available for a drillhole are defined by the drillhole database. Each drillhole within the same database will have the same tables and fields, though the values stored will vary. Drillholes may have different numbers of rows in each table.

Note:  If a drillhole is not part of a database, the SDK will raise an error when attempting to open it.

Drillhole operations

The following examples demonstrate how to perform common operations on a drillhole.

Accessing the collar point

A Drillhole object provides two properties for accessing the collar location:

Drillhole.raw_collar

This is collar location as it appears in the drillhole database. It does not take into account any coordinate system set on the drillhole object. This property can be edited to update the collar point of the drillhole.

Drillhole.converted_collar

This is the collar location after being adjusted for the drillhole’s coordinate system. If the drillhole has a coordinate system applied, this will be the point given if you perform a point query in the application and click on the collar point. This property cannot be edited directly. Instead it will be updated when the database is saved after editing the raw_collar property.

The example script below asks the user to pick a drillhole and then outputs the two collar locations to the report window.

from mapteksdk.project import Project
from mapteksdk.geologycore import Drillhole
from mapteksdk.operations import object_pick, write_report, PickFailedError
import numpy as np


def report_collar_points(drillhole: Drillhole):
  """Writes a report containing the collar of the drillhole.


  Parameters
  ----------
  drillhole
    The drillhole to report the collar of.
  """
  raw_collar = drillhole.raw_collar
  converted_collar = drillhole.converted_collar


  if np.allclose(raw_collar, converted_collar):
    # If the raw and converted collar are the same, the drillhole has
    # no coordinate system.
    message = f"Collar: {drillhole.raw_collar}"
  else:
    # The drillhole has a coordinate system applied so report both collar
    # points.
    message = (f"Raw collar: {drillhole.raw_collar}\n"
      f"Converted collar: {drillhole.converted_collar}")
  write_report(
    f"Collar for: {drillhole.name}", message=message
  )


if __name__ == "__main__":
  with Project() as project:
    # This will loop until break is called (i.e. Until the user cancels
    # the pick operation).
    while True:
      try:
        oid = object_pick(label="Pick a drillhole to query the collar point of.")
      except PickFailedError:
        # The user cancelled the pick operation.
        break


      # Check that the picked object is a drillhole.
      if not oid.is_a(Drillhole):
        write_report("Warning", "The picked object was not a drillhole.")
        continue


      # Write the report.
      with project.read(oid) as drillhole:
        report_collar_points(drillhole)

Reading the values of a drillhole

This example demonstrates reading the values of the various built-in tables and fields, as well as values from custom fields and tables. In your own scripts you do not need to assign each field to a variable before use; the following is merely an example showing how to access each property.

Field Table Property Field Property
Collar easting collar_table = drillhole.collar_table collar_table.easting
Collar northing collar_table.northing
Collar elevation collar_table.elevation
Collar azimuth collar_table.azimuth
Collar dip collar_table.dip
Survey depth survey_table = drillhole.survey_table survey_table.depth
Survey dip survey_table.dip
Survey azimuth survey_table.azimuth
Geology to depth geology_table = drillhole.geology_table geology_table.to_depth
Geology from depth geology_table.from_depth
Geology thickness geology_table.thickness
Geology rock type geology_table.rock_type
Geology horizon geology_table.horizon
Assay to depth assay_table = drillhole.assay_table assay_table.to_depth
Assay from depth assay_table.from_depth
Assay thickness assay_table.thickness
Downhole to depth downhole_table = drillhole.downhole_table downhole_table.to_depth
Downhole from depth downhole_table.from_depth
Downhole thickness downhole_table.thickness
Quality to depth quality_table = drillhole.quality_table quality_table.to_depth
Quality from depth quality_table.from_depth
Quality thickness quality_table.thickness
Custom field in custom table
# Non-built-in tables must be accessed by name.
custom_table = \ drillhole.table_by_name("table_name")
# Custom fields must be accessed by name.
custom_field = \ custom_table.field_by_name("field_name")
Note
  • Attempting to access a table that does not exist in the drillhole will raise a TableNotFoundError.

  • Attempting to access a field that does not exist in a table will raise a FieldNotFoundError .

The following script iterates over the drillholes in a drillhole database and counts every rock type. When it is finished, it writes a report to the report window containing every rock type in the database and the count of intervals that had that rock type.

import collections
 
from mapteksdk.project import Project
from mapteksdk.geologycore import Drillhole, DrillholeDatabase
from mapteksdk.data import ObjectID
from mapteksdk.operations import object_pick, write_report
 
def count_rock_types(project: Project, database_id: ObjectID[DrillholeDatabase]
    ) -> collections.Counter:
  """Count the rock types in the drillhole database.
 
  Parameters
  ----------
  project
    The Project containing the drillhole database to summarise.
  database_id
    Object ID of the drillhole database to summarise.
 
  Returns
  -------
  collections.Counter
    A counter object containing the counts of every rock type in the
    drillhole database.
  """
  counts = collections.Counter()
  for drillhole_id in project.get_children(database_id).ids():
    if not drillhole_id.is_a(Drillhole):
      # Skip non-drillholes.
      continue
    with project.read(drillhole_id) as drillhole:
      drillhole: Drillhole
      geology_table = drillhole.geology_table
      lithology_values = geology_table.rock_type.values
      counts.update(lithology_values)
 
  return counts
 
if __name__ == "__main__":
  with Project() as project:
    picked_drillhole = object_pick(label="Pick database to summarise")
    target_database = picked_drillhole.parent
    counts = count_rock_types(project, target_database)
 
    # Generate a report which prints out the rock types one per line with their
    # counts.
    report_lines = [
      f"{rock_type} : {count}" for rock_type, count in counts.most_common()
    ]
 
    report_lines.insert(0, "Rock type : Interval count")
 
    # Write the report to the application and print it to standard output.
    print(*report_lines, sep="\n")
    write_report(
      f"Summary of: {target_database.path}",
      "\n".join(report_lines)
    )

Describing the displayed table of a drillhole

This example prompts the user to pick on a drillhole. The script adds a report to the report window that includes the minimum, maximum and mean of each numeric field in the displayed table of the drillhole.

from mapteksdk.project import Project
from mapteksdk.geologycore import Drillhole
from mapteksdk.operations import object_pick, write_report
 
if __name__ == "__main__":
  with Project() as project:
    drillhole_id = object_pick(label="Pick a drillhole to describe")
    if not drillhole_id.is_a(Drillhole):
      raise ValueError("The picked object was not a drillhole.")
 
    with project.read(drillhole_id) as drillhole:
      drillhole: Drillhole
      displayed_table = drillhole.displayed_table
      report_title = f"Summary of: {drillhole.name}"
      with displayed_table.dataframe() as frame:
        write_report(report_title, str(frame.describe()))

Setting the visualisation of the drillhole

The following example demonstrates how to set the field and colour map used to colour a drillhole in the viewer. Note that the script only colours the picked drillhole. It does not consider the entire drillhole database.

import numpy as np
 
from mapteksdk.project import Project
from mapteksdk.data import StringColourMap
from mapteksdk.geologycore import Drillhole
from mapteksdk.operations import object_pick
 
def generate_rock_type_map(project: Project, rock_types: np.ndarray
    ) -> StringColourMap:
  """Generates a colour map based on the given array.
 
  The colour map has every unique element in the rock_types array as a key.
  The colours vary from red to blue based on the order the unique values in
  rock_types are found.
 
  Parameters
  ----------
  project
    The Project to use to create the colour map.
  rock_types
    Array of strings. Each unique string is used as a key in the colour map.
 
  Returns
  -------
  StringColourMap
    The colour map object. It is closed when returned. It is also an orphan.
  """
  with project.new(None, StringColourMap) as colour_map:
    # Pass the result to np.array because colour_map.legend does not
    # support masked arrays.
    colour_map.legend = np.array(np.unique(rock_types))
    colour_count = len(colour_map.legend)
    # The colours vary from red to blue based on the order they ended up
    # in for the colour map.
    colour_map.colours = [
      [255 * (x / colour_count),
        0,
        255 - (255 * (x / colour_count)),
        255]
        for x in range(colour_count)
    ]
 
  return colour_map
 
if __name__ == "__main__":
  with Project() as project:
    picked_drillhole = object_pick(label="Pick drillhole to colour.")
    with project.edit(picked_drillhole) as drillhole:
      drillhole: Drillhole
      if not drillhole.id.is_a(Drillhole):
        raise TypeError("This script only supports drillholes")
 
      field_to_colour_by = drillhole.geology_table.rock_type
      colour_map = generate_rock_type_map(project, field_to_colour_by.values)
      drillhole.set_visualisation(field_to_colour_by, colour_map)

Setting the desurvey method

The DrillholeDatabase.desurvey_method property allows you to set or retrieve the method used to calculate the geometry of a drillhole based on the dip and plunge values found in the database’s Survey table.

The Python SDK supports the following desurvey methods:

DesurveyMethod.NONE

The survey information is ignored, resulting in a straight drillhole.

DesurveyMethod.SEGMENT_FOLLOWING

Each drillhole interval following a survey measurement is positioned using that measurement.

DesurveyMethod.SEGMENT_PRECEDING

Each drillhole interval preceding a survey measurement is positioned using that measurement.

DesurveyMethod.TANGENT

Each drillhole interval about a survey measurement is positioned using that measurement as a tangent.

DesurveyMethod.TANGENT_WITH_LENGTH

Similar to DesurveyMethod.TANGENT, but with the tangent length configurable via thetangent_length property in the database.

DesurveyMethod.UNDEFINED

Indicates an undefined desurvey method.

DesurveyMethod.UNKNOWN

Indicates that the desurvey method is not supported by the SDK.

The animation below shows the differences between the following desurvey methods:

In this example, the azimuth of each drillhole alternates between -90 and 90 degrees, with a consistent dip of -45 degrees.

Note

The following animation shows the same scenario but uses a drillhole database with more natural curves. Here, the DesurveyMethod.TANGENT method (the recommended method) performs significantly better:

Editing the values in a field

The values stored in a field can be edited by assigning new values to the DrillholeDatabaseField.values array.

The following example demonstrates converting all of the values in the GeologyTable.rock_type field to lowercase. This is useful for databases with inconsistent casing. For example, different drilling operators may input the same rock type using different cases (e.g. “Ore”, “ORE”, or “ore”). The application treats these as three distinct rock types rather than recognising them as the same. The script lowercases all of these (i.e. to “ore”), enabling the application to treat them as the same rock type.

import numpy as np
 
from mapteksdk.project import Project
from mapteksdk.geologycore import Drillhole
from mapteksdk.operations import object_pick
 
if __name__ == "__main__":
  with Project() as project:
    picked_drillhole = object_pick(label="Pick database to lowercase rock type")
    target_database = picked_drillhole.parent
    for drillhole_id in project.get_children(target_database).ids():
      if not drillhole_id.is_a(Drillhole):
        continue
 
      with project.edit(drillhole_id) as drillhole:
        drillhole: Drillhole
        geology_table = drillhole.geology_table
        rock_type = geology_table.rock_type
        rock_type.values = np.char.lower(rock_type.values)

Note:  Editing the values array only allows you to modify existing values. It does not support adding new values or removing existing ones. For these operations, see Adding new rows to the table and Removing rows from a table on this page.

Handling invalid values

The values property of a field is stored as a NumPy masked array. This allows specific elements of an array to be marked as invalid. For example, when an interval has no data for a field, it should be marked as “invalid” rather than storing a placeholder value like -99, which could be misleading.

The DrillholeDatabaseField.values property returns all values stored in the field, including those marked as invalid. Invalid values can be represented in various ways depending on the data type of the field:

  • Float fields: Invalid values are typically represented by NaN (Not a Number).

  • Integer fields: Invalid values are represented by a valid but unspecified value.

  • String fields: Invalid values are generally represented by an empty string.

To determine the validity of each value in the array, the field.values.mask property can be used. This property returns an array where each element indicates whether the corresponding value in field.values is valid:

  • If field.values.mask[i] is True, then field.values[i] is invalid.

  • If field.values.mask[i] is False, then field.values[i] is valid.

Fields that store Boolean values do not support invalid values.

The following code snippet illustrates these properties:

# The values stored in the field. Some may be invalid.
field.values
# The validity of the values. True indicates invalid, False indicates valid.
field.values.mask

Because field.values might contain invalid values, performing calculations directly on this array can produce incorrect results by including the invalid values. For instance, consider an assay table with the following fields and values:

To depth From depth Ore Ore.mask
0.00 1.05 NaN True
1.05 2.10 0.13 False
2.10 3.15 0.15 False
3.15 4.20 NaN True
4.20 5.25 0.14 False

In this example, the Ore column represents the field’s values, and the Ore.mask column shows which values are invalid (True indicates an invalid value). Invalid values remain stored in the field, but calculations should ignore them.

If you calculate the mean value of the Ore field directly, the result would be NaN because NaN combined with any other number results in NaN. Therefore, it is necessary to filter out the invalid values before performing calculations. This can be done manually, as shown in the following code snippet:

# NOTE: This fragment is incomplete and won’t run by itself.
ore_field = assay_table.field_by_name("Ore")
to_depth = assay_table.to_depth
from_depth = assay_table.from_depth
 
# Invert the mask to get an array where True indicates valid
# and False indicates invalid.
ore_validity = ~ore_field.values.mask
 
# This filters out every interval that does not have a valid ore value.
valid_to_depth = to_depth.values[ore_validity]
valid_from_depth = from_depth.values[ore_validity]
valid_ore = ore_field.values[ore_validity]
# Now do something useful with this information.
		

The script above filters the fields to retain only the valid intervals:

Valid from depth Valid to depth Valid ore
1.05 2.10 0.13
2.10 3.15 0.15
4.20 5.25 0.14

Calculating the mean on valid_ore would correctly return 0.14 because the NaN values have been filtered out.

A simpler alternative is to use the numpy.ma.mean function, which ignores invalid values in the inputs. The numpy.ma package contains many utility functions for handling calculations with invalid values.

Adding new rows to the table

New rows can be added to a table by calling the BaseDrillholeTable.add_row() or BaseDrillholeTable.add_rows() add_rows() methods.

The add_row() method adds a single new row to the table. By default the new row is appended to the end of the table. The optional index argument allows you to specify the position at which the new row should be inserted.

The following code snippet illustrates the use of add_row():

# NOTE: This fragment is incomplete and won’t run by itself.
# Add a row at the end of the table.
table.add_row()

# Add a row at the start of the table.
table.add_row(index=0)

# Add a row at index i in the table.
table.add_row(index=i)

Use add_rows() to add multiple new rows to the table at once. If adding more than one row, this is significantly more efficient than calling add_row() multiple times. Similar to add_row(), the default behaviour is to append new rows to the end of the table, while the index argument can be used to specify the position where the new rows should be inserted.

The following code snippet illustrates the use of add_rows():

# NOTE: This fragment is incomplete and won’t run by itself.
# Add 10 rows to the end of the table
table.add_rows(10) # Add 10 rows to the start of the table table.add_rows(10, index=0)
# Add 10 rows at index i in the table. table.add_rows(10, index=i)

Note:  The newly added rows are initially filled with invalid values. After adding rows to the table, it is the caller's responsibility to populate these rows with appropriate values. For a complete example of adding rows and setting their values, see Adding a new table to a database on this page.

Removing rows from a table

Rows can be removed from a table by calling the BaseDrillholeTable.remove_row() or BaseDrillholeTable.remove_rows() methods.

The remove_row() method removes the row at the specified index. The values that were stored in that row are deleted and cannot be recovered.

The remove_rows() method removes the specified number of rows starting at the specified index. The values that were stored in those rows are deleted and cannot be recovered.

# NOTE: This fragment is incomplete and won’t run by itself.
# Remove the ith row from the table.
table.remove_row(i)
 
# Remove three rows, starting from the ith row from the table.
table.remove_rows(i, 3)

The following example requests the user to pick a drillhole interval, after which the corresponding row in the table is deleted:

from mapteksdk.project import Project
from mapteksdk.geologycore import Drillhole
from mapteksdk.operations import (primitive_pick, SelectablePrimitiveType, PickFailedError)
 
if __name__ == "__main__":
  with Project() as project:
    while True:
      try:
        edge = primitive_pick(
          SelectablePrimitiveType.EDGE,
          label="Pick an interval of a drillhole to delete.")
        with project.edit(edge.path) as drillhole:
          drillhole: Drillhole
          if not isinstance(drillhole, Drillhole):
            raise ValueError("You must pick a drillhole.")
          displayed_table = drillhole.displayed_table
          displayed_table.remove_row(edge.index)
      except PickFailedError:
        break

Accessing the points and edges

Drillholes have specific properties for accessing points and edges, which are used to display the drillhole. These properties allow you to query and work with the points and edges associated with a drillhole.

The following script demonstrates how to query the points and edges of a drillhole and generate a report:

from mapteksdk.project import Project
from mapteksdk.geologycore import Drillhole
from mapteksdk.operations import object_pick, write_report
 
if __name__ == "__main__":
  with Project() as project:
    drillhole_id = object_pick(label="Pick a drillhole to query edges for")
 
    if not drillhole_id.is_a(Drillhole):
      raise ValueError("This script only supports drillholes.")
 
    with project.edit(drillhole_id) as drillhole:
      points = "\n".join(
        f"{x:.3f}, {y:.3f}, {z:.3f}" for x, y, z in drillhole.points)
      edges = "\n".join(f"{start}, {end}" for start, end in drillhole.edges)
      message = (f"Points (X, Y, Z):\n{points}\n"
                f"Edges (Start, end):\n{edges}")
      write_report(f"{drillhole.id.name}", message)
Note
  • The points and edges returned by these properties are based on the current drillhole visualisation. If the drillhole is edited they will not change until the drillhole is saved.

  • The points and edges of a drillhole database cannot be edited directly.

  • The points and edges are calculated based on the following information:

    • The collar point

    • The intervals in the displayed table

    • The values in the survey table

    • The desurvey method of the drillhole database

The following script demonstrates how to use edge picks to query and print values from a specific interval in a drillhole:

from mapteksdk.project import Project
from mapteksdk.geologycore import Drillhole
from mapteksdk.operations import (
  primitive_pick, write_report, SelectablePrimitiveType)
 
if __name__ == "__main__":
  with Project() as project:
    edge = primitive_pick(
      SelectablePrimitiveType.EDGE,
      label="Pick an interval in a drillhole to query.")
 
    with project.edit(edge.path) as drillhole:
      drillhole: Drillhole
      if not drillhole.id.is_a(Drillhole):
        raise ValueError("This script only supports drillholes.")
 
      field_description = {}
      displayed_table = drillhole.displayed_table
      for field in displayed_table.fields:
        field_description[field.name] = field.values[edge.index]
 
      write_report(
        f"Interval {edge.index} of {edge.path}",
        "\n".join(f"{key} : {value}" for key, value in field_description.items())
      )

Writing tables to a CSV file

This example demonstrates writing the contents of a drillhole database to a folder of CSV files, with each CSV file representing a table from the database. This method may be less efficient compared to the export tool in the GeologyCore application, but it provides a way to aggregate data from different drillholes into a single pandas dataframe.

import os
import pathlib
 
from mapteksdk.project import Project
from mapteksdk.geologycore import Drillhole, DrillholeDatabase
from mapteksdk.operations import object_pick
from mapteksdk.data import ObjectID
import pandas as pd
 
# Path to the directory containing the script.
SCRIPT_DIRECTORY = pathlib.Path(os.path.dirname(__file__))
 
def gather_table_dataframes(
    project: Project,
    database_id: ObjectID[DrillholeDatabase],
    table_name: str) -> pd.DataFrame:
  """Get a dataframe of the specified table for every drillhole.
 
  This iterates over every drillhole in the database and concatenates
  the dataframes of the specified table into a single dataframe.
 
  Parameters
  ----------
  project
    Project to use to open the drillholes.
  database_id
    Object ID of the drillhole database to read drillholes from.
  table_name
    Name of the table to concatenate the dataframes for.
 
  Returns
  -------
  pd.DataFrame
    The dataframes for the specified table for every drillhole in the
    specified database concatenated into a single dataframe.
 
  Warnings
  --------
  This loads all the values in the table for every drillhole which may be
  very slow or impossible for large drillhole databases (or drillhole
  databases containing a large number of text fields).
  """
  dataframes: list[pd.DataFrame] = []
  for drillhole_id in project.get_children(database_id).ids():
    if not drillhole_id.is_a(Drillhole):
      continue
 
    with project.read(drillhole_id) as drillhole:
      drillhole: Drillhole
      table = drillhole.table_by_name(table_name)
      with table.dataframe() as table_frame:
        dataframes.append(table_frame)
 
  return pd.concat(dataframes)
 
if __name__ == "__main__":
  with Project() as project:
    picked_drillhole = object_pick(label="Pick database to save to CSV.")
    target_database = picked_drillhole.parent
 
    # Make the output directory before generating the output files
    # to make sure it is valid.
    output_path = SCRIPT_DIRECTORY / "output" / target_database.name
    os.makedirs(output_path, exist_ok=True)
 
    with project.edit(target_database) as database:
      table_names = [table.name for table in database.tables]
 
    # This will iterate over every drillhole in the database for every
    # table. Though the dataframes for each table could be generated
    # simultaneously in a single loop that would limit this script to small
    # databases because it would require every table in every drillhole
    # to be loaded into memory at the same time.
    # This approach means only one table is loaded into memory at a time,
    # which allows for larger databases to be exported.
    for table_name in table_names:
      table_path = output_path / f"{table_name}.csv"
      table_frame = gather_table_dataframes(project, target_database, table_name)
      table_frame.to_csv(table_path, index=False)

Drillhole database operations

This section covers various operations available for drillhole databases, including accessing fields and tables, adding new data, and modifying existing structures.

Accessing fields and tables in a database

Both drillholes and drillhole databases consist of tables and fields. However, there are key differences in how these elements are managed:

  • Drillholes: The tables and fields within a drillhole contain values that can be read or edited. However, the structure of these tables and fields, as well as their properties, cannot be altered.

  • Drillhole Databases: These define the structure of the data stored within drillholes. Tables and fields in a database can be edited, and new tables or fields can be added. However, the tables and fields themselves do not contain values until they are populated by drillholes.

The following table summarises which operations are available on tables in drillholes and drillhole databases:

Operation Drillhole Database
Get table name Yes Yes
Get table type Yes Yes
Set table type No Yes (new tables only)
Get row count Yes No
Access fields Yes No
Add rows Yes No
Delete rows Yes No
Pandas dataframe Yes No
Add fields No Yes

The following table summarises the operations available on fields read from drillholes and drillhole databases.

Operation Drillhole Database
Read field name Yes Yes
Edit field name No Yes (new fields only)
Read field type Yes Yes
Set field type No Yes
Read field data type Yes Yes
Set field data type No Yes (new fields only)
Read field unit Yes Yes
Edit field unit No Yes
Get values Yes No
Set values Yes No

Adding a new drillhole to a drillhole database

Use the DrillholeDatabase.new_drillhole() method to add a new drillhole to a database. This method requires a single argument, the drillhole ID, which is a unique string identifier for the new hole within the database. It returns the ObjectID of the newly created drillhole, which represents the drillhole within the project.

Upon creation, the drillhole will have a single empty row in its collar table, and all other tables will be empty. To make the drillhole visible in the application, at a minimum, you need to provide values for the northing and easting fields in the collar table. To visualise the drillhole with intervals, you must at least one table with "to" and "from" depth fields and call Drillhole.set_visualisation() to apply a colour map.

Note:  You must close the drillhole database before opening the newly created drillhole, otherwise an OrphanDrillholeError will be raised. The error results because the drillhole has not been fully integrated into the database until the database is closed.

To add a new drillhole to a drillhole database, follow these basic steps:

  1. Open the database for editing.

  2. Call DrillholeDatabase.new_drillhole() on the database and pass the ID of the new drillhole.

  3. Close the database.

  4. Open the drillhole for editing.

  5. Set the collar point for the new drillhole.

  6. Populate the other tables for the drillhole.

This process is demonstrated in the following script:

from mapteksdk.project import Project
from mapteksdk.geologycore import DrillholeDatabase
 
if __name__ == "__main__":
  with Project() as project:
    database_path = "path/to/database"
    # To add a new drillhole to a database, first open the database for editing.
    with project.edit(database_path) as database:
      database: DrillholeDatabase
      # Use new drillhole to add the new drillhole.
      drillhole_id = database.new_drillhole("DRILLHOLE_ID")
 
    # Once the database is closed, open the drillhole for editing.
    with project.edit(drillhole_id) as drillhole:
      # Set the collar point of the new drillhole.
      drillhole.raw_collar = [0, 0, 0]
      # Now populate any other tables and fields the drillhole has data for.
      # Note that tables (aside from the collar table) initially contain
      # no rows so the first operation to perform on each table
      # is table.add_rows(number_of_rows_in_table).
Note

For a detailed example of creating a drillhole and populating all its fields, see Creating a new drillhole database on this page.

Adding a new field to an existing table

To add a new field to a table in a drillhole database, use the BaseTableInformation.add_field() method on the relevant table property (e.g. DrillholeDatabase.collar_table). This method requires the following parameters:

name The name of the new field
data_type

The type of data that will be stored in the field

description

A description of the field

Additionally, you can provide optional parameters:

field_type The semantic type of the field (i.e. the kind of data stored in the field)
unit

The unit of measurement for the data stored in the field (e.g. metres or degrees). Note that only distance or angle units are supported.

index

The index at which the field should be added. If not specified, the field will be appended to the end of the table.

The newly added field will be empty for all drillholes until you populate it by opening drillholes and writing values into that field.

Note:  Most field types do not support duplicates. Attempting to add a field with the same type as an existing field will raise a DuplicateFieldTypeError. However, fields of type “Rock type”, “Horizon”, and “None” can have duplicates and will not raise an error.

The following script demonstrates how to add a “Total depth” field to a drillhole database. This example handles potential errors and reports the status via the report window.

from mapteksdk.project import Project
from mapteksdk.geologycore import (
  DrillholeDatabase, DrillholeFieldType,
  DuplicateFieldTypeError, DuplicateFieldNameError)
from mapteksdk.operations import write_report, object_pick
from mapteksdk.data import ObjectID
 
def add_total_depth_to_database(
    project: Project, database_id: ObjectID[DrillholeDatabase]):
  """Adds a total depth field to the database.
 
  Parameters
  ----------
  project
    The Project which contains the drillhole database.
  database_id
    The object id of the database to add the total depth field to.
 
  Raises
  ------
  RuntimeError
    If the database already contains a maximum depth field.
  """
  with project.edit(database_id) as database:
    database: DrillholeDatabase
    if not database.id.is_a(DrillholeDatabase):
      raise ValueError(f"'{database.id.path}' is not a drillhole database.")
 
    collar_table = database.collar_table
 
    try:
      collar_table.add_field(
        "Max depth",
        float,
        "The maximum depth of the drillhole",
        field_type=DrillholeFieldType.TOTAL_DEPTH
      )
    except (DuplicateFieldTypeError, DuplicateFieldNameError):
      raise RuntimeError(
        f"'{database.id.path}' already contained a total depth field."
      )
 
REPORT_TITLE = "Add total depth"
 
if __name__ == "__main__":
  with Project() as project:
    picked_drillhole = object_pick(label="Pick database to add total depth to.")
    target_database = picked_drillhole.parent
    try:
      add_total_depth_to_database(project, target_database)
 
      write_report(
        REPORT_TITLE,
        f"Successfully added max depth to: {target_database.path}"
      )
    except Exception as error:
      write_report(REPORT_TITLE, str(error))
Note
  • This script only adds the “Total depth” field to the database. It does not populate the field with values for any drillholes. For a complete script that also populates the field, see Calculating values over a drillhole database on this page.

  • Errors are reported via the report window if it can connect to an application.

  • Different field types can be added by specifying the field_type parameter. This parameter is optional; if omitted, the new field will have no type.

Adding a new table to a database

To add a new table to a database, use the DrillholeDatabase.add_table() method. This method requires the following:

  • The name of the new table

  • The table type for the new table

Note
  • The new table is created in a valid and consistent state. The table will include all necessary fields corresponding to its table_type. For example, a newly created Survey table will have fields for depth, azimuth, and dip. The exception to this is for tables of type DrillholeTableType.OTHER. These tables are created without any fields, and at least one field must be added before they are considered valid.

  • The newly created table is immediately accessible via the properties of the drillhole database, allowing for additional fields to be added or existing fields to be edited if needed.

  • After saving the drillhole database, the new table will be available across all drillholes in the database. Initially, the table will not contain any rows.

Tip:  After adding a table, be sure to close the drillhole database before opening any drillholes.

The following example demonstrates how to add a Survey table to a drillhole database. This table records the angle of the drillholes.

from mapteksdk.project import Project
from mapteksdk.geologycore import (
  DrillholeDatabase, Drillhole, DuplicateTableTypeError, DrillholeTableType,
  FieldNotFoundError)
from mapteksdk.operations import object_pick
import numpy as np
 
def populate_survey_table(drillhole: Drillhole):
  """Populates the survey table for the drillhole.
 
  This adds rows to the survey table for each 10 units of depth.
  The dip goes from -82 degrees to -42 degrees.
  The azimuth varies from 151 degrees to 185 degrees.
 
  This function assumes that the dip and azimuth values are stored
  in radians.
 
  Parameters
  ----------
  drillhole
    The drillhole to set the survey table values for.
 
  Raises
  ------
  ValueError
    If the maximum depth of the drillhole couldn't be determined.
  """
  # Try to derive the maximum depth of the drillhole from:
  # 1: The max depth field in the collar table.
  # 2: The maximum to_depth value in the displayed table.
  try:
    max_depth = drillhole.collar_table.total_depth.values[0]
  except (FieldNotFoundError, IndexError):
    max_depth = np.max(drillhole.displayed_table.to_depth.values)
  except Exception as error:
    raise ValueError("Failed to derive total depth for drillhole") from error
 
  # This generates one row in the survey table for each 10 units.
  row_count = int(max_depth // 10)
  survey_table = drillhole.survey_table
  survey_table.add_rows(row_count)
 
  # The dip will go from -82 at the top of the hole to -42 degrees
  # at the bottom of the hole.
  start_dip = np.radians(-82)
  end_dip = np.radians(-42)
 
  # The azimuth will go from 151 degrees at the top of the hole to 185 degrees
  # at the bottom of the hole.
  start_azimuth = np.radians(151)
  end_azimuth = np.radians(185)
 
  # The depth goes from 0 to the maximum depth.
  survey_table.depth.values = np.linspace(
    start=0,
    stop=max_depth,
    num=row_count,
    dtype=float)
  # Populate the azimuth and dip fields using the above limits.
  survey_table.azimuth.values = np.linspace(
    start=start_azimuth,
    stop=end_azimuth,
    num=row_count,
    dtype=float)
  survey_table.dip.values = np.linspace(
    start=start_dip,
    stop=end_dip,
    num=row_count,
    dtype=float)
 
if __name__ == "__main__":
  with Project() as project:
    picked_drillhole = object_pick(label="Pick database to add total depth to.")
    target_database = picked_drillhole.parent
 
    # First add the survey table to the database.
    with project.edit(target_database) as database:
      database: DrillholeDatabase
      if not database.id.is_a(DrillholeDatabase):
        raise TypeError("Database path is not a database.")
 
      try:
        database.add_table("SURVEY", DrillholeTableType.SURVEY)
      except DuplicateTableTypeError as error:
        raise ValueError(
          "Cannot add a survey table to a database which already contains one."
        ) from error
 
    # Now fill out the values in the survey table for each drillhole.
    for drillhole_id in project.get_children(target_database).ids():
      if not drillhole_id.is_a(Drillhole):
        continue
      with project.edit(drillhole_id) as drillhole:
        drillhole: Drillhole
        populate_survey_table(drillhole)

The following animation shows the script in action on a small, randomly generated drillhole database:

Deleting tables from a drillhole database

To delete a table from a drillhole database, use the BaseTableInformation.delete() method on the table. The following script demonstrates how to delete the quality table from all selected drillhole databases.

Important:  The delete operation cannot be undone, so use it with caution.

from mapteksdk.project import Project
from mapteksdk.geologycore import DrillholeDatabase


def main(project: Project):
    database_ids = project.get_selected().where(DrillholeDatabase)
    if len(database_ids) == 0:
        raise ValueError("This script requires a drillhole database to be selected.")
    for database_id in database_ids:
        with project.edit(database_id) as database:
            quality_table = database.quality_table
            quality_table.delete()


if __name__ == "__main__":
    with Project() as main_project:
        main(main_project)

Editing the properties of an existing field

When a drillhole database is open for editing, you can update the following properties of an existing field:

description

The field's descriptive label

field_type The semantic type of the field
unit

The unit of measurement for the data stored in the field

The example below illustrates how to change a field's type from “Rock type” to “Horizon”. This operation can be useful for correcting a database where stratigraphy information was incorrectly imported as rock type information.

from mapteksdk.project import Project
from mapteksdk.geologycore import DrillholeDatabase, DrillholeFieldType
from mapteksdk.operations import object_pick
 
if __name__ == "__main__":
  with Project() as project:
    picked_drillhole = object_pick(label="Pick database to change rock type to horizon.")
    target_database = picked_drillhole.parent
 
    with project.edit(target_database) as database:
      if not database.id.is_a(DrillholeDatabase):
        raise ValueError("This script only supports drillhole databases.")
      database: DrillholeDatabase
      # Get the rock type field.
      geology_table = database.geology_table
      rock_type = geology_table.rock_type
 
      # Edit the description and field to be for a horizon field.
      rock_type.description = "Stratigraphy layer"
      rock_type.field_type = DrillholeFieldType.HORIZON

Important:  Editing the field type may put the table into an inconsistent state if it no longer contains all required fields. This will trigger an error when saving the database.

Note
  • The field type can only be changed to types that support the data type and unit of the existing data. For example, converting a “Rock type” field to a “To depth” field is not allowed because their data types (string and numeric) are incompatible.

  • The unit of measurement can only be changed to units supported by the field type. For example, setting the unit of a “To depth” field to radians is not allowed as it does not support angle units.

  • Non-numeric fields do not support units. Attempting to assign units to these fields will result in an error.

Creating a new drillhole database

You can create a new drillhole database using the Project.new() method. Below is a simple script to create a basic drillhole database.

from mapteksdk.project import Project
from mapteksdk.geologycore import DrillholeDatabase
 
if __name__ == "__main__":
  with Project() as project:
    with project.new("drillholes/example_basic_database", DrillholeDatabase):
      pass

This script creates an empty drillhole database with a default configuration. Initially, the database only includes a collar table, which contains the fields “Northing” and “"Easting”.

To build a more functional drillhole database, you need to add additional tables, fields, and drillholes. The following example creates a new drillhole database and adds a geology table and a single drillhole.

from mapteksdk.project import Project
from mapteksdk.geologycore import (
  DrillholeDatabase, DrillholeTableType, DrillholeFieldType)
 
if __name__ == "__main__":
  with Project() as project:
    with project.new("drillholes/example_basic_database", DrillholeDatabase
        ) as database:
 
      # Add a geology table to the database. This table starts with
      # to and from depth fields, so those fields do not need to be added
      # manually.
      database.add_table("Geology", DrillholeTableType.GEOLOGY)
 
      geology_table = database.geology_table
      geology_table.add_field(
        "Rock type", # The name of the field.
        str, # The type of data stored in the field.
        "The type of rock in the interval", # Description of the field.
        field_type=DrillholeFieldType.ROCK_TYPE # The type of the field.
      )
 
      # Now add a drillhole to the database.
      drillhole_id = database.new_drillhole("example-1")
 
    # Populate the drillhole.
    with project.edit(drillhole_id) as drillhole:
      # Assign the collar point of the drillhole to a northing of 12
      # and an easting of -11.4.
      drillhole.raw_collar = (12.0, -11.4)
 
      # Get a reference to the drillhole geology table.
      geology_table = drillhole.geology_table
 
      # Add five intervals to the drillhole.
      geology_table.add_rows(5)
 
      # Assign values to the from depth to define the intervals.
      geology_table.from_depth.values = [0.0, 1.1, 2.25, 3.22, 4.18]
 
      # This copies the second from depth value to the first to depth
      # value and so on for the entire drillhole.
      # Thus after this line the to depth values will be:
      # [1.1, 2.25, 3.22, 4.18, NaN]
      # (NaN is 'Not a Number' indicating an invalid value).
      # This ensures that there are no gaps in the intervals of the drillhole.
      geology_table.to_depth.values[:-1] = geology_table.from_depth.values[1:]
 
      # Assign the final to depth value.
      geology_table.to_depth.values[-1] = 5.22
 
      # Assign the rock types for each interval.
      geology_table.rock_type.values = [
        "DIRT",
        "ROCK",
        "ORE",
        "ROCK",
        "ORE"
      ]

The script creates a simple drillhole database that stores the collar location and rock type for each interval and adds a single drillhole. You can also include other tables such as assay, downhole, or quality tables to define intervals or additional data for the drillhole.

Tip:  The created drillhole appears as a grey cylinder in the application. See Setting the visualisation of the drillhole on this page for how to programmatically define a colour map. Alternatively, use the drillhole visualisation tool in GeologyCore to change the drillhole’s appearance.

Deleting a field

The Python SDK provides the ability to delete fields from a database by calling delete() on the field in the database. This is useful for removing irrelevant or incorrect fields from the database.

Tip:  This functionality is particularly useful for managing fields that were automatically created by the SDK. For instance, if a “From depth” and “To depth” field were added automatically during table creation, deleting one of these fields may be simpler than initially managing it.

The following example demonstrates how to delete the “From depth” field from a quality table. After this operation, the quality table will retain only the “To depth” field.

from mapteksdk.project import Project
from mapteksdk.geologycore import DrillholeDatabase, DrillholeTableType
 
if __name__ == "__main__":
  with Project() as project:
    with project.new("drillholes/only_to_depth", DrillholeDatabase) as database:
      # When the Quality table is created, it contains both a "To depth"
      # and a "From depth" field.
      database.add_table("Quality", DrillholeTableType.QUALITY)
      quality_table = database.quality_table
 
      # Calling delete on the from depth field will delete it.
      quality_table.from_depth.delete()
 
      quality_table.add_field("ORE_PURITY", float, "The purity of the ore")
Important
  • Deleting a field will permanently remove all associated values from that field across all drillholes in the database. This operation cannot be undone.

  • Deleting a field may result in the table being in an invalid state if it no longer contains all required fields. This issue will only be detected when changes are saved.

Handling curved drillholes with the Survey table

In reality, drillholes often exhibit curvature due to factors such as the angle of the drill, the steadiness of the drilling equipment, and the hardness of the material being drilled. This curvature is represented in the Survey table, which includes the following built-in fields:

  • Depth: The depth at which each measurement was taken

  • Dip: The dip measured at each depth

  • Azimuth: The azimuth measured at each depth

These fields, along with the desurvey information of the drillhole database, are used to determine the points and edges for visualising the drillhole.

The following example demonstrates how to create a drillhole with a curved shape similar to a candy cane:

import numpy as np
 
from mapteksdk.project import Project
from mapteksdk.data import StringColourMap, ObjectID
from mapteksdk.data.units import DistanceUnit, AngleUnit
from mapteksdk.geologycore import (
  DrillholeDatabase, DrillholeFieldType, DrillholeTableType)
from mapteksdk.operations import open_new_view
 
def generate_database(
    project: Project,
    path: str,
    distance_unit: DistanceUnit,
    angle_unit: AngleUnit) -> ObjectID[DrillholeDatabase]:
  """Generates a drillhole database with fields in non-standard units.
 
  Parameters
  ----------
  project
    The project to create the database in.
  path
    The path to give the newly created database.
  distance_unit
    The unit for distance fields.
  angle_unit
    The unit for angle fields.
 
  Returns
  -------
  ObjectID[DrillholeDatabase]
    The object id of the newly created database.
  """
  with project.new(path, DrillholeDatabase, overwrite=True) as database:
    collar_table = database.collar_table
 
    # Set the northing and easting field to use the specified unit.
    collar_table.northing.unit = distance_unit
    collar_table.easting.unit = distance_unit
 
    # Add an elevation field.
    collar_table.add_field(
      "Elevation",
      float,
      "Elevation above sea level",
      field_type=DrillholeFieldType.ELEVATION,
      unit=distance_unit,
      )
 
    # Add a survey table and set the units.
    database.add_table("Survey", DrillholeTableType.SURVEY)
    survey_table = database.survey_table
    survey_table.azimuth.unit = angle_unit
    survey_table.depth.unit = distance_unit
    survey_table.dip.unit = angle_unit
 
    # Add a geology table and set the units.
    database.add_table("Geology", DrillholeTableType.GEOLOGY)
    geology_table = database.geology_table
    geology_table.to_depth.unit = distance_unit
    geology_table.from_depth.unit = distance_unit
 
    # The rock type field stores strings so doesn't allow units.
    geology_table.add_field(
      "Rock type",
      str,
      "The type of rock",
      field_type=DrillholeFieldType.ROCK_TYPE)
  return database.id
 
def generate_drillhole(
    project: Project,
    database_id: ObjectID[DrillholeDatabase],
    drillhole_name: str,
    collar_point: list):
  """Generates a drillhole which looks like a candy cane.
 
  Parameters
  ----------
  project
    The project containing the drillhole database.
  database_id
    The object id of the drillhole database to create the drillhole in.
  drillhole_name
    The name to give the newly created drillhole.
  collar_point
    The collar point for the newly created drillhole.
  """
  # Total rows in the drillhole.
  total_rows = 40
 
  # The number of stripes to give the candy cane.
  stripe_count = 20
 
  # Number of rows used for the curve of the candy cane.
  curve_rows = 20
 
  # Start and end dip values used to generate the curve of the
  # candy cane.
  start_dip = 90
  end_dip = -90
 
  # Start and end azimuth values used to generate the curve of the
  # candy candy.
  start_azimuth = 15
  end_azimuth = 30
 
  # Total depth for the drillhole.
  total_depth = 10
 
  with project.edit(database_id) as database:
    drillhole_id = database.new_drillhole(drillhole_name)
 
  with project.edit(drillhole_id) as drillhole:
    # Set the collar point.
    drillhole.raw_collar = collar_point
 
    survey_table = drillhole.survey_table
 
    if (survey_table.azimuth.unit is not AngleUnit.DEGREES
        or survey_table.dip.unit is not AngleUnit.DEGREES):
      raise ValueError(
        "This script only supports databases which store azimuth and dip "
        "in degrees.")
 
    survey_table.add_rows(total_rows)
 
    # The depth values are equally spaced down the drillhole.
    survey_table.depth.values = np.linspace(0, total_depth, total_rows)
 
    # The first curve_rows rows form the curve of the candy cane.
    survey_table.azimuth.values[:curve_rows] = np.linspace(
      start_azimuth, end_azimuth, curve_rows)
    survey_table.dip.values[:curve_rows] = np.linspace(
      start_dip, end_dip, curve_rows)
 
    # The remaining rows use the same azimuth and dip values to form the
    # straight part of the candy cane.
    survey_table.azimuth.values[curve_rows:] = end_azimuth
    survey_table.dip.values[curve_rows:] = end_dip
 
    geology_table = drillhole.geology_table
    geology_table.add_rows(stripe_count)
 
    # The from depth values start at 0 and reach the to depth.
    # stripe_count + 1 causes this to generate one more interval than
    # needed. The [:-1] then discards this interval.
    # This ensures that the final from depth value is one interval length before
    # the total depth of the drillhole.
    geology_table.from_depth.values = np.linspace(
      0, total_depth, stripe_count + 1)[:-1]
 
    # Set the to depth values to exactly match up with the from depth values,
    # except for the last row. This ensures no overlapping intervals.
    geology_table.to_depth.values[:-1] = geology_table.from_depth.values[1:]
    # The final to depth is the total depth of the drillhole.
    geology_table.to_depth.values[-1] = total_depth
 
    # Set the rock type for every even interval to "R".
    geology_table.rock_type.values[::2] = "R"
    # Set the rock type for every odd interval to "W".
    geology_table.rock_type.values[1::2] = "W"
 
    # Create a string colour map to use for the rock type.
    # "R" is coloured red.
    # "W" is coloured white.
    with project.new(None, StringColourMap) as colour_map:
      colour_map.legend = ["R", "W"]
      colour_map.colours = [[255, 0, 0, 255], [255, 255, 255, 255]]
 
    drillhole.set_visualisation(geology_table.rock_type, colour_map)
 
if __name__ == "__main__":
  with Project() as project:
    database_id = generate_database(
      project,
      "drillholes/candy_cane_db",
      DistanceUnit.METRE,
      AngleUnit.DEGREES)
 
    collar_point = [0, 0, 0]
 
    # Calling this function with multiple different names and collar points
    # allows for creating multiple drillholes in the database.
    drillhole_id = generate_drillhole(
      project,
      database_id,
      "Candy_cane",
      collar_point)
 
    open_new_view(database_id)
Note
  • Depth is typically stored in meters. Dip and Azimuth default to radians but can be set to degrees as needed.

  • Depth values in the survey table do not necessarily align with the “From depth” and “To depth” values in other tables.

ISIS and CSV-backed databases

The Python SDK supports working with drillhole databases backed by ISIS and CSV formats. However, there are some limitations and specific requirements, as follows:

  • Database Import

    ISIS and CSV databases must be imported into GeologyCore to ensure they have a path in the project. This import process allows the database to be opened using the Project class.

  • Supported Operations

    Most operations available for internal drillhole databases are also supported for ISIS and CSV databases.

  • Unsupported Operations

    The following operations are not supported for Isis and CSV databases:

    • Adding new tables to the database

    • Adding new fields to existing tables

    • Editing the description, type, and unit of existing fields

    • Adding new tables to the database.

Useful examples

The following scripts combine concepts introduced above into more practical examples.

Copying a drillhole

This example requests the user to pick a drillhole, and then pick a point to create a copy of the picked drillhole. It demonstrates iterating over the tables and fields of the drillhole to copy the information across. The collar of the copied drillhole is placed at the point picked by the user, as demonstrated in the following animation:

from __future__ import annotations
 
from mapteksdk.project import Project
from mapteksdk.data import ObjectID
from mapteksdk.geologycore import (
  Drillhole, DrillholeDatabase, DrillholeTableType, TableNotFoundError)
from mapteksdk.operations import (
  object_pick, coordinate_pick, PickFailedError, active_view)
 
def copy_drillhole(
    project: Project,
    source_id: ObjectID[Drillhole],
    new_collar: list[float]) -> ObjectID[Drillhole]:
  """Create a copy of the drillhole with the specified collar point.
 
  The copy is in the same database as the source drillhole and its tables
  contain all the same values, except for the collar point which is
  set to new_collar.
 
  Parameters
  ----------
  project
    Project to use to open the drillhole.
  source_id
    Object id of the drillhole to copy.
  new_collar
    Array-like of floats to set as the collar point for the new drillhole.
 
  Returns
  -------
  ObjectID[Drillhole]
    The object id of the copied drillhole.
  """
  original_name = source_id.name
  database_id: ObjectID[DrillholeDatabase] = oid.parent
 
  # Create the new drillhole.
  with project.edit(database_id) as database:
    new_drillhole_id = database.new_drillhole(f"{original_name}_copy")
 
  with project.read(oid) as drillhole:
    with project.edit(new_drillhole_id) as new_drillhole:
      drillhole: Drillhole
      # Copy the values from the source drillhole to the copy drillhole.
      for table in drillhole.tables:
        # Skip tables with no rows.
        if table.row_count == 0:
          continue
        new_table = new_drillhole.table_by_name(table.name)
        # Rows need to be added to each table except for the collar table.
        if new_table.table_type is not DrillholeTableType.COLLAR:
          new_table.add_rows(table.row_count)
        for field in table.fields:
          new_field = new_table.field_by_name(field.name)
          new_field.values = field.values
 
      # Set the collar point to be the given collar point.
      new_drillhole.raw_collar = new_collar
 
      # Attempt to set the displayed field.
      try:
        displayed_field_name = drillhole.displayed_field.name
        displayed_table_name = drillhole.displayed_table.name
        new_displayed_table = new_drillhole.table_by_name(displayed_table_name)
        new_displayed_field = new_displayed_table.field_by_name(displayed_field_name)
        new_drillhole.set_visualisation(new_displayed_field, drillhole.get_colour_map())
      except TableNotFoundError:
        # There was no displayed table.
        pass
 
    return new_drillhole_id
 
with Project() as project:
  try:
    while True:
      oid = object_pick(label="Pick a drillhole to copy")
      if not oid.is_a(Drillhole):
        continue
      new_collar = coordinate_pick(label="Pick collar for copy of drillhole")
 
      copy_id = copy_drillhole(project, oid, new_collar)
 
      # Add the newly created drillhole the currently active view.
      view = active_view()
      view.add_objects([copy_id])
  except PickFailedError:
    pass

Calculating values over a drillhole database

This script adds a total depth field to a drillhole database, iterates over every drillhole in the database, and calculates the deepest depth measurement recorded. The result is displayed in the report window.

from mapteksdk.project import Project
from mapteksdk.geologycore import (
  DrillholeDatabase, BaseDrillholeTable, DrillholeFieldType, DrillholeTableType,
  DuplicateFieldTypeError, DuplicateFieldNameError, Drillhole)
from mapteksdk.operations import write_report, object_pick
from mapteksdk.data import ObjectID
 
def add_total_depth_to_database(
    project: Project, database_id: ObjectID[DrillholeDatabase]):
  """Adds a total depth field to the database.
 
  Parameters
  ----------
  project
    The Project which contains the drillhole database.
  database_id
    The object id of the database to add the total depth field to.
 
  Raises
  ------
  RuntimeError
    If the database already contains a maximum depth field.
  """
  with project.edit(database_id) as database:
    database: DrillholeDatabase
    if not database.id.is_a(DrillholeDatabase):
      raise ValueError(f"'{database.id.path}' is not a drillhole database.")
 
    collar_table = database.collar_table
 
    try:
      collar_table.add_field(
        "Max depth",
        float,
        "The maximum depth of the drillhole",
        field_type=DrillholeFieldType.TOTAL_DEPTH
      )
    except (DuplicateFieldTypeError, DuplicateFieldNameError):
      raise RuntimeError(
        f"'{database.id.path}' already contained a total depth field."
      )
 
def populate_total_depth(
    project: Project, database_id: ObjectID[DrillholeDatabase]):
  """Populates the total depth field of the drillholes in the database.
 
  Parameters
  ----------
  project
    The Project which contains the drillhole database.
  database_id
    Object ID of the drillhole database containing the drillholes to populate
    the total depth field for.
  """
  for drillhole_id in project.get_children(database_id).ids():
    # Skip non-drillholes.
    if not drillhole_id.is_a(Drillhole):
      continue
 
    # Set the maximum depth for each drillhole.
    with project.edit(drillhole_id) as drillhole:
      drillhole: Drillhole
      max_depth = get_drillhole_total_depth(drillhole)
      collar_table = drillhole.collar_table
      collar_table.total_depth.values = max_depth
 
def get_drillhole_total_depth(drillhole: Drillhole) -> float:
  """Calculate the total depth of a drillhole based on its fields.
 
  This returns the maximum depth value read from any field in the drillhole's
  assay, downhole, geology, quality or survey tables.
 
  Parameters
  ----------
  drillhole: Drillhole
    The drillhole to calculate the total depth of.
 
  Returns
  -------
  float
    The total depth of the drillhole.
  """
  tables_with_depths = (
        DrillholeTableType.ASSAY,
        DrillholeTableType.DOWNHOLE,
        DrillholeTableType.GEOLOGY,
        DrillholeTableType.QUALITY,
        DrillholeTableType.SURVEY
      )
 
  # Generate a list of maximum depths for each table with the specific
  # types.
  max_depths = []
  for table_type in tables_with_depths:
    # Iterating over tables_by_type allows this script to work correctly
    # on databases with multiple tables with the same type.
    for table in drillhole.tables_by_type(table_type):
      max_depths.append(get_drillhole_table_total_depth(table))
  # Return the maximum max depth from any table.
  if max_depths:
    return max(max_depths)
  else:
    # The database had no tables which the max depth could be read from.
    return 0
 
def get_drillhole_table_total_depth(table: BaseDrillholeTable) -> float:
  """Get the maximum depth value in any field in a table.
 
  This returns the greatest depth value in the to depth, from depth or
  depth field in the given table.
 
  Parameters
  ----------
  table
    The table to calculate the maximum depth of. This can be any table which
    has to depth, from depth or depth fields.
 
  Returns
  -------
  float
    The deepest to depth, from depth or depth value found in the table.
  """
  field_types = (
    DrillholeFieldType.TO_DEPTH,
    DrillholeFieldType.FROM_DEPTH,
    DrillholeFieldType.DEPTH,
  )
 
  # Generate a list of max depths in each relevant field in the table.
  max_depths = []
  for field_type in field_types:
    try:
      field = table.fields_by_type(field_type)[0]
    except IndexError:
      # An IndexError indicates the table doesn't have this field. Move
      # onto the next one.
      continue
    max_depths.append(max(field.values))
 
  # Return the maximum depth.
  return max(max_depths)
 
REPORT_TITLE = "Add total depth"
 
if __name__ == "__main__":
  with Project() as project:
    picked_drillhole = object_pick(label="Pick database to add total depth to.")
    target_database = picked_drillhole.parent
    try:
      add_total_depth_to_database(project, target_database)
      populate_total_depth(project, target_database)
 
      write_report(
        REPORT_TITLE,
        f"Successfully added max depth to: {target_database.path}"
      )
    except Exception as error:
      write_report(REPORT_TITLE, str(error))

Querying a point at depth

Consider the following drillhole database containing a single drillhole:

Tip:  Download the drillhole database using this link: curved-drillhole-example.maptekobj.

The naive approach for getting the midpoint of each interval would be to take the start and end point of each interval, add them together, and then divide by two. This is demonstrated in the following code snippet:

mid_points = [
        (target_drillhole.points[a] + drillhole.points[b]) / 2
        for a, b in drillhole.edges
    ]
		

None of the mid-points generated by the naive approach are located on the drillhole, as seen in the image below.

The cause of this problem is easier to see if the edges of drillhole are extracted:

The naive approach calculated the mid-points as if each interval of the drillhole was a straight line from the start point to the end point. The script ignored that the drillhole curves in each interval. To accurately generate mid-points while accounting for these curves, you can use the Drillhole.point_at_depth() method. This method accepts a depth along the drillhole and returns the point at that depth, taking into account the curves of the interval. This allows for generating accurate midpoints for intervals in a drillhole, as shown in the following animation:

The animation demonstrates the result of running the following script that uses Drillhole.point_at_depth() :

from mapteksdk.project import Project
from mapteksdk.geologycore import Drillhole
from mapteksdk.data import PointSet
from mapteksdk.operations import object_pick, active_view
import numpy as np

def extract_mid_points(target_drillhole: Drillhole) -> np.ndarray:
    """Extracts the mid points of the displayed field of the drillhole.

    Parameters
    ----------
    target_drillhole
    Drillhole to extract the mid points for.

    Returns
    -------
    numpy.ndarray
    Numpy array containing the midpoints.

    Raises
    ------
    TableNotFoundError
    If the drillhole has no displayed table.
    """
    table = target_drillhole.displayed_table
    mid_depths = (table.to_depth.values + table.from_depth.values) / 2

    mid_points = np.empty((table.row_count, 3), np.float64)

    mid_points[:] = [
        target_drillhole.point_at_depth(x) + target_drillhole.converted_collar
        for x in mid_depths
    ]
    return mid_points

if __name__ == "__main__":
    with Project() as project:
        drillhole_id = object_pick(
            object_types=(Drillhole,),
            label="Pick a drillhole to extract mid-points from.")

        with project.read(drillhole_id) as drillhole:
            extracted_points = extract_mid_points(drillhole)
            displayed_field = drillhole.displayed_field
            displayed_field_values = displayed_field.values
            displayed_field_name = displayed_field.name
            colour_map_id = drillhole.get_colour_map()

        with project.new(
                f"cad/extracted_mid_points/{drillhole_id.name}", PointSet) as point_set:
            point_set.points = extracted_points
            point_set.point_attributes[displayed_field_name] = displayed_field_values
            point_set.point_attributes.set_colour_map(
            displayed_field_name, colour_map_id)

        view = active_view()
        view.add_object(point_set)
        project.set_selected([point_set])

Editing a pandas dataframe

By default, any changes to the DataFrame object returned by table.dataframe() are not propagated back to the drillhole. To ensure that modifications are propagated, set the save_changes flag to True. This setting ensures that any updates to the columns are saved back to the drillhole when the with block ends. This feature is particularly useful for performing batch operations on the entire table.

Note
  • Fields cannot be added or removed from the drillhole through a pandas dataframe. Dropping a column in the dataframe does not affect the values of the column in the drillhole.

  • Rows can be added by appending rows to the dataframe.

  • Rows can be removed by removing rows from the dataframe.

  • Adding or removing rows (or rearranging rows) via pandas DataFrame objects may corrupt the data in columns that are not included in the dataframe.

Sorting by From depth

Sorting the intervals in a drillhole helps identify issues such as overlapping intervals more easily. The simplest method to sort a table by the “From depth” field, or any other field, is to convert it to a pandas dataframe and use the sort_values() method with inplace=True. The following example demonstrates how to pick a drillhole, sort its geology table by "FROM_DEPTH", and remove any duplicate rows:

from mapteksdk.project import Project
    from mapteksdk.geologycore import Drillhole
    from mapteksdk.operations import object_pick

    if __name__ == "__main__":
        with Project() as project:
            oid = object_pick(
                object_types=Drillhole,
                label="Pick a drillhole to sort and drop duplicates.")

            with project.edit(oid) as drillhole:
                geology_table = drillhole.geology_table
                with geology_table.dataframe(save_changes=True, include_hole_id=False
                        ) as frame:
                    frame.sort_values(by="FROM_DEPTH", inplace=True)
                    frame.drop_duplicates(inplace=True)

Sorting an entire database by From depth

The previous script only affects a single table within a single drillhole. To extend this functionality to every table in every drillhole within a database, you can iterate over both drillholes and tables.

Important:  Sorting every table in every drillhole may take considerable time for large databases.

from mapteksdk.project import Project
from mapteksdk.geologycore import Drillhole
from mapteksdk.operations import object_pick

if __name__ == "__main__":
    with Project() as project:
        oid = object_pick(
            object_types=Drillhole,
            label="Pick a drillhole in the database to sort and drop duplicates.")

        database_id = oid.parent

        for drillhole_id in project.get_children(database_id).ids():
            # Skip non-drillholes in the database.
            if not drillhole_id.is_a(Drillhole):
                continue
            with project.edit(drillhole_id, Drillhole) as drillhole:
                for table in drillhole.tables:
                    try:
                        # Get the name of the from depth field.
                        from_depth_name = table.from_depth.name
                    except AttributeError:
                        # Skip tables without a from depth field.
                        continue

                    with table.dataframe(save_changes=True, include_hole_id=False
                            ) as frame:
                        frame.sort_values(by=from_depth_name, inplace=True)
                        frame.drop_duplicates(inplace=True)

Extracting points and edges from a drillhole

This example demonstrates how to extract the points and edges of a drillhole into an edge network.

Tip:  Download the drillhole database using this link: curved-drillhole-example.maptekobj.

from mapteksdk.project import Project
from mapteksdk.data import ObjectID, EdgeNetwork, StringColourMap
from mapteksdk.geologycore import Drillhole
from mapteksdk.operations import object_pick, active_view

def extract_drillhole_edges(
    project: Project,
    drillhole_id: ObjectID[Drillhole]) -> ObjectID[EdgeNetwork]:
    """Extracts the edges from a drillhole.

    This copies the points and edges from the drillhole. If the drillhole
    is coloured using a string colour map, the colour of the drillhole is
    also copied to the edge network.

    This ignores the desurvey information of the drillhole. The points are
    joined via straight edges even if the drillhole is curved.

    Parameters
    ----------
    project
        Project containing the drillhole.
    drillhole_id
        ObjectID of the drillhole to use to create an EdgeNetwork.

    Returns
    -------
    ObjectID[EdgeNetwork]
        Object ID of the created edge network. Its path will be the path to
        the drillhole with "_edges" appended to it.
    """
    with project.read(drillhole_id) as drillhole:
        with project.new(drillhole_id.path + "_edges", EdgeNetwork) as network:
            network.points = drillhole.points
            network.edges = drillhole.edges

            colour_map = drillhole.get_colour_map()
            # colour_map.get(value) isn't implemented for NumericColourMaps, so
            # this only works for StringColourMaps.
            if colour_map and colour_map.is_a(StringColourMap):
                displayed_field = drillhole.displayed_field
                with project.read(drillhole.get_colour_map(), StringColourMap
                        ) as colour_map:
                    # The number of edges in a drillhole is equal to the number
                    # of rows in the displayed table, so this should always construct
                    # an appropriately sized list.
                    network.edge_colours = [
                        colour_map.get(value) for value in displayed_field.values
                    ]
    return network.id

def main():
    with Project() as project:
        drillhole_id = object_pick(
            object_types=(Drillhole,),
            label="Pick a drillhole to extract edges from"
        )
        network = extract_drillhole_edges(project, drillhole_id)
        view = active_view()
        view.add_object(network)

if __name__ == "__main__":
    main()

The following animation shows the script being run on a procedurally generated, highly curved drillhole:

Note
  • The script copies the points and edges from the drillhole, connecting the edges of each displayed interval. This approach does not take the curvature of the drillhole along each interval into account. For a method that considers curvature when extracting points, see Querying a point at depth on this page.

  • If the drillhole were entirely straight (i.e. using a desurvey method of "None"), the extracted edges would precisely match the edges of the drillhole.

  • The extracted edge network contains a copy of the points, edges and colours of the drillhole. It will not update if the drillhole is changed.