Example notebook

[1]:
from datetime import date, datetime

from cognite.client import CogniteClient
from cognite.client.data_classes import Sequence

from cognite.well_model import CogniteWellsClient
from cognite.well_model.models import *

Authentication

The well-service is running inside CDF, so we can authenticate the same way as with the Cognite SDK.

[2]:
PROJECT_NAME = "react-demo-app-bluefield"
COGNITE_BASE_URL = "https://bluefield.cognitedata.com"

client = CogniteClient(
    client_name="WDL example notebook",
    base_url=COGNITE_BASE_URL,
    project=PROJECT_NAME,
    disable_pypi_version_check=True,
)
wells_client = CogniteWellsClient(
    client_name="WDL example notebook",
    base_url=COGNITE_BASE_URL,
    project=PROJECT_NAME,
)

The data model

The figure below shows how data is structured in the well data layer.

data-model.png

In this example, we have two sources: EDM and Petrel. In the Petrel source, we have ingested a well, a wellbore, casings, trajectories, and measurements. In the EDM source, we have ingested a well, a wellbore, NPT, and NDS events.

One of the main goals of the well-data-layer, is to be able to match data from different sources. In the figure above, you can see that the wells and wellbores from the two different sources are matched and combined.

Matching

With matching we mean the process of identifying that two or more wells or wellbores refer to the same object. The well data layer doesn’t do any automatic matching on its own. This is deemed too complex, and Cognite already has a contextualization service that should be used for this.

Instead, you may send in a matching_id when ingesting wells and wellbores. The well-data-layer will look for other wells with the same matching id, and if it is the same, it will match them together. If the matching_id is new, it will create a new well or wellbore. Don’t worry if this is confusing right now, we’ll see it in use further down.

Merging

When two or more wells or wellbores are matched, the well-data-layer will use a set of merge rules to determine what the matched well or wellbore should look like. We refer to this process of combining two or more wells/wellbores as merging.

Ingestion

Ingesting sources

Lets start by ingesting some sources:

[3]:
wells_client.sources.ingest(
    [
        Source(name="EDM", description="Engineers Data Model"),
        Source(name="Petrel", description="Petrel Studio"),
    ]
)
[3]:
name description
0 EDM Engineers Data Model
1 Petrel Petrel Studio
[4]:
# We can see all sources that have been registered like this:
wells_client.sources.list()
[4]:
name description
0 EDM Engineers Data Model
1 Petrel Petrel Studio

Merge rules

Next, we are going to define some merge rules. The merge rules allows us to configure what the matched well and wellbore should look like.

[5]:
wells_client.wells.merge_rules.set(
    rules=WellMergeRules(
        name=["EDM", "Petrel"],
        description=["EDM", "Petrel"],
        # The country should only be set to the Petrel value.
        country=["Petrel"],
        quadrant=["EDM", "Petrel"],
        # The source list for blocks is empty, so `block` will never be set.
        block=[],
        field=["EDM", "Petrel"],
        operator=["EDM", "Petrel"],
        spud_date=["EDM", "Petrel"],
        license=["EDM", "Petrel"],
        well_type=["EDM", "Petrel"],
        water_depth=["EDM", "Petrel"],
        wellhead=["EDM", "Petrel"],
        region=["EDM", "Petrel"],
    )
)

wells_client.wellbores.merge_rules.set(
    rules=WellboreMergeRules(
        name=["EDM", "Petrel"],
        description=["EDM", "Petrel"],
        datum=["EDM", "Petrel"],
        # Wellbore parents should be read only from EDM
        parents=["EDM"],
        well_tops=["EDM", "Petrel"],
    )
)

# If we want to apply the same rules for all fields, the above code can be written as
wells_client.wells.merge_rules.set(["EDM", "Petrel"])
wells_client.wellbores.merge_rules.set(["EDM", "Petrel"])
[5]:
value
name ["EDM", "Petrel"]
description ["EDM", "Petrel"]
datum ["EDM", "Petrel"]
parents ["EDM", "Petrel"]
wellTops ["EDM", "Petrel"]
holeSections ["EDM", "Petrel"]
trajectories ["EDM", "Petrel"]
casings ["EDM", "Petrel"]

Wells and wellbores

Now that we have ingested some sources and set the merge rules, we can start ingesting wells and wellbores

Ingesting wells

[6]:
edm_well = WellIngestion(
    matching_id="well-1-matching-id",
    name="well-1",
    description="My new Well",
    country="Norway",
    operator="Shell",
    well_type="Production",
    water_depth=Distance(value=150, unit="meter"),
    wellhead=Wellhead(x=10.0, y=60.0, crs="EPSG:4326"),
    source=AssetSource(asset_external_id="EDM:well-1", source_name="EDM"),
)
petrel_well = WellIngestion(
    matching_id="well-1-matching-id",
    name="well-1",
    quadrant="34",
    block="8",
    spud_date=date(2020, 5, 17),
    source=AssetSource(asset_external_id="Petrel:well-1", source_name="Petrel"),
)

