import os
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)
_LAS_WRITE_BASE = """\
~VERSION INFORMATION
VERS. 2.0 : LAS VERSION 2.0
WRAP. NO : ONE LINE PER DEPTH STEP
~WELL INFORMATION
STRT.M 600.0 : START DEPTH
STOP.M 603.0 : STOP DEPTH
STEP.M 1.0 : STEP VALUE
NULL. -999.25 : NULL VALUE
COMP. FRONTIER DRILLING : COMPANY
WELL. OSPREY-2 #4 : WELL NAME
UWI . 05-077-20130-00-00 : WELL IDENTIFIER
~CURVE INFORMATION
DEPT.M : MEASURED DEPTH
GR .GAPI : GAMMA RAY
DT .US/M : SONIC TRANSIT TIME
RHOB.G/CC : BULK DENSITY
~ASCII LOG DATA
600.0 48.30 78.10 2.421
601.0 62.50 84.20 2.555
602.0 71.80 90.50 2.688
603.0 55.20 81.30 2.499
"""
_LAS_V12_WRITE = """\
~VERSION INFORMATION
VERS. 1.2 : LAS VERSION 1.2
WRAP. NO : ONE LINE PER DEPTH STEP
~WELL INFORMATION BLOCK
STRT.M 700.0 : START DEPTH
STOP.M 701.0 : STOP DEPTH
STEP.M 1.0 : STEP VALUE
NULL. -999.25 : NULL VALUE
COMP. : SPRINGFIELD RESOURCES
WELL. : HAWK-5
~CURVE INFORMATION BLOCK
DEPT.M : DEPTH
GR .GAPI : GAMMA RAY
~ASCII LOG DATA
700.0 28.40
701.0 33.60
"""
_LAS_WRONG_STOP = """\
~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 999.0 : STOP DEPTH (WRONG)
STEP.M 1.0 : STEP VALUE
NULL. -999.25 : NULL VALUE
WELL. BADSTOP-1 : WELL NAME
~CURVE INFORMATION
DEPT.M : DEPTH
GR .GAPI : GAMMA RAY
~ASCII LOG DATA
100.0 40.0
101.0 45.0
102.0 50.0
"""
_LAS_NO_PARAMS = """\
~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 1.0 : STEP VALUE
NULL. -999.25 : NULL VALUE
WELL. NOPARAM-1 : WELL NAME
~CURVE INFORMATION
DEPT.M : DEPTH
GR .GAPI : GAMMA RAY
~ASCII LOG DATA
900.0 66.0
901.0 70.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()
@pytest.mark.xfail(reason="not yet implemented")
def test_write_lhs_spacer():
las = _read_base()
output = _write_to_string(las, lhs_spacer="")
data_start = output.upper().index("~A")
data_lines = [
ln for ln in output[data_start:].splitlines()
if ln.strip() and not ln.strip().startswith("~") and not ln.strip().startswith("#")
]
for line in data_lines[:3]:
assert not line.startswith(" "), (
f"Expected no leading space but got: {line!r}"
)
@pytest.mark.xfail(reason="not yet implemented")
def test_write_spacer_comma():
las = _read_base()
output = _write_to_string(las, spacer=",")
data_start = output.upper().index("~A")
data_section = output[data_start:]
data_lines = [
ln for ln in data_section.splitlines()
if ln.strip() and not ln.strip().startswith("~") and not ln.strip().startswith("#")
]
assert any("," in line for line in data_lines)
@pytest.mark.xfail(reason="not yet implemented")
def test_write_len_numeric_field():
las = _read_base()
output = _write_to_string(las, len_numeric_field=15)
data_start = output.upper().index("~A")
data_section = output[data_start:]
data_lines = [
ln for ln in data_section.splitlines()
if ln.strip() and not ln.strip().startswith("~") and not ln.strip().startswith("#")
]
first_line = data_lines[0]
tokens = first_line.split()
assert len(tokens) == 4
assert len(first_line.rstrip()) >= 4 * 14
@pytest.mark.xfail(reason="not yet implemented")
def test_write_len_numeric_field_minus1():
las = _read_base()
output_aligned = _write_to_string(las, len_numeric_field=15)
output_free = _write_to_string(las, len_numeric_field=-1)
assert output_aligned != output_free
@pytest.mark.xfail(reason="not yet implemented")
def test_write_column_fmt():
las = _read_base()
output = _write_to_string(las, column_fmt={0: "%.3f"})
data_start = output.upper().index("~A")
data_section = output[data_start:]
data_lines = [
ln for ln in data_section.splitlines()
if ln.strip() and not ln.strip().startswith("~") and not ln.strip().startswith("#")
]
first_depth_token = data_lines[0].split()[0]
assert first_depth_token == "600.000" or first_depth_token.endswith(".000")
@pytest.mark.xfail(reason="not yet implemented")
def test_write_data_width():
las = _read_base()
output = _write_to_string(las, data_width=40, wrap=True)
data_start = output.upper().index("~A")
data_section = output[data_start:]
data_lines = [
ln for ln in data_section.splitlines()
if ln.strip() and not ln.strip().startswith("~") and not ln.strip().startswith("#")
]
for line in data_lines:
assert len(line) <= 40, (
f"Line exceeds data_width=40: {len(line)} chars: {line!r}"
)
@pytest.mark.xfail(reason="not yet implemented")
def test_write_header_width():
las = _read_base()
output = _write_to_string(las, header_width=80)
in_well = False
well_lines = []
for line in output.splitlines():
stripped = line.strip()
if stripped.upper().startswith("~W"):
in_well = True
continue
if stripped.startswith("~") and in_well:
break
if in_well and "." in line:
well_lines.append(line)
for line in well_lines:
assert len(line) <= 80 or len(line.rstrip()) <= 80, (
f"Header line exceeds header_width=80: {len(line)} chars: {line!r}"
)
@pytest.mark.xfail(reason="not yet implemented")
def test_write_data_section_header_custom():
las = _read_base()
output = _write_to_string(las, data_section_header="~A")
lines = output.splitlines()
ascii_lines = [ln.strip() for ln in lines if ln.strip().upper().startswith("~A")]
assert any(ln == "~A" for ln in ascii_lines), (
"Expected '~A' as data section header but found: " + str(ascii_lines)
)
@pytest.mark.xfail(reason="not yet implemented")
def test_write_mnemonics_header():
las = _read_base()
output = _write_to_string(las, mnemonics_header=True)
data_start = output.upper().index("~A")
lines_after = output[data_start:].splitlines()
non_empty = [ln for ln in lines_after if ln.strip()]
header_line = non_empty[1] if len(non_empty) > 1 else ""
for curve in las.curves:
assert curve.mnemonic in header_line, (
f"Mnemonic '{curve.mnemonic}' not found in mnemonics header: {header_line!r}"
)
@pytest.mark.xfail(reason="not yet implemented")
def test_write_version_none_uses_file():
las = las_rs.read(_LAS_V12_WRITE)
output = _write_to_string(las, version=None)
assert "1.2" in output
@pytest.mark.xfail(reason="not yet implemented")
def test_write_version_12_explicit():
las = _read_base()
output = _write_to_string(las, version=1.2)
assert "1.2" in output
assert "FRONTIER DRILLING" in output
@pytest.mark.xfail(reason="not yet implemented")
def test_write_version_20_explicit():
las = las_rs.read(_LAS_V12_WRITE)
output = _write_to_string(las, version=2.0)
assert "2.0" in output
@pytest.mark.xfail(reason="not yet implemented")
def test_write_preserves_inmemory_version():
las = las_rs.read(_LAS_V12_WRITE)
original_vers = las.version["VERS"].value
_write_to_string(las, version=2.0)
assert las.version["VERS"].value == original_vers
@pytest.mark.xfail(reason="not yet implemented")
def test_write_strt_override():
las = _read_base()
output = _write_to_string(las, STRT=500.0)
assert "500" in output
data_start = output.upper().index("~A")
data_section = output[data_start:]
assert "600" in data_section
@pytest.mark.xfail(reason="not yet implemented")
def test_write_stop_override():
las = _read_base()
output = _write_to_string(las, STOP=600.0)
header_section = output[: output.upper().index("~A")]
assert "600" in header_section
@pytest.mark.xfail(reason="not yet implemented")
def test_write_step_override():
las = _read_base()
output = _write_to_string(las, STEP=0.5)
header_section = output[: output.upper().index("~A")]
assert "0.5" in header_section
@pytest.mark.xfail(reason="not yet implemented")
def test_write_recalculates_wrong_stop():
las = las_rs.read(_LAS_WRONG_STOP, ignore_header_errors=True)
output = _write_to_string(las)
header_section = output[: output.upper().index("~A")]
assert "102" in header_section
lines = header_section.splitlines()
stop_lines = [ln for ln in lines if "STOP" in ln.upper()]
for sl in stop_lines:
assert "999" not in sl or "102" in sl, (
f"STOP line still shows wrong value: {sl!r}"
)
@pytest.mark.xfail(reason="not yet implemented")
def test_write_empty_value_none():
las = _read_base()
las.well["UWI"].value = None
output = _write_to_string(las)
well_end = output.upper().index("~C")
header_block = output[:well_end]
uwi_lines = [ln for ln in header_block.splitlines() if "UWI" in ln.upper()]
for ln in uwi_lines:
assert "None" not in ln, f"Unexpected 'None' literal in: {ln!r}"
@pytest.mark.xfail(reason="not yet implemented")
def test_write_duplicate_mnemonics_original():
las_text = """\
~VERSION INFORMATION
VERS. 2.0 : LAS VERSION 2.0
WRAP. NO : ONE LINE PER DEPTH STEP
~WELL INFORMATION
STRT.M 1.0 : START
STOP.M 2.0 : STOP
STEP.M 1.0 : STEP
NULL. -999.25 : NULL
WELL. DUP-WELL : WELL
~CURVE INFORMATION
DEPT.M : DEPTH
GR .GAPI : GAMMA RAY (PRIMARY)
GR .GAPI : GAMMA RAY (REPEAT)
~ASCII LOG DATA
1.0 30.0 31.5
2.0 35.0 36.8
"""
las = las_rs.read(las_text, ignore_header_errors=True)
output = _write_to_string(las)
curve_section_start = output.upper().index("~C")
curve_section_end = output.upper().index("~A")
curve_block = output[curve_section_start:curve_section_end]
assert ":1" not in curve_block
assert ":2" not in curve_block
@pytest.mark.xfail(reason="not yet implemented")
def test_write_empty_params():
las = las_rs.read(_LAS_NO_PARAMS)
output = _write_to_string(las)
assert isinstance(output, str)
assert "~V" in output.upper() or "VERS" in output.upper()
@pytest.mark.xfail(reason="not yet implemented")
def test_write_after_set_data_from_df():
pytest.importorskip("pandas")
import pandas as pd
las = _read_base()
dept = las.curves["DEPT"].data
new_gr = np.array([10.0, 20.0, 30.0, 40.0])
new_dt = las.curves["DT"].data.copy()
new_rhob = las.curves["RHOB"].data.copy()
df = pd.DataFrame(
{"GR": new_gr, "DT": new_dt, "RHOB": new_rhob},
index=dept,
)
df.index.name = "DEPT"
las.set_data_from_df(df)
output = _write_to_string(las)
data_start = output.upper().index("~A")
data_section = output[data_start:]
assert "10.0" in data_section or "10." in data_section
assert "48.30" not in data_section