Core: Well and Property¶
Well¶
- class logsuite.core.well.Well(name, sanitized_name, parent_manager=None)[source]
Bases:
objectSingle well containing multiple log properties.
- Parameters:
- name
Original well name.
- Type:
- sanitized_name
Sanitized name for attribute access.
- Type:
- parent_manager
Parent manager.
- Type:
WellDataManager, optional
- original_las
First LAS file loaded (for template-based export).
- Type:
LasFile, optional
Examples
>>> well = manager.well_12_3_2_B >>> well.load_las("log1.las").load_las("log2.las") >>> print(well.properties) ['PHIE', 'PERM', 'Zone', 'NTG_Flag'] >>> print(well.sources) # ['log1.las', 'log2.las'] >>> >>> # Add DataFrame as source >>> df = pd.DataFrame({'DEPT': [2800, 2801], 'SW': [0.3, 0.32]}) >>> well.add_dataframe(df, unit_mappings={'SW': 'v/v'}) >>> print(well.sources) # ['log1.las', 'log2.las', 'external_df'] >>> >>> stats = well.phie.filter('Zone').sums_avg()
- load_las(las, path=None, sampled=False, resample_method=None, merge=False, combine=None, source_name=None)[source]
Load LAS file(s) into this well, organized by source.
Properties are grouped by source (LAS file). The source name is derived from the filename stem (without extension), sanitized for Python attribute access. If the filename starts with the well name, that prefix is removed to avoid redundancy (e.g., “36_7_5_B_CorePor.las” becomes “CorePor”).
- Parameters:
las (Union[LasFile, str, Path, list[Union[str, Path]]]) – Either a LasFile instance, path to LAS file, or list of LAS file paths. When providing a list, filenames can be relative to the path parameter.
path (Union[str, Path], optional) – Directory path to prepend to all filenames. Useful when loading multiple files from the same directory. If None, filenames are used as-is.
sampled (bool, default False) – If True, mark all properties from this source as ‘sampled’ type. Use this for core plug data or other point measurements where boundary insertion during filtering should be disabled.
resample_method (str, optional) – Method to use if depth grids are incompatible. Only used when loading data with different depth grids than existing data: - None (default): Will raise error if grids incompatible - ‘linear’: Linear interpolation (for continuous properties) - ‘nearest’: Nearest neighbor (for discrete/sampled) - ‘previous’: Forward-fill / previous value (for discrete) - ‘next’: Backward-fill / next value
merge (bool, default False) – If True and a source with the same name already exists, merge the new properties into the existing source instead of overwriting it. When merging with incompatible depth grids, resample_method must be specified. If False (default), existing source is overwritten with warning.
combine (str, optional) – When loading multiple files (list), combine them into a single source: - None (default): Load files as separate sources, no combining - ‘match’: Combine using match method (safest, errors on mismatch) - ‘resample’: Combine using resample method (interpolates to first file) - ‘concat’: Combine using concat method (merges all unique depths)
source_name (str, optional) – Name for combined source when combine is specified. If not specified, uses ‘combined_match’, ‘combined_resample’, or ‘combined_concat’
- Returns:
Self for method chaining
- Return type:
Well
- Raises:
WellNameMismatchError – If LAS well name doesn’t match this well
WellError – If no depth column found in LAS file If merging with incompatible depths and no resample_method specified
Examples
>>> # Load single file >>> well.load_las("36_7-5_B_CorePor.las") # Source name: "CorePor"
>>> # Load multiple files as separate sources >>> well.load_las(["file1.las", "file2.las", "file3.las"]) >>> print(well.sources) # ['file1', 'file2', 'file3']
>>> # Load and combine multiple files >>> well.load_las( ... ["file1.las", "file2.las", "file3.las"], ... combine="match", ... source_name="CombinedLogs" ... ) >>> print(well.sources) # ['CombinedLogs']
>>> # Load core plug data as sampled >>> well.load_las("36_7-5_B_Core.las", sampled=True)
>>> # Load with resample combining >>> well.load_las( ... ["log1.las", "log2.las"], ... combine="resample", ... source_name="CombinedLogs" ... )
>>> # Merge into existing source (legacy behavior) >>> well.load_las("CorePor.las") # Load initial properties >>> well.load_las("CorePor_Extra.las", merge=True) # Merge new properties
>>> # Load multiple files from same directory >>> well.load_las( ... ["file1.las", "file2.las", "file3.las"], ... path="data/well_logs", ... combine="match", ... source_name="AllLogs" ... ) >>> # Loads: data/well_logs/file1.las, data/well_logs/file2.las, etc.
>>> # Mix of relative and absolute paths >>> well.load_las( ... ["log1.las", "log2.las"], ... path="/absolute/path/to/logs" ... )
- add_dataframe(df, unit_mappings=None, type_mappings=None, label_mappings=None)[source]
Add properties from a DataFrame to this well as a new source.
The DataFrame must contain a DEPT column. All other columns will be added as properties grouped under a source named ‘external_df’, ‘external_df_1’, etc.
- Parameters:
df (pd.DataFrame) – DataFrame containing DEPT and property columns
unit_mappings (dict[str, str], optional) – Mapping of property names to units (e.g., {‘PHIE’: ‘v/v’})
type_mappings (dict[str, str], optional) – Mapping of property names to types: ‘continuous’ or ‘discrete’ Default is ‘continuous’ for all properties
label_mappings (dict[str, dict[int, str]], optional) – Label mappings for discrete properties Format: {‘PropertyName’: {0: ‘Label0’, 1: ‘Label1’}}
- Returns:
Self for method chaining
- Return type:
Well
Examples
>>> # Create DataFrame with properties >>> df = pd.DataFrame({ ... 'DEPT': [2800, 2800.5, 2801], ... 'PHIE': [0.2, 0.25, 0.22], ... 'SW': [0.3, 0.35, 0.32], ... 'Zone': [0, 1, 1] ... }) >>> >>> # Add to well with metadata >>> well.add_dataframe( ... df, ... unit_mappings={'PHIE': 'v/v', 'SW': 'v/v', 'Zone': ''}, ... type_mappings={'Zone': 'discrete'}, ... label_mappings={'Zone': {0: 'NonReservoir', 1: 'Reservoir'}} ... ) >>> >>> # Check sources >>> print(well.sources) # ['log', 'external_df'] >>> # Access properties >>> well.external_df.Zone
- rename_source(old_name, new_name)[source]
Rename a source to resolve conflicts or improve clarity.
The source is marked for rename. When the project is saved, the corresponding LAS file will be renamed on disk.
- Parameters:
- Returns:
Self for method chaining
- Return type:
Well
- Raises:
KeyError – If old source doesn’t exist
ValueError – If new name already exists or conflicts
Examples
>>> well.load_las("log.las") # Creates source named 'log' >>> well.load_las("core.las") # Creates source named 'core' >>> # Both have PHIE - rename one for clarity >>> well.rename_source("log", "wireline") >>> well.wireline.PHIE # Access renamed source >>> manager.save() # Will rename log.las to wireline.las on disk
- mark_source_modified(name)[source]
Mark a source as modified so it will be re-exported on save.
This is useful if you manually modify property values and want to ensure the source is saved to disk.
- Parameters:
name (str) – Source name to mark as modified
- Returns:
Self for method chaining
- Return type:
Well
- Raises:
KeyError – If source doesn’t exist
Examples
>>> # Modify property values directly >>> well.log.PHIE.values *= 1.1 # Scale up by 10% >>> well.mark_source_modified("log") >>> manager.save() # Will re-export the modified source
- remove_source(name)[source]
Remove one or more sources and all their properties from the well.
The sources are marked for deletion. When the project is saved, the corresponding LAS files will be deleted from disk.
- Parameters:
name (str or list[str]) – Source name(s) to remove. Can be a single string or list of strings.
- Returns:
Self for method chaining
- Return type:
Well
- Raises:
KeyError – If any source doesn’t exist
Examples
>>> # Remove single source >>> well.load_las("log.las") >>> well.load_las("core.las") >>> well.sources # ['log', 'core'] >>> well.remove_source("log") >>> well.sources # ['core'] >>> manager.save() # Will delete log.las from project folder >>> >>> # Remove multiple sources >>> well.remove_source(["log", "core"]) >>> well.sources # []
- property properties: list[str]
List of all accessible property names with their access patterns.
If a property is unique across all sources, it’s listed as-is. If a property exists in multiple sources, each is listed with source prefix. If a source name conflicts with a property name, the property needs source prefix.
Examples
>>> well.properties ['PHIE', 'SW', 'log.PERM', 'core.PERM'] # PERM exists in both sources >>> # If source named 'PHIE' exists with property 'PHIE': >>> well.properties ['PHIE.PHIE', 'SW', 'PERM'] # PHIE property needs prefix due to source conflict
- property sources: list[str]
List of all source names loaded into this well.
Source names are sanitized from LAS file names (without extension) or ‘external_df’ for DataFrames.
Examples
>>> well.load_las("log1.las") >>> well.add_dataframe(df) >>> print(well.sources) # ['log1', 'external_df'] >>> # Access sources >>> well.log1.PHIE >>> well.external_df.Zone
- property original_las: LasFile | None
Get the first (original) LAS file loaded into this well.
Returns None if no sources have been loaded yet.
- Returns:
First LasFile object loaded, or None
- Return type:
Optional[LasFile]
Examples
>>> well.load_las("log1.las") >>> original = well.original_las >>> well.export_to_las("output.las", use_template=original)
- get_property(name, source=None)[source]
Explicit property getter.
If source is specified, gets property from that source only. If source is None, gets property if it’s unique across all sources, raises error if ambiguous.
- Parameters:
- Returns:
The requested property
- Return type:
Property
- Raises:
PropertyNotFoundError – If property not found or is ambiguous (exists in multiple sources)
See also
propertiesList all accessible property names.
sourcesList all source names.
Examples
>>> prop = well.get_property("PHIE") # Gets PHIE if unique >>> prop = well.get_property("PHIE", source="log") # Gets PHIE from log source
- get_intervals(name)[source]
Get saved filter intervals by name.
- Parameters:
name (str) – Name of the saved filter intervals
- Returns:
List of interval definitions, each with keys ‘name’, ‘top’, ‘base’
- Return type:
- Raises:
KeyError – If no intervals with this name exist
Examples
>>> # Save intervals >>> well.PHIE.filter_intervals([ ... {"name": "Zone_A", "top": 2500, "base": 2650} ... ], save="My_Zones") >>> >>> # Retrieve them later >>> intervals = well.get_intervals("My_Zones") >>> print(intervals) [{'name': 'Zone_A', 'top': 2500, 'base': 2650}]
- merge(method='match', sources=None, properties=None, depth_step=None, depth_range=None, depth_grid=None, source_name=None)[source]
Merge properties from multiple sources into a new “merged” source.
This operation replaces the selected properties with new merged versions. The original LAS files are not modified, but the in-memory properties will be replaced with the merged versions.
- Parameters:
method ({'match', 'resample', 'concat'}, default 'match') –
Merge method:
’match’: Use first source’s depth grid as reference. Continuous properties must have exact depth match (errors otherwise). Discrete properties are automatically resampled using interval logic, since they define depth intervals. This is the safest option when depths should already align.
’resample’: Use first source’s depth grid, interpolate other sources to match. If first source has regular spacing (e.g., every 0.1m), allows extrapolation for other sources. If irregular spacing, raises error when other sources extend beyond first source’s range.
’concat’: Merge all unique depths, fill NaN where depth doesn’t exist
sources (list[str], optional) – List of source names to include (e.g., [‘CorePerm’, ‘CorePor’]) Required for ‘match’ and ‘resample’ methods. The first source is used as the reference depth grid.
properties (list[str], optional) – List of property names to include. If None, includes all properties
depth_step (float, optional) – Not used (deprecated - kept for backward compatibility)
depth_range (tuple[float, float], optional) – Not used (deprecated - kept for backward compatibility)
depth_grid (np.ndarray, optional) – Not used (deprecated - kept for backward compatibility)
source_name (str, optional) – Custom source name for merged properties. If None, uses ‘merged_{method}’
- Returns:
Self for method chaining
- Return type:
Well
- Raises:
ValueError – If invalid method specified
WellError – If no properties match the filters, if ‘match’ method detects incompatible depths, or if ‘resample’ with irregular grid detects extrapolation requirements
Examples
>>> # Match sources with compatible depths (default, safest) >>> well.merge( ... sources=['CorePerm', 'CorePor'], ... source_name='CorePlugs' ... )
>>> # Resample to first source's grid (allows interpolation) >>> well.merge( ... method='resample', ... sources=['CompLogs', 'CorePerm'], ... source_name='Resampled' ... )
>>> # Concatenate specific sources with custom name >>> well.merge( ... method='concat', ... sources=['log1', 'log2'], ... source_name='combined_logs' ... )
>>> # Match only specific properties >>> well.merge( ... sources=['source1', 'source2'], ... properties=['PHIE', 'SW'], ... source_name='selected_props' ... )
- data(reference_property=None, include=None, exclude=None, merge_method='match', discrete_labels=True, clip_edges=True, clip_to_property=None)[source]
Export properties as DataFrame with optional merging and filtering.
This method does NOT modify the original property depth grids. Properties are temporarily aligned using the specified merge method for DataFrame output only.
- Parameters:
reference_property (str, optional) – Property to use as depth reference. All properties will be aligned to this property’s depth grid using the merge_method. If not specified, defaults to the first property that was added (typically the first property from the first LAS file loaded).
include (str or list[str], optional) – Property name(s) to include. If None, includes all properties. Can be a single string or a list of strings.
exclude (str or list[str], optional) – Property name(s) to exclude. If both include and exclude are specified, exclude overrides (removes properties from include list). Can be a single string or a list of strings.
merge_method ({'match', 'resample', 'concat'}, default 'match') –
Method to align properties to reference depth grid:
’match’: Require exact depth match for continuous properties (errors if not aligned). Discrete properties are automatically resampled using interval logic. Safest option.
’resample’: Interpolate properties to reference depth grid
’concat’: Merge unique depths, fill NaN where missing
discrete_labels (bool, default True) – If True, apply label mappings to discrete properties with labels defined.
clip_edges (bool, default True) – If True, remove rows at the start and end where all data columns (excluding DEPT) contain NaN values. This trims the DataFrame to the range where actual data exists.
clip_to_property (str, optional) – Clip output to the defined range of this specific property. If specified, overrides clip_edges behavior and clips to where this property has valid data.
- Returns:
DataFrame with DEPT and selected properties
- Return type:
pd.DataFrame
- Raises:
WellError – If properties have different depth grids and merge_method is ‘match’, or if merge requirements fail
PropertyNotFoundError – If reference_property or included properties are not found
Examples
>>> # Export all properties (errors if depths don't match exactly) >>> df = well.data()
>>> # Export with interpolation if depths don't align >>> df = well.data(merge_method='resample')
>>> # Export with specific reference property >>> df = well.data(reference_property='PHIE')
>>> # Include only specific properties >>> df = well.data(include=['PHIE', 'SW', 'PERM'])
>>> # Include single property (string or list) >>> df = well.data(include='PHIE') # Same as include=['PHIE']
>>> # Exclude specific properties >>> df = well.data(exclude=['QC_Flag', 'Temp_Data'])
>>> # Exclude single property >>> df = well.data(exclude='QC_Flag')
>>> # Include with exclusions (exclude overrides) >>> df = well.data(include=['PHIE', 'SW', 'PERM', 'Zone'], exclude=['Zone'])
>>> # Use concat merge method to include all unique depths >>> df = well.data(merge_method='concat')
>>> # Disable label mapping for discrete properties >>> df = well.data(discrete_labels=False)
>>> # Disable edge clipping to keep all NaN rows >>> df = well.data(clip_edges=False)
>>> # Clip to specific property's defined range >>> df = well.data(clip_to_property='PHIE')
- head(n=5, include=None, exclude=None)[source]
Return first n rows of well data.
Convenience method equivalent to well.data(**kwargs).head(n).
- Parameters:
- Returns:
First n rows of well data
- Return type:
pd.DataFrame
Examples
>>> well.head() >>> well.head(10) >>> well.head(include=['PHIE', 'SW']) >>> well.head(exclude=['QC_Flag'])
- to_las(include=None, exclude=None, store_labels=True, use_template=None)[source]
Convert well properties to a LasFile object.
This creates a LasFile object from the well’s properties. If properties have different depth grids, they will be automatically aligned using interpolation (resample method) to the first property’s depth grid. This ensures the exported LAS file has a consistent depth grid.
Note: Unlike the data() method which defaults to ‘match’ for safety, this method always resamples to ensure LAS export succeeds. If you need strict depth alignment checking, use well.merge(method=’match’) before calling to_las().
- Parameters:
include (list[str], optional) – List of property names to include. If None, includes all properties.
exclude (list[str], optional) – List of property names to exclude. If None, no properties are excluded.
store_labels (bool, default True) – If True, store discrete property label mappings in the ~Parameter section.
use_template (Union[bool, LasFile, None], optional) – If True, uses the primary (first) LAS file as template to preserve original metadata. If a LasFile object is provided, uses that specific file as template.
- Returns:
LasFile object ready for export or further manipulation
- Return type:
LasFile
- Raises:
WellError – If no properties to export or if use_template=True but no source LAS files exist
ValueError – If both include and exclude are specified
PropertyNotFoundError – If requested properties are not found
Examples
>>> # Create LasFile and export >>> las = well.to_las(include=['PHIE', 'SW', 'PERM']) >>> las.export('output.las')
>>> # Create LasFile with template metadata preserved >>> las = well.to_las(use_template=True) >>> las.export('updated.las')
>>> # Create LasFile, modify, then export >>> las = well.to_las() >>> # ... modify via las.set_data(df) if needed ... >>> las.export('output.las')
>>> # If strict depth alignment is required, merge first >>> well.merge(method='match', sources=['source1', 'source2']) >>> las = well.to_las()
- export_to_las(filepath, include=None, exclude=None, store_labels=True, null_value=-999.25, use_template=None)[source]
Export well data to LAS 2.0 format file.
This is a convenience method that calls to_las() and then exports. For more control, use to_las() to get a LasFile object first.
- Parameters:
filepath (Union[str, Path]) – Output LAS file path
include (list[str], optional) – List of property names to include. If None, includes all properties.
exclude (list[str], optional) – List of property names to exclude. If None, no properties are excluded.
store_labels (bool, default True) – If True, store discrete property label mappings in the ~Parameter section. The actual data values remain numeric (standard LAS format).
null_value (float, default -999.25) – Value to use for missing data in LAS file
use_template (Union[bool, LasFile, None], optional) – If True, uses the primary (first) LAS file as template to preserve original metadata. If a LasFile object is provided, uses that specific file as template. If None or False, creates a new LAS file without template (default). Template mode preserves: ~Version info, ~Well parameters, and ~Parameter entries not related to discrete labels.
- Raises:
WellError – If properties have different depth grids (call merge() first) If use_template=True but no source LAS files exist
ValueError – If both include and exclude are specified
- Return type:
None
Examples
>>> # Export all properties with labels stored in parameter section >>> well.export_to_las('output.las')
>>> # Export specific properties >>> well.export_to_las('output.las', include=['PHIE', 'SW', 'PERM'])
>>> # Export without storing discrete labels >>> well.export_to_las('output.las', store_labels=False)
>>> # Export using original LAS as template (preserves metadata) >>> well.export_to_las('updated.las', use_template=True)
>>> # For more control, use to_las() then export() >>> las = well.to_las(include=['PHIE', 'SW']) >>> # ... modify las if needed ... >>> las.export('output.las')
- delete_renamed_sources(folder_path)[source]
Delete old LAS files for sources that were renamed.
This method is called automatically by WellDataManager.save() after exporting sources to delete the old files with the previous names.
- Parameters:
folder_path (str or Path) – Folder path containing the source LAS files.
- Return type:
None
Examples
>>> well.rename_source("log", "wireline") >>> well.delete_renamed_sources("/path/to/project/well_36_7_5_B") Renamed source: 36_7-5_B_log.las -> 36_7-5_B_wireline.las (old file deleted)
- delete_marked_sources(folder_path)[source]
Delete LAS files for sources marked for deletion.
This method is called automatically by WellDataManager.save() to remove source files that were deleted using remove_source().
- Parameters:
folder_path (str or Path) – Folder path containing the source LAS files.
- Return type:
None
Examples
>>> well.remove_source("log") >>> well.delete_marked_sources("/path/to/project/well_36_7_5_B") Deleted source file: /path/to/project/well_36_7_5_B/36_7-5_B_log.las
- export_sources(folder_path)[source]
Export all sources as individual LAS files to a folder.
Each source is exported as a separate LAS file with the well name prefix. Filename format: {well_sanitized_name}_{source_name}.las
- Parameters:
folder_path (str or Path) – Folder path to export LAS files to. Will be created if it doesn’t exist.
- Return type:
None
Examples
>>> well = manager.well_36_7_5_B >>> well.export_sources("/path/to/output") # Creates (hyphens preserved in filenames): # /path/to/output/36_7-5_B_Log.las # /path/to/output/36_7-5_B_CorePor.las
- WellView(depth_range=None, tops=None, template=None, figsize=None, dpi=100, header_config=None)[source]
Create a well log display for this well.
Convenience method that creates a WellView object for visualization.
- Parameters:
depth_range (tuple[float, float], optional) – Depth interval to display [start_depth, end_depth]. Mutually exclusive with tops parameter.
tops (list[str], optional) – List of formation top names to display. The depth range will be calculated automatically from the minimum and maximum depths of these tops, with 5% padding added (minimum range of 50m). Mutually exclusive with depth_range parameter. Requires that formation tops have been loaded in the well or added to the template.
template (Union[Template, dict, str], optional) – Display template (Template object, dict config, or name from manager)
figsize (tuple[float, float], optional) – Figure size (width, height) in inches
dpi (int, default 100) – Figure resolution
header_config (dict, optional) – Header styling configuration. Supported keys: - header_box_top (float): Fixed top position of header boxes - header_title_spacing (float): Vertical space between log name and scale line - header_log_spacing (float): Vertical space allocated per log - header_top_padding (float): Padding above content in header box - header_bottom_padding (float): Padding below content in header box
- Returns:
Well log display object
- Return type:
Examples
>>> # Simple display with default template >>> view = well.WellView(depth_range=[2800, 3000]) >>> view.show() >>> >>> # Use stored template from manager >>> view = well.WellView(depth_range=[2800, 3000], template="reservoir") >>> view.show() >>> >>> # Use formation tops to auto-calculate range >>> view = well.WellView(tops=["Top_Brent", "Top_Statfjord"], template="reservoir") >>> view.show() >>> >>> # Use custom template >>> from logsuite.visualization import Template >>> template = Template("custom") >>> template.add_track( ... track_type="continuous", ... logs=[{"name": "GR", "color": "green"}] ... ) >>> view = well.WellView(depth_range=[2800, 3000], template=template) >>> view.save("well_log.png", dpi=300) >>> >>> # Customize header spacing >>> view = well.WellView( ... depth_range=[2800, 3000], ... template=template, ... header_config={"header_log_spacing": 0.04} ... )
Notes
Deprecated since version ``Well.WellView()``: violates the layered-dependency rule (
corecannot importvisualization). Construct directly instead:WellView(well, ...).
- Crossplot(x=None, y=None, layers=None, shape=None, color=None, size=None, colortemplate='viridis', color_range=None, size_range=(20, 200), title='Cross Plot', xlabel=None, ylabel=None, figsize=(10, 8), dpi=100, marker='o', marker_size=50, marker_alpha=0.7, edge_color='black', edge_width=0.5, x_log=False, y_log=False, grid=True, grid_alpha=0.3, depth_range=None, show_colorbar=True, show_legend=True)[source]
Create a beautiful crossplot for this well.
- Parameters:
x (str) – Name of property for x-axis
y (str) – Name of property for y-axis
shape (str, optional) – Property name for shape mapping. Default: None (single shape)
color (str, optional) – Property name for color mapping. Use “depth” to color by depth. Default: None (single color)
size (str, optional) – Property name for size mapping. Default: None (constant size)
colortemplate (str, optional) – Matplotlib colormap name (e.g., “viridis”, “plasma”, “coolwarm”) Default: “viridis”
color_range (tuple[float, float], optional) – Min and max values for color mapping. If None, uses data range. Default: None
size_range (tuple[float, float], optional) – Min and max marker sizes for size mapping. Default: (20, 200)
title (str, optional) – Plot title. Default: “Cross Plot”
xlabel (str, optional) – X-axis label. If None, uses property name.
ylabel (str, optional) – Y-axis label. If None, uses property name.
figsize (tuple[float, float], optional) – Figure size (width, height) in inches. Default: (10, 8)
dpi (int, optional) – Figure resolution. Default: 100
marker (str, optional) – Marker style. Default: “o”
marker_size (float, optional) – Base marker size. Default: 50
marker_alpha (float, optional) – Marker transparency (0-1). Default: 0.7
edge_color (str, optional) – Marker edge color. Default: “black”
edge_width (float, optional) – Marker edge width. Default: 0.5
x_log (bool, optional) – Use logarithmic scale for x-axis. Default: False
y_log (bool, optional) – Use logarithmic scale for y-axis. Default: False
grid (bool, optional) – Show grid. Default: True
grid_alpha (float, optional) – Grid transparency. Default: 0.3
depth_range (tuple[float, float], optional) – Depth range to filter data. Default: None (all depths)
show_colorbar (bool, optional) – Show colorbar when using color mapping. Default: True
show_legend (bool, optional) – Show legend when using shape mapping. Default: True
- Returns:
Crossplot visualization object
- Return type:
Examples
Basic crossplot:
>>> plot = well.Crossplot(x="RHOB", y="NPHI") >>> plot.show()
With color and size mapping:
>>> plot = well.Crossplot( ... x="PHIE_2025", ... y="NetSand_2025", ... color="depth", ... size="Sw_2025", ... colortemplate="viridis", ... color_range=[2000, 2500], ... title="Porosity vs Net Sand" ... ) >>> plot.show()
With regression analysis:
>>> plot = well.Crossplot(x="RHOB", y="NPHI") >>> plot.add_regression("linear") >>> plot.add_regression("polynomial", degree=2, line_color="blue") >>> plot.show() >>> print(plot.regressions["linear"].equation())
With logarithmic scales:
>>> plot = well.Crossplot(x="PERM", y="PHIE", x_log=True) >>> plot.show()
Notes
Deprecated since version ``Well.Crossplot()``: violates the layered-dependency rule (
corecannot importvisualization). Construct directly:Crossplot(well, x=..., y=...).
Property¶
- class logsuite.core.property.Property(name, depth=None, values=None, parent_well=None, unit='', prop_type='continuous', description='', null_value=-999.25, labels=None, colors=None, styles=None, thicknesses=None, source_las=None, source_name=None, original_name=None, lazy=False)[source]
Bases:
PropertyOperationsMixinSingle log property with depth-value pairs and filtering operations.
A Property can contain secondary properties (filters) that are aligned on the same depth grid. This enables chained filtering operations.
- Parameters:
name (str) – Property name (sanitized for Python attribute access)
depth (np.ndarray) – Depth values
values (np.ndarray) – Log values
parent_well (Well, optional) – Parent well for property lookup during filtering
unit (str, default '') – Unit string
prop_type (str, default 'continuous') – Either ‘continuous’ or ‘discrete’
description (str, default '') – Property description
null_value (float, default -999.25) – Value to treat as null/missing
labels (dict[int, str], optional) – Label mapping for discrete properties (e.g., {0: ‘NonNet’, 1: ‘Net’})
colors (dict[int, str], optional) – Color mapping for discrete properties (e.g., {0: ‘red’, 1: ‘green’})
styles (dict[int, str], optional) – Line style mapping for discrete properties (e.g., {0: ‘solid’, 1: ‘dashed’})
thicknesses (dict[int, float], optional) – Line thickness mapping for discrete properties (e.g., {0: 1.5, 1: 2.0})
original_name (str, optional) – Original property name with special characters (from LAS file)
source_las (LasFile | None)
source_name (str | None)
lazy (bool)
- name
Property name (sanitized for Python attribute access)
- Type:
- original_name
Original property name with special characters (from LAS file)
- Type:
- depth
Depth values
- Type:
np.ndarray
- values
Log values (nulls converted to np.nan)
- Type:
np.ndarray
- unit
Unit string
- Type:
- type
‘continuous’ or ‘discrete’
- Type:
- description
Description
- Type:
- parent_well
Parent well reference
- Type:
Well | None
- source_las
Source LAS file this property came from
- Type:
LasFile | None
- source
Source LAS file path (read-only property)
- Type:
str | None
- secondary_properties
List of aligned filter properties
- Type:
list[Property]
Examples
>>> phie = well.get_property('PHIE') >>> filtered = phie.filter('Zone').filter('NTG_Flag') >>> stats = filtered.sums_avg()
- property source: str | None
Get the source this property came from.
- Returns:
Source name - either a LAS file path or external DataFrame name (e.g., ‘path/to/original.las’ or ‘external_df’)
- Return type:
Optional[str]
Examples
>>> prop = well.get_property('PHIE') >>> print(prop.source) # 'path/to/original.las' or 'external_df'
- property type: str
Get the property type (‘continuous’ or ‘discrete’).
- Returns:
Property type
- Return type:
- property thicknesses: dict[int, float] | None
Get the line thickness mapping for discrete property values.
- property depth: ndarray
Get depth array with lazy loading support.
For lazy properties, data is loaded from source_las on first access. For derived properties, data is stored directly.
- Returns:
Depth values
- Return type:
np.ndarray
- property values: ndarray
Get values array with lazy loading support.
For lazy properties, data is loaded from source_las on first access. For derived properties, data is stored directly.
- Returns:
Property values (nulls converted to np.nan)
- Return type:
np.ndarray
- property MD: ndarray
Get Measured Depth array (standardized depth accessor).
This is an alias for .depth that provides a standardized naming convention for use in calculations and conditionals across all well log toolkit methods.
- Returns:
Measured depth values (same as .depth)
- Return type:
np.ndarray
Examples
>>> # Use in conditional calculations >>> shallow_phie = np.where(well.PHIE.MD < 2000, well.PHIE, np.nan) >>> >>> # Combine with Well for multi-well conditionals >>> well.calc = np.where((well.PHIE.MD > 2000) & (well.PHIE.MD < 3000), ... well.PHIE * 2, ... well.PHIE)
- property Well: ndarray
Get Well name as an array aligned with depth.
Returns an array of well names, one for each depth point. This enables conditional logic based on well name in calculations and filtering operations.
- Returns:
Array of well names (str), one per depth point
- Return type:
np.ndarray
Examples
>>> # Use in multi-well calculations >>> plot_data = crossplot.data # DataFrame with 'well' column >>> >>> # In property calculations (returns array of well names) >>> well_names = well.PHIE.Well >>> # array(['36/7-5 ST2', '36/7-5 ST2', ...]) >>> >>> # Conditional based on well >>> result = np.where(well.PHIE.Well == "36/7-5", well.PHIE * 1.1, well.PHIE)
- property is_filtered: bool
Check if this property is a filtered copy with modified depth grid.
- Returns:
True if this is a filtered copy, False if original
- Return type:
- filter_info()[source]
Get information about filtering applied to this property.
- Returns:
Dictionary with filtering metadata: - is_filtered: bool - whether this is a filtered copy - filters: list[str] - names of applied filters - original_sample_count: int - samples before filtering - current_sample_count: int - samples after filtering - boundary_samples_inserted: int - synthetic samples added at boundaries
- Return type:
- resample(target_depth)[source]
Resample property to a new depth grid using appropriate interpolation.
This method creates a new Property object with values interpolated to match the target depth grid. This is required when combining properties with different sampling rates.
- Parameters:
target_depth (np.ndarray or Property) – Target depth grid to resample to. Can be: - numpy array of depth values - Property object (will use its .depth attribute)
- Returns:
New property with values interpolated to target depth grid
- Return type:
Property
Notes
Uses linear interpolation for continuous data
Uses forward-fill (previous) for discrete data - geological zones extend from their top/boundary until the next boundary is encountered. For example, “Cerisa West top” at 2929.93m remains active until “Cerisa West SST 1 top” at 2955.10m is intercepted.
Values outside the original depth range are set to NaN
NaN values in original data are excluded from interpolation
Examples
>>> # Resample to another property's depth grid >>> phie_resampled = well.CorePHIE.resample(well.PHIE.depth) >>> result = well.PHIE + phie_resampled
>>> # Or pass the property directly >>> phie_resampled = well.CorePHIE.resample(well.PHIE) >>> result = well.PHIE + phie_resampled
>>> # Resample to regular 0.5m grid >>> target = np.arange(2800, 3500, 0.5) >>> phie_regular = well.PHIE.resample(target)
See also
filterAdd discrete property as grouping dimension (auto-resamples).
- apply(func, name=None)[source]
Apply a function to values, returning a new Property.
- Parameters:
func (callable) – Function that takes a numpy array and returns a numpy array of the same shape.
name (str, optional) – Name for the new property. Defaults to ‘{name}_applied’.
- Returns:
New property with transformed values on the same depth grid.
- Return type:
Property
Examples
>>> log_perm = well.PERM.apply(np.log10, name='LOG_PERM') >>> normalized = well.PHIE.apply(lambda v: (v - v.min()) / (v.max() - v.min()))
- histogram(bins=50, weighted=True)[source]
Compute histogram of property values.
- Parameters:
- Returns:
(counts, bin_edges) — same format as
numpy.histogram.- Return type:
tuple[np.ndarray, np.ndarray]
Examples
>>> counts, edges = well.PHIE.histogram(bins=20) >>> counts, edges = well.PHIE.histogram(weighted=False)
- min()[source]
Return minimum value (ignoring NaN).
- Returns:
Minimum value, or NaN if all values are NaN
- Return type:
Examples
>>> prop.min() 0.05
- max()[source]
Return maximum value (ignoring NaN).
- Returns:
Maximum value, or NaN if all values are NaN
- Return type:
Examples
>>> prop.max() 0.35
- mean(weighted=True)[source]
Compute mean value.
- Parameters:
weighted (bool, default True) – If True, compute depth-weighted mean using interval thicknesses (default for well logs). If False, compute simple arithmetic mean (for sampled data like core points).
- Returns:
Mean value, or NaN if no valid data
- Return type:
Examples
>>> prop.mean() # Depth-weighted by default 0.185 >>> prop.mean(weighted=False) # Arithmetic mean 0.182
- std(weighted=True)[source]
Compute standard deviation.
- Parameters:
weighted (bool, default True) – If True, compute depth-weighted standard deviation (default for well logs). If False, compute simple arithmetic standard deviation (for sampled data).
- Returns:
Standard deviation, or NaN if insufficient valid data
- Return type:
Examples
>>> prop.std() # Depth-weighted by default 0.042 >>> prop.std(weighted=False) # Arithmetic std 0.044
- percentile(p, weighted=True)[source]
Compute percentile value.
- Parameters:
p (float) – Percentile to compute (0-100 scale). For example: - p=10 gives 10th percentile (P10) - p=50 gives median (P50) - p=90 gives 90th percentile (P90)
weighted (bool, default True) – If True, compute depth-weighted percentile (default for well logs). If False, compute simple arithmetic percentile (for sampled data).
- Returns:
Percentile value, or NaN if no valid data
- Return type:
Examples
>>> prop.percentile(50) # Depth-weighted median by default 0.18 >>> prop.percentile(90) # Depth-weighted P90 0.24 >>> prop.percentile(10, weighted=False) # Arithmetic P10 0.09
- median(weighted=True)[source]
Compute median value (50th percentile).
- Parameters:
weighted (bool, default True) – If True, compute depth-weighted median (default for well logs). If False, compute simple arithmetic median (for sampled data).
- Returns:
Median value, or NaN if no valid data
- Return type:
Examples
>>> prop.median() # Depth-weighted median by default 0.18 >>> prop.median(weighted=False) # Arithmetic median 0.175
- mode(weighted=True, bins=50)[source]
Compute mode (most frequent value).
For continuous data, values are binned before finding the mode. For discrete data, bins parameter is ignored.
- Parameters:
- Returns:
Mode value, or NaN if no valid data
- Return type:
Examples
>>> prop.mode() # Depth-weighted mode 0.18 >>> prop.mode(weighted=False) # Arithmetic mode 0.17 >>> discrete_prop.mode() # For discrete: most common value 1.0
- get_value(target_depth)[source]
Get the value at the closest depth point.
Finds the nearest sample point to the target depth and returns both the actual depth and the corresponding value.
- Parameters:
target_depth (float) – Target depth to query
- Returns:
Dictionary with keys: - ‘depth’: Actual depth of the nearest sample - ‘value’: Property value at that depth - ‘distance’: Absolute distance from target to actual depth
- Return type:
Examples
>>> # Get porosity at approximately 2850m >>> result = well.PHIE.get_value(2850.0) >>> print(result) {'depth': 2850.5, 'value': 0.18, 'distance': 0.5}
>>> # Access the values >>> actual_depth = result['depth'] >>> phie_value = result['value']
>>> # For discrete properties with labels >>> zone = well.Zone.get_value(2850.0) >>> print(zone) {'depth': 2850.0, 'value': 0.0, 'distance': 0.0} >>> # To get the label, use the labels dict >>> if well.Zone.labels: ... label = well.Zone.labels.get(int(zone['value']))
- filter(property_name, insert_boundaries=None, source=None)[source]
Add a discrete property from parent well as a filter dimension.
Returns new Property with the discrete property values interpolated to the current depth grid. The depth grid remains unchanged - only the discrete values are added as a secondary property.
- Parameters:
property_name (str) – Name of discrete property in parent well
insert_boundaries (bool, optional) – If True, insert synthetic samples at discrete property boundaries. Default is True for continuous properties, False for sampled properties. Set to False for sampled data (core plugs) to preserve original measurements.
source (str, optional) – Source name to get filter property from. If None, searches across all sources. Use this when the filter property exists in multiple sources to avoid ambiguity.
- Returns:
New property instance with secondary property added (same depth grid)
- Return type:
Property
- Raises:
PropertyNotFoundError – If parent_well is None or property doesn’t exist
PropertyTypeError – If property is not discrete type
See also
sums_avgCompute statistics grouped by filter values.
filter_intervalsFilter by depth intervals instead of discrete property.
resampleResample to a different depth grid before filtering.
Examples
>>> filtered = well.phie.filter("Zone").filter("NTG_Flag") >>> stats = filtered.sums_avg() >>> # Shape remains the same - only discrete values are added >>> original.shape # (29, 2) >>> filtered.data().shape # (29, 3) - added Zone column
>>> # For sampled data (core plugs), boundaries are not inserted by default >>> core_phie.type = 'sampled' >>> filtered = core_phie.filter("Zone") # No boundary insertion
>>> # When filter property is ambiguous, specify source >>> log_phie = well.get_property("PHIE", source="log") >>> filtered = log_phie.filter("Zone", source="log") # Use Zone from log source
- filter_intervals(intervals, name='Custom_Intervals', insert_boundaries=None, save=None)[source]
Filter by custom depth intervals defined as top/base pairs.
Each interval is processed independently, allowing overlapping intervals where the same depths can be counted in multiple zones.
- Parameters:
intervals (list[dict] | dict[str, list[dict]] | str) –
Interval definitions. Can be:
list[dict]: Direct list of intervals for the current well
dict[str, list[dict]]: Well-specific intervals keyed by well name. Current well must be included or raises error.
str: Name of a previously saved filter to use
name (str, default "Custom_Intervals") – Name for the filter property (used in output labels)
insert_boundaries (bool, optional) – If True, insert synthetic samples at interval boundaries. Default is True for continuous properties, False for sampled properties.
save (str, optional) – If provided, save the intervals to the well(s) under this name. Overwrites any existing filter with the same name.
- Returns:
New property instance with custom intervals as filter dimension
- Return type:
Property
Examples
>>> # Filter by custom zones >>> intervals = [ ... {"name": "Zone_A", "top": 2500, "base": 2600}, ... {"name": "Zone_B", "top": 2600, "base": 2750}, ... ] >>> filtered = well.PHIE.filter_intervals(intervals) >>> filtered.sums_avg()
>>> # Save intervals for reuse >>> well.PHIE.filter_intervals(intervals, save="Reservoir_Zones") >>> # Later, use saved filter by name >>> well.PHIE.filter_intervals("Reservoir_Zones").sums_avg()
>>> # Save different intervals for multiple wells >>> manager.well_A.PHIE.filter_intervals({ ... "well_A": intervals_a, ... "well_B": intervals_b ... }, save="My_Zones") >>> # Now both wells have "My_Zones" saved
>>> # Overlapping intervals - each calculated independently >>> intervals = [ ... {"name": "Full_Reservoir", "top": 2500, "base": 2800}, ... {"name": "Upper_Only", "top": 2500, "base": 2650} ... ] >>> # Depths 2500-2650 will be counted in BOTH zones
Notes
Intervals can overlap or have gaps. Depths outside all intervals are excluded from statistics. Overlapping intervals are calculated independently - the same depths can contribute to multiple zones.
- sums_avg(weighted=None, arithmetic=None, precision=6)[source]
Compute hierarchical statistics grouped by all secondary properties.
- Parameters:
weighted (bool, optional) – Include depth-weighted statistics. Default: True for continuous/discrete, False for sampled
arithmetic (bool, optional) – Include arithmetic (unweighted) statistics. Default: False for continuous/discrete, True for sampled
precision (int, default 6) – Number of decimal places for rounding numeric results
- Returns:
Nested dictionary with statistics for each group combination. Structure includes: - mean: weighted and/or arithmetic mean - sum: weighted and/or arithmetic sum - std_dev: weighted and/or arithmetic standard deviation - percentile: {p10, p50, p90} values - range: {min, max} value range - depth_range: {min, max} depth range within the zone - samples: number of valid samples - thickness: depth interval for this group - gross_thickness: total depth interval (all groups) - thickness_fraction: thickness / gross_thickness - calculation: ‘weighted’, ‘arithmetic’, or ‘both’
- Return type:
Examples
>>> # Simple statistics (no filters) >>> phie = well.get_property('PHIE') >>> stats = phie.sums_avg() >>> # {'mean': 0.18, 'sum': 45.2, 'samples': 251, ...}
>>> # With arithmetic stats >>> stats = phie.sums_avg(arithmetic=True) >>> # {'mean': {'weighted': 0.18, 'arithmetic': 0.17}, ...}
>>> # Grouped statistics >>> filtered = phie.filter('Zone').filter('NTG_Flag') >>> stats = filtered.sums_avg() >>> # {'Zone_1': {'NTG_Flag_0': {...}, 'NTG_Flag_1': {...}}, ...}
>>> # Sampled data uses arithmetic by default >>> core_phie.type = 'sampled' >>> stats = core_phie.sums_avg() # arithmetic=True, weighted=False
>>> # Custom precision >>> stats = phie.sums_avg(precision=3) >>> # {'mean': 0.180, 'sum': 45.200, ...}
See also
filterAdd a discrete property as a grouping dimension.
meanCompute single mean value (without full statistics).
stdCompute single standard deviation.
- discrete_summary(precision=6, skip=None)[source]
Compute summary statistics for discrete/categorical properties.
This method is designed for discrete logs (like facies, lithology flags, or net/gross indicators) where categorical statistics are more meaningful than continuous statistics like mean or standard deviation.
- Parameters:
- Returns:
Nested dictionary with statistics for each discrete value. If secondary properties (filters) exist, the structure is hierarchical.
For each discrete value, includes (unless skipped): - code: Numeric code for this category - count: Number of samples with this value - thickness: Total depth interval (meters) for this category - fraction: Proportion of total thickness (0-1) - depth_range: {min, max} depth extent
- Return type:
Examples
>>> # Simple discrete summary (no filters) >>> facies = well.get_property('Facies') >>> stats = facies.discrete_summary() >>> # {'Sand': {'code': 1, 'count': 150, 'thickness': 25.5, 'fraction': 0.45, ...}, >>> # 'Shale': {'code': 2, 'count': 180, 'thickness': 30.8, 'fraction': 0.55, ...}}
>>> # Skip certain fields >>> stats = facies.discrete_summary(skip=['code', 'count']) >>> # {'Sand': {'thickness': 25.5, 'fraction': 0.45}, ...}
>>> # Grouped by zones >>> filtered = facies.filter('Well_Tops') >>> stats = filtered.discrete_summary() >>> # {'Zone_A': {'Sand': {...}, 'Shale': {...}}, >>> # 'Zone_B': {'Sand': {...}, 'Shale': {...}}}
Notes
For continuous properties, use sums_avg() instead.
- data(include=None, exclude=None, discrete_labels=True, clip_edges=True, clip_to_property=None)[source]
Export property and secondary properties as DataFrame.
- Parameters:
include (str or list[str], optional) – Secondary property name(s) to include. If None, includes all. Main property is always included. Can be a single string or a list of strings.
exclude (str or list[str], optional) – Secondary property name(s) to exclude. If both include and exclude are specified, exclude overrides (removes from include list). Can be a single string or a list of strings.
discrete_labels (bool, default True) – If True, apply label mappings to discrete properties
clip_edges (bool, default True) – If True, remove rows at the start and end where all data columns (excluding DEPT) contain NaN values. This trims the DataFrame to the range where actual data exists.
clip_to_property (str, optional) – Clip output to the defined range of this specific property. If specified, overrides clip_edges behavior.
- Returns:
DataFrame with DEPT, main property, and secondary properties
- Return type:
pd.DataFrame
Examples
>>> filtered = well.phie.filter('Zone').filter('NTG_Flag') >>> df = filtered.data() >>> print(df.head())
>>> # Include only specific secondary properties >>> df = filtered.data(include=['Zone'])
>>> # Include single secondary property (string or list) >>> df = filtered.data(include='Zone') # Same as include=['Zone']
>>> # Exclude specific secondary properties >>> df = filtered.data(exclude=['NTG_Flag'])
>>> # Exclude single secondary property >>> df = filtered.data(exclude='NTG_Flag')
>>> # Clip to property range >>> df = filtered.data(clip_to_property='Zone')
- head(n=5, include=None, exclude=None)[source]
Return first n rows of property data.
Convenience method equivalent to property.data(**kwargs).head(n).
- Parameters:
- Returns:
First n rows of property data
- Return type:
pd.DataFrame
Examples
>>> well.PHIE.head() >>> well.PHIE.filter('Zone').head(10) >>> well.PHIE.filter('Zone').filter('NTG').head(include=['Zone'])
- export_to_las(filepath, well_name=None, store_labels=True, null_value=-999.25)[source]
Export property to LAS 2.0 format file.
- Parameters:
filepath (Union[str, Path]) – Output LAS file path
well_name (str, optional) – Well name for the LAS file. If not provided and parent_well exists, uses parent well’s name. Otherwise uses ‘UNKNOWN’.
store_labels (bool, default True) – If True, store discrete property label mappings in the ~Parameter section. The actual data values remain numeric (standard LAS format).
null_value (float, default -999.25) – Value to use for missing data in LAS file
- Return type:
None
Examples
>>> prop = well.get_property('PHIE') >>> prop.export_to_las('phie_only.las')
>>> # Export with secondary properties (filters) and labels >>> filtered = well.phie.filter('Zone').filter('NTG_Flag') >>> filtered.export_to_las('filtered_phie.las', well_name='12/3-2 B')