import math
import os
import tempfile
from io import StringIO
import numpy as np
import pytest
import las_rs
test_dir = os.path.dirname(__file__)
def fixture(*parts):
return os.path.join(test_dir, "fixtures", *parts)
V12_FILE = fixture("v12", "sample_v12.las")
def read_v12():
return las_rs.read(V12_FILE)
def csv_lines(las, **kwargs):
buf = StringIO()
las.to_csv(buf, **kwargs)
return buf.getvalue().splitlines()
_LATIN1_BAD_FILE = fixture("encodings", "latin1_bad_bytes.las")
_MALFORMED_HEADER_LAS = """\
~VERSION INFORMATION
VERS 2.0 LAS VERSION 2.0 NO COLON SEPARATOR
~WELL INFORMATION
STRT.M 100.0 : START
STOP.M 102.0 : STOP
STEP.M 1.0 : STEP
NULL. -999.25 : NULL
~CURVE INFORMATION
DEPT.M : DEPTH
~ASCII LOG DATA
100.0
101.0
102.0
"""
_LAS_CONTENT = """\
~VERSION INFORMATION
VERS. 2.0 : LAS VERSION 2.0
WRAP. NO : ONE LINE PER DEPTH STEP
~WELL INFORMATION
STRT.M 3000.0 : START DEPTH
STOP.M 3004.0 : STOP DEPTH
STEP.M 2.0 : STEP VALUE
NULL. -999.25 : NULL VALUE
COMP. PROPS TEST CORP : COMPANY
WELL. PROPSTEST-1 #3 : WELL NAME
UWI . 05-077-20130-00-00 : UNIQUE WELL ID
~CURVE INFORMATION
DEPT.M : DEPTH
DT .US/M : SONIC TRANSIT TIME
PORO.V/V : POROSITY
~PARAMETER INFORMATION
BHT .DEGC 155.0 : BOTTOM HOLE TEMPERATURE
MUD . OBM : MUD TYPE
~OTHER
Processed by PropsTest pipeline v2.
~ASCII LOG DATA
3000.0 312.45 0.182
3002.0 298.11 0.196
3004.0 325.67 0.174
"""
def _read_las():
return las_rs.read(_LAS_CONTENT)
def hitem(mnemonic, unit="", value="", descr=""):
return las_rs.HeaderItem(mnemonic=mnemonic, unit=unit, value=value, descr=descr)
def citem(mnemonic, unit="", value="", descr="", data=None):
if data is None:
data = np.array([])
return las_rs.CurveItem(mnemonic=mnemonic, unit=unit, value=value, descr=descr, data=data)
def _read_inline(las_text, **kwargs):
return las_rs.read(las_text, **kwargs)
def _minimal_header(well="INLINE-TEST", null="-999.25"):
return (
"~VERSION INFORMATION\n"
" VERS. 2.0 : LAS VERSION 2.0\n"
" WRAP. NO : ONE LINE PER DEPTH STEP\n"
"~WELL INFORMATION\n"
" STRT.M 1.0 : START\n"
" STOP.M 3.0 : STOP\n"
" STEP.M 1.0 : STEP\n"
f" NULL. {null} : NULL\n"
f" WELL. {well} : WELL\n"
)
_LAS_WRITE_BASE = """\
~VERSION INFORMATION
VERS. 2.0 : LAS VERSION 2.0
WRAP. NO : ONE LINE PER DEPTH STEP
~WELL INFORMATION
STRT.M 900.0 : START DEPTH
STOP.M 901.0 : STOP DEPTH
STEP.M 0.5 : STEP VALUE
NULL. -999.25 : NULL VALUE
COMP. WRITETEST CORP : COMPANY
WELL. WRITETEST-1 #2 : WELL NAME
UWI . 05-077-20130-00-00 : UNIQUE WELL ID
~CURVE INFORMATION
DEPT.M : DEPTH
GR .GAPI : GAMMA RAY
SP .MV : SPONTANEOUS POTENTIAL
~ASCII LOG DATA
900.0 55.0 -42.0
900.5 62.0 -47.0
901.0 70.0 -51.0
"""
def _read_base():
return las_rs.read(_LAS_WRITE_BASE)
def _write_to_string(las, **kwargs):
buf = StringIO()
las.write(buf, **kwargs)
return buf.getvalue()
_LAS_ASCII_BODY = """\
~VERSION INFORMATION
VERS. 2.0 : LAS VERSION 2.0
WRAP. NO : ONE LINE PER DEPTH STEP
~WELL INFORMATION
STRT.M 100.0 : START DEPTH
STOP.M 102.0 : STOP DEPTH
STEP.M 1.0 : STEP VALUE
NULL. -999.25 : NULL VALUE
COMP. Bohrinsel AG : COMPANY
WELL. UTF16-TEST : WELL NAME
~CURVE INFORMATION
DEPT.M : DEPTH
GR .GAPI : GAMMA RAY
~ASCII LOG DATA
100.0 55.0
101.0 62.0
102.0 70.0
"""
_UTF16_LE_FILE = fixture("encodings", "utf16_le_explicit.las")
if not os.path.exists(_UTF16_LE_FILE):
os.makedirs(os.path.dirname(_UTF16_LE_FILE), exist_ok=True)
with open(_UTF16_LE_FILE, "wb") as f:
f.write(_LAS_ASCII_BODY.encode("utf-16-le"))
def test_mnemonic_rename_to_empty():
sec = las_rs.SectionItems()
sec.append(citem("", unit="GAPI", data=np.array([30.0, 40.0])))
keys = list(sec.keys())
assert any("UNKNOWN" in k.upper() for k in keys)
def test_sparse_curves():
las = las_rs.read(fixture("edge_cases/sparse_curves.las"))
curve_mnemonics = [c.mnemonic for c in las.curves]
assert "NPHI" in curve_mnemonics
assert all(math.isnan(v) for v in las.curves["RHOB"].data)
assert all(math.isnan(v) for v in las.curves["DT"].data)
assert all(math.isnan(v) for v in las.curves["PE"].data)
def test_reshape_error_raises():
las_text = (
_minimal_header("RESHAPEERR")
+ "~CURVE INFORMATION\n"
" DEPT.M : DEPTH\n"
" GR .GAPI : GAMMA RAY\n"
" RHOB.G/CC : DENSITY\n"
"~ASCII LOG DATA\n"
" 1.0 30.5\n"
" 2.0 40.1\n"
" 3.0 50.7\n"
)
las = _read_inline(las_text)
assert las.data.shape == (3, 3)
assert all(math.isnan(v) for v in las.curves["RHOB"].data)
def test_runon_hyphen_separated():
las = las_rs.read(
fixture("edge_cases/runon.las"),
read_policy="run-on(-)",
)
gr = las.curves["GR"].data
assert not np.isnan(gr[0])
assert len(gr) > 0
def test_to_csv_custom_mnemonics():
las = read_v12()
custom = ["DEPTH_M", "GAMMA", "NPOR", "DENSITY"]
lines = csv_lines(las, mnemonics=custom)
header = lines[0]
for name in custom:
assert name in header
col_names = header.split(",")
assert col_names == custom
def test_write_nan_as_null():
las = read_v12()
gr_data = las["GR"].copy()
gr_data[0] = np.nan
las["GR"] = gr_data
null_str = str(las.well["NULL"].value).strip()
buf = StringIO()
las.write(buf)
output = buf.getvalue()
data_section = output[output.upper().index("~A"):]
assert null_str in data_section
def test_write_empty_value_string():
las_text = (
"~VERSION INFORMATION\n"
" VERS. 2.0 : LAS VERSION 2.0\n"
" WRAP. NO : WRAP\n"
"~WELL INFORMATION\n"
" STRT.M 100.0 : START\n"
" STOP.M 102.0 : STOP\n"
" STEP.M 1.0 : STEP\n"
" NULL. -999.25 : NULL\n"
" WELL. EMPTY-VAL : WELL\n"
" UWI . : UNIQUE WELL ID\n"
"~CURVE INFORMATION\n"
" DEPT.M : DEPTH\n"
"~ASCII LOG DATA\n"
" 100.0\n 101.0\n 102.0\n"
)
las = las_rs.read(las_text)
uwi_val = str(las.well["UWI"].value).strip()
assert uwi_val == ""
buf = StringIO()
las.write(buf)
buf.seek(0)
reread = las_rs.read(buf, ignore_header_errors=True)
assert str(reread.well["UWI"].value).strip() == ""
def test_write_renamed_mnemonic():
las = _read_base()
gr_data = las["GR"].copy()
las.delete_curve(mnemonic="GR")
las.append_curve("GAMMA", gr_data, unit="GAPI", descr="Gamma ray")
output = _write_to_string(las)
assert "GAMMA" in output
curve_section_start = output.upper().index("~C")
curve_section_end = output.upper().index("~A")
curve_block = output[curve_section_start:curve_section_end]
lines_with_gr = [ln for ln in curve_block.splitlines() if " GR " in ln or ln.strip().startswith("GR")]
assert len(lines_with_gr) == 0
def test_write_unit_change_propagates():
las = _read_base()
dept_data = las["DEPT"].copy()
las.delete_curve(mnemonic="DEPT")
las.insert_curve(0, "DEPT", dept_data, unit="FT", descr="DEPTH")
output = _write_to_string(las)
header_section = output[: output.upper().index("~A")]
strt_lines = [ln for ln in header_section.splitlines() if "STRT" in ln.upper()]
assert any("FT" in ln for ln in strt_lines), (
f"Expected 'FT' unit in STRT line but got: {strt_lines}"
)
@pytest.mark.xfail(reason="not yet implemented")
def test_utf16_le_explicit():
las = las_rs.read(_UTF16_LE_FILE, encoding="UTF-16-LE")
assert isinstance(las, las_rs.LASFile)
dept = las.curves["DEPT"].data
assert len(dept) == 3
@pytest.mark.xfail(reason="not yet implemented")
def test_encoding_errors_strict():
with pytest.raises((UnicodeDecodeError, las_rs.LASUnknownUnitError, Exception)):
las_rs.read(_LATIN1_BAD_FILE, encoding="utf-8", encoding_errors="strict")
def test_las_header_error_on_malformed():
las = las_rs.read(_MALFORMED_HEADER_LAS)
assert isinstance(las, las_rs.LASFile)
def test_version_setter():
las = _read_las()
new_section = las_rs.SectionItems(
[las_rs.HeaderItem(mnemonic="VERS", unit="", value="2.0", descr="LAS version")]
)
las.version = new_section
assert list(las.version.keys()) == ["VERS"]
assert str(las.version["VERS"].value) == "2.0"
def test_well_setter():
las = _read_las()
new_section = las_rs.SectionItems(
[las_rs.HeaderItem(mnemonic="WELL", unit="", value="SETTER-WELL-1", descr="")]
)
las.well = new_section
assert list(las.well.keys()) == ["WELL"]
assert las.well["WELL"].value == "SETTER-WELL-1"
def test_curves_setter():
las = _read_las()
new_section = las_rs.SectionItems(
[las_rs.CurveItem(mnemonic="DEPTH", unit="M", value="", descr="Depth")]
)
las.curves = new_section
assert list(las.curves.keys()) == ["DEPTH"]
assert las.curves["DEPTH"].unit == "M"
def test_params_setter():
las = _read_las()
new_section = las_rs.SectionItems(
[las_rs.HeaderItem(mnemonic="RES", unit="OHM", value="1.5", descr="Resistivity")]
)
las.params = new_section
assert list(las.params.keys()) == ["RES"]
assert las.params["RES"].unit == "OHM"
def test_update_start_stop_step():
las = _read_las()
new_dept = np.array([5000.0, 5005.0, 5010.0])
las["DEPT"] = new_dept
las["DT"] = np.array([300.0, 310.0, 320.0])
las["PORO"] = np.array([0.18, 0.19, 0.17])
las.update_start_stop_step()
assert float(las.well["STRT"].value) == pytest.approx(5000.0)
assert float(las.well["STOP"].value) == pytest.approx(5010.0)
assert float(las.well["STEP"].value) == pytest.approx(5.0)
def test_update_units_from_index_curve():
las = _read_las()
dept_data = las["DEPT"].copy()
las.delete_curve(mnemonic="DEPT")
las.insert_curve(0, "DEPT", dept_data, unit="FT", descr="DEPTH")
las.update_start_stop_step()
assert las.well["STRT"].unit == "FT"
assert las.well["STOP"].unit == "FT"
assert las.well["STEP"].unit == "FT"