#  Since the two wells have the exact same matching id, they will be matched.
wells_client.wells.ingest([edm_well, petrel_well])
[6]:
matchingId name description country quadrant block operator spudDate wellType wellheadX wellheadY wellheadCrs waterDepth waterDepthUnit sources
0 well-1-matching-id well-1 My new Well Norway 34 8 Shell 2020-05-17 Production 10.0 60.0 EPSG:4326 150.0 meter [{"assetExternalId": "EDM:well-1", "sourceName...
[7]:
# We can retrieve all wells like this
wells_client.wells.list()
[7]:
matchingId name description country quadrant block operator spudDate wellType wellheadX wellheadY wellheadCrs waterDepth waterDepthUnit sources
0 well-1-matching-id well-1 My new Well Norway 34 8 Shell 2020-05-17 Production 10.0 60.0 EPSG:4326 150.0 meter [{"assetExternalId": "EDM:well-1", "sourceName...
[8]:
# Or a single well like this
wells_client.wells.retrieve(asset_external_id="EDM:well-1")
[8]:
value
matchingId well-1-matching-id
name well-1
description My new Well
country Norway
quadrant 34
block 8
operator Shell
spudDate 2020-05-17
wellType Production
wellheadX 10.0
wellheadY 60.0
wellheadCrs EPSG:4326
waterDepth 150.0
waterDepthUnit meter
sources [{"assetExternalId": "EDM:well-1", "sourceName...
[9]:
# We can filter wells based on a search query. The query below searches for all
# wells that have operator set to either Shell or Equinor. The query is case
# sensitive.
wells_client.wells.list(operators=["Shell", "Equinor"])
[9]:
matchingId name description country quadrant block operator spudDate wellType wellheadX wellheadY wellheadCrs waterDepth waterDepthUnit sources
0 well-1-matching-id well-1 My new Well Norway 34 8 Shell 2020-05-17 Production 10.0 60.0 EPSG:4326 150.0 meter [{"assetExternalId": "EDM:well-1", "sourceName...
[10]:
# We can see the origins of the well by listing the sources
wells = wells_client.wells.list()
wells[0].sources
[10]:
[AssetSource(asset_external_id='EDM:well-1', source_name='EDM'),
 AssetSource(asset_external_id='Petrel:well-1', source_name='Petrel')]
[11]:
# And we can use the merge_details function to get more detailed information about the merge.
wells_client.wells.merge_details(matching_id="well-1-matching-id")
[11]:
value sourceName assetExternalId
property
name well-1 EDM EDM:well-1
description My new Well EDM EDM:well-1
country Norway EDM EDM:well-1
quadrant 34 Petrel Petrel:well-1
spudDate 2020-05-17 Petrel Petrel:well-1
block 8 Petrel Petrel:well-1
field None NaN NaN
operator Shell EDM EDM:well-1
wellType Production EDM EDM:well-1
license None NaN NaN
waterDepth {"value": 150.0, "unit": "meter"} EDM EDM:well-1
wellhead {"x": 10.0, "y": 60.0, "crs": "EPSG:4326"} EDM EDM:well-1

Ingesting wellbores

Let’s ingest some wellbores.

[12]:
edm_wb = WellboreIngestion(
    matching_id="wb-1",
    name="wb-1",
    description="Wellbore #1",
    well_asset_external_id="EDM:well-1",
    source=AssetSource(asset_external_id="EDM:wb-1", source_name="EDM"),
)

petrel_wb = WellboreIngestion(
    matching_id="wb-1",
    name="wb-1",
    well_asset_external_id="Petrel:well-1",
    datum=Datum(value=30.0, unit="meter", reference="KB"),
    source=AssetSource(asset_external_id="Petrel:wb-1", source_name="Petrel"),
)

edm_wb2 = WellboreIngestion(
    name="wb-2",
    description="Wellbore #2",
    well_asset_external_id="EDM:well-1",
    datum=Datum(value=30.0, unit="meter", reference="KB"),
    source=AssetSource(asset_external_id="EDM:wb-2", source_name="EDM"),
)

# EDM:wb-1 and Petrel:wb-1 will be matched since they have the same matching_id.
# EDM:wb-2 will be its own wellbore since the name doesn't match any existing
# wellbore and the matching_id isn't set.
wells_client.wellbores.ingest([edm_wb, edm_wb2, petrel_wb])
[12]:
matchingId name description wellMatchingId sources datum datumUnit datumReference
0 wb-1 wb-1 Wellbore #1 well-1-matching-id [{"assetExternalId": "EDM:wb-1", "sourceName":... 30.0 meter KB
1 a87da69c-fc01-4203-907d-d64c772ff611 wb-2 Wellbore #2 well-1-matching-id [{"assetExternalId": "EDM:wb-2", "sourceName":... 30.0 meter KB
[13]:
# We can see the wellbore with retrieve_multiple
wells_client.wellbores.retrieve_multiple(
    asset_external_ids=["EDM:wb-1", "EDM:wb-2"]
)
# The `wb-1` wellbore has the `matching_id` set to `wb-1`, while `wb-2` has
# gotten an auto-generated one from the well-service.
[13]:
matchingId name description wellMatchingId sources datum datumUnit datumReference
0 wb-1 wb-1 Wellbore #1 well-1-matching-id [{"assetExternalId": "EDM:wb-1", "sourceName":... 30.0 meter KB
1 a87da69c-fc01-4203-907d-d64c772ff611 wb-2 Wellbore #2 well-1-matching-id [{"assetExternalId": "EDM:wb-2", "sourceName":... 30.0 meter KB
[14]:
# A query for wells will also retrieve wellbores
for well in wells_client.wells.list():
    print("Well:", well.matching_id)
    for wellbore in well.wellbores:
        print("  Wellbore:", wellbore.matching_id)
Well: well-1-matching-id
  Wellbore: wb-1
  Wellbore: a87da69c-fc01-4203-907d-d64c772ff611
[15]:
# Just as with wells, we can view the merge details for a wellbore.
wells_client.wellbores.merge_details(matching_id="wb-1")
[15]:
value sourceName assetExternalId
property
name wb-1 EDM EDM:wb-1
description Wellbore #1 EDM EDM:wb-1
datum {"value": 30.0, "unit": "meter", "reference": ... Petrel Petrel:wb-1

Trajectories

Trajectories describe the path of a wellbore. The trajectory is derived from multiple points along the path (also called stations). Each point must have a - azimuth angle: north-east orientation. - inclination angle: down-up orientation. - measured depth (MD): distance along the wellbore path.

From these three values, the well data layer will automatically compute: - true vertical depth - dogleg severity (DLS): how much the trajectory curves. - offsets in north and east directions. - equivalent departure: horizontal distance from the wellhead.

Definitive trajectories

At most one of the trajectories for a given wellbore can be marked as the definitive trajectory. This is the trajectory that will be used for MD -> TVD calculations.

Let’s see it in action.

[16]:
trajectory_ingestion = TrajectoryIngestion(
    source=SequenceSource(
        sequence_external_id="EDM:trajectory1", source_name="EDM"
    ),
    wellbore_asset_external_id="EDM:wb-1",
    measured_depth_unit=DistanceUnitEnum.meter,
    inclination_unit=AngleUnitEnum.degree,
    azimuth_unit=AngleUnitEnum.degree,
    is_definitive=True,
    rows=[
        TrajectoryIngestionRow(
            measured_depth=100.0, inclination=0.0, azimuth=0.0
        ),
        TrajectoryIngestionRow(
            measured_depth=500.0, inclination=10.0, azimuth=60.0
        ),
        TrajectoryIngestionRow(
            measured_depth=600.0, inclination=10.0, azimuth=60.0
        ),
        TrajectoryIngestionRow(
            measured_depth=750.0, inclination=80.0, azimuth=70.0
        ),
        TrajectoryIngestionRow(
            measured_depth=900.0, inclination=90.0, azimuth=100.0
        ),
    ],
)

wells_client.trajectories.ingest([trajectory_ingestion])
[16]:
wellboreAssetExternalId wellboreMatchingId maxMeasuredDepth maxTrueVerticalDepth maxInclination maxDoglegSeverity maxDoglegSeverityUnit sourceName sourceSequenceExternalId isDefinitive
0 EDM:wb-1 wb-1 900.0 709.473624 90.0 14.031666 degree/30 meter EDM EDM:trajectory1 True
[17]:
trajectory = wells_client.trajectories.list(
    wellbore_asset_external_ids=["EDM:wb-1"]
)[0]
trajectory.data()
[17]:
measuredDepth trueVerticalDepth azimuth inclination northOffset eastOffset equivalentDeparture northing easting doglegSeverity
0 100.0 100.000000 0.0 0.0 0.000000 0.000000 0.000000 60.000000 10.000000 0.000000
1 500.0 497.972308 60.0 10.0 17.409033 30.153329 34.818065 60.000156 10.000542 0.750000
2 600.0 596.453083 60.0 10.0 26.091442 45.191702 52.182883 60.000234 10.000812 0.000000
3 750.0 696.112296 70.0 80.0 62.536889 137.740246 151.272066 60.000562 10.002475 14.031666
4 900.0 709.473624 100.0 90.0 75.092430 284.722097 294.458054 60.000674 10.005116 6.294990
[18]:
def plot_trajectory(trajectory):
    import matplotlib.pyplot as plt

    data = trajectory.data()
    x = [row.east_offset for row in data.rows]
    y = [row.north_offset for row in data.rows]
    z = [-row.true_vertical_depth for row in data.rows]
    ax = plt.axes(projection="3d")
    ax.set_xlabel("east")
    ax.set_ylabel("north")
    ax.set_zlabel("depth")
    ax.plot3D(x, y, z)


plot_trajectory(trajectory)
_images/wdl_24_0.png
[19]:
# With a trajectory ingested, we can use the interpolate endpoint to get TVD
# values between the stations. Since the `EDM:trajectory-1` trajectory is marked
# as definitive, that is the one that is going to be used for the interpolation.
wells_client.trajectories.interpolate(
    wellbore_matching_id="wb-1", measured_depths=[610, 611, 612, 613, 613.1]
)
[19]:
trueVerticalDepths
measuredDepths
610.0 606.220570
611.0 607.187141
612.0 608.151603
613.0 609.113892
613.1 609.209999

Depth measurements

Depth measurements are links to CDF sequences with additional metadata like the measurement type and the depth column. With this metadata, we support fetching by measured depth range and querying by measurement type.

Before we can ingest a sequence with measurements, we must first ingest a sequence into CDF.

[20]:
column_def = [
    {"valueType": "DOUBLE", "externalId": "MD", "metadata": {"unit": "0.1 m"}},
    {
        "valueType": "DOUBLE",
        "externalId": "GR",
        "metadata": {"unit": "roentgen"},
    },
    {"valueType": "DOUBLE", "externalId": "CAL1", "metadata": {"unit": "feet"}},
]

sequence = Sequence(external_id="EDM:meas-1", columns=column_def)

row_data = [
    (1, [100.0, 101.3, 423]),
    (2, [200.0, 101.4, 342]),
    (3, [300.0, 101.5, 343]),
    (4, [400.0, 101.6, 23]),
    (5, [500.0, 101.7, 3423]),
    (6, [600.0, 12.3, 343]),
    (7, [700.0, 145.3, 3423]),
    (9, [900.0, 70.3, 323]),
    (10, [1000.0, 92.3, 23]),
]

if client.sequences.retrieve(external_id=sequence.external_id) is not None:
    client.sequences.delete(external_id=sequence.external_id)

client.sequences.create(sequence)
client.sequences.data.insert(
    external_id=sequence.external_id,
    column_external_ids=["MD", "GR", "CAL1"],
    rows=row_data,
)
client.sequences.data.retrieve(external_id="EDM:meas-1", start=0, end=None)
[20]:
MD GR CAL1
1 100.0 101.3 423.0
2 200.0 101.4 342.0
3 300.0 101.5 343.0
4 400.0 101.6 23.0
5 500.0 101.7 3423.0
6 600.0 12.3 343.0
7 700.0 145.3 3423.0
9 900.0 70.3 323.0
10 1000.0 92.3 23.0

Mnemonics and the PWLS 3.0 standard

Mnemonics is the term used in oil and gas for different measurements. The mnemonic is determined by the logging company and the tool used to create the measurement.

There are tens of thousands of different mnemonics used in the oil and gas business. To be able to help classifying these mnemonics, WDL chose to lean on the PWLS 3.0 standard.

The PWLS 3.0 standard was standardized in March, 2021 and contains 3661 different measurement types. It also contains a list of mnemonics from different vendors that map to these measurement types. As an example, the GR mnemonic from Baker Hughes Inteq maps to the gamma ray measurement type. More information about the PWLS 3.0 standard cand be found on the Energistics web site.

To help classify unknown mnemonics, the WDL offers a mnemonic search.

[21]:
mnemonics_result = wells_client.mnemonics.search(["GR", "CAL1"])
mnemonics_result
[21]:
mnemonic companyName measurementType primaryQuantityClass tools
0 GR Baker Atlas (formerly Dresser Atlas) gamma ray API gamma ray []
1 GR Baker Hughes Inteq gamma ray api gamma ray [{"code": "DSL", "description": null}, {"code"...
2 GR Halliburton Logging gamma ray API gamma ray [{"code": "D4TG", "description": "DITS 4 Telem...
3 GR Halliburton Logging gamma ray API gamma ray [{"code": "D4TG", "description": "DITS 4 Telem...
4 GR Numar MRI Logging gamma ray API gamma ray []
5 GR Schlumberger gamma ray API gamma ray [{"code": "ARC3", "description": "3.125 Inch A...
6 CAL1 Baker Hughes Inteq caliper length [{"code": "EART", "description": null}, {"code...
7 CAL1 Halliburton Logging caliper length [{"code": "EMI_A", "description": "Electric Mi...
8 CAL1 Schlumberger borehole diameter length [{"code": "BHTV", "description": "Borehole Tel...

You can use this mnemonic search to get suggestions for measurement types.

Ingesting depth measurements

Now that we have a CDF sequence with measurement data, and we know which measurement types we should use for the differen columns, we can ingest a depth measurements into CDF.

[22]:
# Lets add some measurements
measurement = DepthMeasurementIngestion(
    # Connect the measurement to a wellbore. This wellbore must already have
    # been ingested.
    wellbore_asset_external_id="EDM:wb-1",
    source=SequenceSource(sequence_external_id="EDM:meas-1", source_name="EDM"),
    # We need to know which column on the sequence is the measured depth.
    depth_column=DepthIndexColumn(
        column_external_id="MD",
        unit=DistanceUnit(unit=DistanceUnitEnum.meter),
        type=DepthIndexTypeEnum.measured_depth,
    ),
    # Then we list the columns that are measurements:
    columns=[
        DepthMeasurementColumn(
            column_external_id="GR",
            measurement_type="gamma ray",
            unit="gAPI",
        ),
        DepthMeasurementColumn(
            column_external_id="CAL1",
            measurement_type="caliper",
            unit="inch",
        ),
    ],
)

wells_client.depth_measurements.ingest([measurement])
[22]:
wellboreAssetExternalId wellboreMatchingId sourceName sourceSequenceExternalId depthColumn datum datumUnit datumReference columns depthRangeMin depthRangeMax depthRangeUnit
0 EDM:wb-1 wb-1 EDM EDM:meas-1 {"columnExternalId": "MD", "unit": {"unit": "m... 30.0 meter KB [{"measurementType": "gamma ray", "columnExter... 100.0 1000.0 meter
[23]:
# We can retrieve the measurement by searching using the
# wellbore asset external id
measurement = wells_client.depth_measurements.list(
    wellbore_asset_external_ids=["EDM:wb-1"]
)[0]
measurement
[23]:
value
wellboreAssetExternalId EDM:wb-1
wellboreMatchingId wb-1
sourceName EDM
sourceSequenceExternalId EDM:meas-1
depthColumn {"columnExternalId": "MD", "unit": {"unit": "m...
datum 30.0
datumUnit meter
datumReference KB
columns [{"measurementType": "gamma ray", "columnExter...
depthRangeMin 100.0
depthRangeMax 1000.0
depthRangeUnit meter
[24]:
# Now we can retrieve the data and specify a lower and upper bound on the measured depth.
measurement.data(
    measured_depth=DistanceRange(
        unit=DistanceUnitEnum.meter, min=400.0, max=900.0
    )
)
[24]:
GR|gamma ray CAL1|caliper
depth
400.0 101.6 23.0
500.0 101.7 3423.0
600.0 12.3 343.0
700.0 145.3 3423.0
[25]:
# We can also search for depth measurements using a list
# of wellbores and a list of measurement types
measurements = wells_client.depth_measurements.list(
    wellbore_matching_ids=["wb-1"],
    measurement_types=["gamma ray"],
)
measurements
[25]:
wellboreAssetExternalId wellboreMatchingId sourceName sourceSequenceExternalId depthColumn datum datumUnit datumReference columns depthRangeMin depthRangeMax depthRangeUnit
0 EDM:wb-1 wb-1 EDM EDM:meas-1 {"columnExternalId": "MD", "unit": {"unit": "m... 30.0 meter KB [{"measurementType": "gamma ray", "columnExter... 100.0000 1000.0000 meter
1 Petrel:wb-1 wb-1 Petrel volve:dlis:90:s:0:0 {"columnExternalId": "TDEP", "unit": {"unit": ... 30.0 meter KB [{"measurementType": "gamma ray", "columnExter... 2777.9472 3749.9544 meter

Non-Productive Time events (NPT)

[26]:
npt_event = NptIngestion(
    wellbore_asset_external_id="EDM:wb-1",
    source=EventSource(event_external_id="EDM:npt-1", source_name="EDM"),
    start_time=datetime(2019, 5, 17, 10, 0, 0).timestamp()
    * 1000,  # milliseconds since 1970
    end_time=datetime(2019, 5, 17, 12, 0, 0).timestamp()
    * 1000,  # milliseconds since 1970
    npt_level="1",
    npt_code="ABCD",
    npt_code_detail="ABCDE",
    description="Factual description",
    measured_depth=Distance(value=4000, unit="foot"),
    root_cause="Leakage in pipe",
)

wells_client.npt.ingest([npt_event])
# Duration is in hours.

# Even though we ingested the measured depth in feet, it is stored internally in
# meters.
[26]:
wellboreAssetExternalId wellboreMatchingId nptCode nptCodeDetail nptLevel sourceName sourceEventExternalId description startTime endTime measuredDepth measuredDepthUnit duration rootCause
0 EDM:wb-1 wb-1 ABCD ABCDE 1 EDM EDM:npt-1 Factual description 1558080000000 1558087200000 1219.2 meter 2.0 Leakage in pipe
[27]:
# We can retrieve NPT events with
wells_client.npt.list()[0]
[27]:
value
wellboreAssetExternalId EDM:wb-1
wellboreMatchingId wb-1
nptCode ABCD
nptCodeDetail ABCDE
nptLevel 1
sourceName EDM
sourceEventExternalId EDM:npt-1
description Factual description
startTime 1558080000000
endTime 1558087200000
measuredDepth 1219.2
measuredDepthUnit meter
duration 2.0
rootCause Leakage in pipe

No Drilling Surprise events (NDS)

NDS events are similar in structure to NPT. The below code block ingests an NDS event under the second wellbore

[28]:
nds_event = NdsIngestion(
    wellbore_asset_external_id="EDM:wb-2",
    source=EventSource(event_external_id="EDM:nds-1", source_name="EDM"),
    hole_top=Distance(value=150.0, unit="meter"),
    hole_base=Distance(value=180.0, unit="meter"),
    severity=3,
    risk_type="Type of NDS risk",
    subtype="subtype",
    probability=4,  # Value between 0-5
)
wells_client.nds.ingest([nds_event])
[28]:
wellboreAssetExternalId wellboreMatchingId sourceName sourceEventExternalId holeStart holeStartUnit holeEnd holeEndUnit holeTop holeTopUnit holeBase holeBaseUnit riskType subtype severity probability
0 EDM:wb-2 a87da69c-fc01-4203-907d-d64c772ff611 EDM EDM:nds-1 150.0 meter 180.0 meter 150.0 meter 180.0 meter Type of NDS risk subtype 3 4
[29]:
# Or we can list all NDS events
wells_client.nds.list()
[29]:
wellboreAssetExternalId wellboreMatchingId sourceName sourceEventExternalId holeStart holeStartUnit holeEnd holeEndUnit holeTop holeTopUnit holeBase holeBaseUnit riskType subtype severity probability
0 EDM:wb-2 a87da69c-fc01-4203-907d-d64c772ff611 EDM EDM:nds-1 150.0 meter 180.0 meter 150.0 meter 180.0 meter Type of NDS risk subtype 3 4
[30]:
# Or by list of wellbore_asset_ids
wells_client.nds.list(wellbore_asset_external_ids=["EDM:wb-2"])
[30]:
wellboreAssetExternalId wellboreMatchingId sourceName sourceEventExternalId holeStart holeStartUnit holeEnd holeEndUnit holeTop holeTopUnit holeBase holeBaseUnit riskType subtype severity probability
0 EDM:wb-2 a87da69c-fc01-4203-907d-d64c772ff611 EDM EDM:nds-1 150.0 meter 180.0 meter 150.0 meter 180.0 meter Type of NDS risk subtype 3 4
[31]:
# Or filter by eg. hole_top
wells_client.nds.list(
    hole_top=DistanceRange(min=140.0, max=180.0, unit=DistanceUnitEnum.meter)
)
[31]:
wellboreAssetExternalId wellboreMatchingId sourceName sourceEventExternalId holeStart holeStartUnit holeEnd holeEndUnit holeTop holeTopUnit holeBase holeBaseUnit riskType subtype severity probability
0 EDM:wb-2 a87da69c-fc01-4203-907d-d64c772ff611 EDM EDM:nds-1 150.0 meter 180.0 meter 150.0 meter 180.0 meter Type of NDS risk subtype 3 4

Casings

[32]:
casing_schematic_ingestion = CasingSchematicIngestion(
    wellbore_asset_external_id="EDM:wb-1",
    source=SequenceSource(sequence_external_id="EDM:casings-1", source_name="EDM"),
    phase=PhaseEnum.actual,
    casing_assemblies=[
        CasingAssembly(
            min_inside_diameter=Distance(value=10, unit="inch"),
            min_outside_diameter=Distance(value=11, unit="inch"),
            max_outside_diameter=Distance(value=11, unit="inch"),
            original_measured_depth_top=Distance(value=8826.7900227906, unit="foot"),
            original_measured_depth_base=Distance(value=9687.0, unit="foot"),
            type="conductor",
            report_description="Drill Collar",
            section_type_code="DC",
            components=[
                CasingComponent(
                    description="Drill Collar 6.5 in, , 105, 91.786",
                    min_inside_diameter=Distance(value=6.5, unit="inch"),
                    max_outside_diameter=Distance(value=6.5, unit="inch"),
                    type_code="DC",
                    top_measured_depth=Distance(value=8826.7900227906, unit="foot"),
                    base_measured_depht=Distance(value=9687.0, unit="foot"),
                    grade="C-90",
                    connection_name="4 1/2 I",
                    joints=2,
                    manufacturer="BTS Ramco",

                    # linear weight is 91.7 pounds/foot
                    linear_weight=LinearWeight(
                        value=91.786,
                        unit=LinearWeightUnit(
                            weight_unit="pound",
                            depth_unit="foot"
                        )
                    )
                )
            ]
        )
    ]
)
wells_client.casings.ingest([casing_schematic_ingestion])
[32]:
wellboreAssetExternalId wellboreMatchingId casingAssemblies sourceName sourceSequenceExternalId phase
0 EDM:wb-1 wb-1 [{"minInsideDiameter": {"value": 0.254, "unit"... EDM EDM:casings-1 actual
[33]:
# List all casing schematics
wells_client.casings.list()
[33]:
wellboreAssetExternalId wellboreMatchingId casingAssemblies sourceName sourceSequenceExternalId phase
0 EDM:wb-1 wb-1 [{"minInsideDiameter": {"value": 0.254, "unit"... EDM EDM:casings-1 actual
[34]:
# Or by list all casing schematic for one or more wellbores
wells_client.casings.list(wellbore_asset_external_ids=["EDM:wb-1"])
[34]:
wellboreAssetExternalId wellboreMatchingId casingAssemblies sourceName sourceSequenceExternalId phase
0 EDM:wb-1 wb-1 [{"minInsideDiameter": {"value": 0.254, "unit"... EDM EDM:casings-1 actual

Querying

With wells and wellbores, we can search for wells and wellbores.

[35]:
def pretty_print_wells(wells):
    if len(wells) == 0:
        print("No wells")
    for w in wells:
        print(f"{w.name} ({w.description})")
        for i, wb in enumerate(w.wellbores):
            if i + 1 < len(w.wellbores):
                print(f"├── {wb.name} ({wb.description})")
            else:
                print(f"└── {wb.name} ({wb.description})")


# This will get all wells and wellbores matching the query. Since we are
# querying on a well property, we get all wellbores on the matching wells
wells = wells_client.wells.list(quadrants=["34"])
pretty_print_wells(wells)
well-1 (My new Well)
├── wb-1 (Wellbore #1)
└── wb-2 (Wellbore #2)
[36]:
# If we search for NPT events that are attached to a wellbore, it will find the
# _wellbores_ that match the query.
wells = wells_client.wells.list(
    # If we search using another distance unit than meter, it will be converted
    # into meters insidet he well-service.
    npt=WellNptFilter(
        measured_depth=DistanceRange(
            min=1219, max=1220, unit=DistanceUnitEnum.meter
        )
    )
)
pretty_print_wells(wells)
# Even though `my-well` has two wellbores, we only get one wellbore in the
# response. Since that is the only one that matched the query.
well-1 (My new Well)
└── wb-1 (Wellbore #1)
[37]:
# We can search for wellbores with NDS events now
wells = wells_client.wells.list(nds=WellNdsFilter())
pretty_print_wells(wells)
well-1 (My new Well)
└── wb-2 (Wellbore #2)
[38]:
# The following query combines multiple criterias.
polygon = (
    "POLYGON((11.519936553755512 63.94138054520784,"
    + "1.720131866255512 58.452506499450145,"
    + "21.89103030375551 57.75594384832666,"
    + "11.519936553755512 63.94138054520784))"
)
wells = wells_client.wells.list(
    quadrants=["34"],
    operators=["Shell"],
    sources=[
        "Petrel",
        "EDM",
    ],  # The well must either have a Petrel source or an EDM source.
    spud_date=DateRange(min=date(1970, 5, 17), max=date(2021, 12, 23)),
    well_types=["Production"],
    water_depth=DistanceRange(
        min=10.0, max=2000.0, unit=DistanceUnitEnum.meter
    ),
    polygon=PolygonFilter(
        geometry=polygon, crs="EPSG:4326", geometry_type="WKT"
    ),
    # If we search using another distance unit than meter, it will be converted
    # into meters inside the well-service.
    npt=WellNptFilter(
        measured_depth=DistanceRange(
            min=1219, max=1220, unit=DistanceUnitEnum.meter
        )
    ),
    trajectories=WellTrajectoryFilter(
        max_measured_depth=DistanceRange(
            min=700.0, max=900, unit=DistanceUnitEnum.meter
        )
    ),
    casings=WellCasingFilter(exists=True),
    depth_measurements=WellDepthMeasurementFilter(
        measured_depth=DistanceRange(
            unit=DistanceUnitEnum.meter, min=300.0, max=300.0
        ),
        measurement_types=ContainsAllOrAnyMeasurementType(
            contains_any=["gamma ray"],
        ),
    ),
)
pretty_print_wells(wells)
well-1 (My new Well)
└── wb-1 (Wellbore #1)

Plotting

The cognite-wells-sdk library has built-in functionality for well log plotting using matplotlib. The following example is using data from the VOLVE data set from Equinor.

[39]:
def get_depth_measurement_columns(depth_sequence):
    """Use the mnemonic search to find suitable measurement types"""
    columns = depth_sequence.columns
    mnemonics = [col["externalId"] for col in columns]
    mnemonics_definitions = wells_client.mnemonics.search(mnemonics)
    ingestion_columns = []
    for col, m_def in zip(columns, mnemonics_definitions):
        measurement_type = None
        for match in m_def.matches:
            if match.company_name == depth_sequence.metadata["producerName"]:
                measurement_type = match.measurement_type
                break
        if measurement_type is None:
            continue
        ingestion_columns.append(
            DepthMeasurementColumn(
                column_external_id=col["externalId"],
                measurement_type=measurement_type,
                unit=col["metadata"]["UNITS"],
                description=col["description"],
            )
        )
    return ingestion_columns
[40]:
# We first need to retrieve the sequence to take a look at the columns/mnemonics.
volve_seq = client.sequences.retrieve(external_id="volve:dlis:90:s:0:0")
depth_measurement_columns = get_depth_measurement_columns(volve_seq)

measurement = DepthMeasurementIngestion(
    wellbore_asset_external_id="Petrel:wb-1",
    source=SequenceSource(sequence_external_id="volve:dlis:90:s:0:0", source_name="Petrel"),
    depth_column=DepthIndexColumn(
        column_external_id="TDEP",
        unit=DistanceUnit(unit=DistanceUnitEnum.inch, factor=0.1),
        type=DepthIndexTypeEnum.measured_depth,
    ),
    columns=depth_measurement_columns,
)

wells_client.depth_measurements.ingest([measurement])
[40]:
wellboreAssetExternalId wellboreMatchingId sourceName sourceSequenceExternalId depthColumn datum datumUnit datumReference columns depthRangeMin depthRangeMax depthRangeUnit
0 Petrel:wb-1 wb-1 Petrel volve:dlis:90:s:0:0 {"columnExternalId": "TDEP", "unit": {"unit": ... 30.0 meter KB [{"measurementType": "depth", "columnExternalI... 2777.9472 3749.9544 meter
[41]:
# specify distance range in meters
depth_range = DistanceRange(min=2900, max=3200, unit=DistanceUnitEnum.meter)

# get all measurement rows within a given measured depth range
dm_data_in_range = wells_client.depth_measurements.list_data(
    "volve:dlis:90:s:0:0", measured_depth=depth_range, limit=None, depth_unit=DistanceUnit(unit="meter")
)

# create a plot
dm_data_in_range.plot(curves=["RFSL", "GRMA", "DWSI_WALK2", "P112"], log_curves=["A16H_COND"])
_images/wdl_55_0.png

Pandas integration

All data types in the WDL can be converted to pandas data frames using the .to_pandas() functions.

[42]:
all_nds_events = wells_client.nds.list(limit=None)
data_frame = all_nds_events.to_pandas()
data_frame
[42]:
wellboreAssetExternalId wellboreMatchingId sourceName sourceEventExternalId holeStart holeStartUnit holeEnd holeEndUnit holeTop holeTopUnit holeBase holeBaseUnit riskType subtype severity probability
0 EDM:wb-2 a87da69c-fc01-4203-907d-d64c772ff611 EDM EDM:nds-1 150.0 meter 180.0 meter 150.0 meter 180.0 meter Type of NDS risk subtype 3 4

As an example, we’ll retrieve all NPT events, and then use pandas to convert the timestamps to datetimes so that it’s human readable.

[43]:
import pandas as pd

npt_events = wells_client.npt.list(limit=None)
df = npt_events.to_pandas()

# Timestamps from CDF are always in milliseconds since 1. jan 1970.
# This will convert them to human readable date times.
df["startTimeStr"] = pd.to_datetime(df["startTime"], unit="ms")
df["endTimeStr"] = pd.to_datetime(df["endTime"], unit="ms")
df
[43]:
wellboreAssetExternalId wellboreMatchingId nptCode nptCodeDetail nptLevel sourceName sourceEventExternalId description startTime endTime measuredDepth measuredDepthUnit duration rootCause startTimeStr endTimeStr
0 EDM:wb-1 wb-1 ABCD ABCDE 1 EDM EDM:npt-1 Factual description 1558080000000 1558087200000 1219.2 meter 2.0 Leakage in pipe 2019-05-17 08:00:00 2019-05-17 10:00:00

Data exporting

With pandas, we can export the WDL data to CSV files or to sqlite3 databases.

[44]:
# Export all NPT events to a CSV file
df.to_csv("npt_events.csv")
[45]:
import sqlite3

# Export all NPT events to a sqlite3 database
connection = sqlite3.connect("wdl-example.db")
df.to_sql("npt_events", connection, if_exists="replace", index=False)

# We can use pandas to query the database we created
pd.read_sql(
    """select wellboreMatchingId,
              nptCode,
              nptCodeDetail,
              startTimeStr,
              endTimeStr
       from npt_events""",
    connection,
)
[45]:
wellboreMatchingId nptCode nptCodeDetail startTimeStr endTimeStr
0 wb-1 ABCD ABCDE 2019-05-17 08:00:00 2019-05-17 10:00:00