WellDataManager¶
- class logsuite.manager.data_manager.WellDataManager(project=None)[source]
Bases:
objectGlobal orchestrator for multi-well analysis.
Manages multiple wells, each containing multiple properties. Provides attribute-based well access for clean API.
- Parameters:
project (str | Path | None)
Examples
>>> manager = WellDataManager() >>> manager.load_las("well1.las").load_las("well2.las") >>> well = manager.well_12_3_2_B >>> stats = well.phie.filter('Zone').sums_avg()
>>> # Load project directly on initialization >>> manager = WellDataManager("Cerisa Project") >>> print(manager.wells) # All wells from project
- filter(*, wells=None, where=None)[source]
Return a filtered view over a subset of wells and/or property values.
The view exposes the same property-proxy and well-attribute access as the manager. Without
where, statistics and.data()calls on the view operate over the well subset. Withwhere,.data()outputs are post-filtered to rows matching the allowed values, while statistical methods raiseNotImplementedError(use.data()and compute externally, or.filter("Zone")for grouped stats).- Parameters:
wells (str or list of str, optional) – Well names (original, sanitized, or manager dict key) to include.
Nonereturns a view containing every well.where (dict, optional) – Mapping of column name -> allowed value(s). Special key
"well"selects wells (intersected withwells); other keys post-filter.data()outputs.
- Returns:
Read-only view. Immutable — chain
ManagerView.filter()to narrow further.- Return type:
ManagerView
Examples
>>> view = manager.filter(wells=["Well_A", "Well_B"]) >>> view.PHIE.mean() {'well_Well_A': 0.182, 'well_Well_B': 0.205} >>> sub = manager.filter(where={"Zone": "Reservoir"}) >>> sub.PHIE.data().head()
- properties(property_names)[source]
Create a multi-property proxy for computing statistics across multiple properties.
This allows computing statistics for multiple properties at once, with property-specific stats (mean, median, etc.) nested under property names and common stats (depth_range, samples, thickness, etc.) at the group level.
- Parameters:
property_names (list[str]) – List of property names to include in statistics
- Returns:
Proxy that supports filter(), filter_intervals(), and sums_avg()
- Return type:
_ManagerMultiPropertyProxy
Examples
>>> # Compute stats for multiple properties grouped by facies >>> manager.properties(['PHIE', 'PERM']).filter('Facies').sums_avg() >>> # Returns: >>> # { >>> # "well_A": { >>> # "Sand": { >>> # "PHIE": {"mean": 0.18, "median": 0.17, ...}, >>> # "PERM": {"mean": 150, "median": 120, ...}, >>> # "depth_range": {...}, >>> # "samples": 387, >>> # "thickness": 29.4, >>> # ... >>> # } >>> # } >>> # }
>>> # With custom intervals >>> manager.properties(['PHIE', 'PERM']).filter('Facies').filter_intervals("Zones").sums_avg() >>> # Returns: >>> # { >>> # "well_A": { >>> # "Zone_1": { >>> # "Sand": { >>> # "PHIE": {"mean": 0.18, ...}, >>> # "PERM": {"mean": 150, ...}, >>> # "depth_range": {...}, >>> # ... >>> # } >>> # } >>> # } >>> # }
- load_las(filepath, path=None, sampled=False, combine=None, source_name=None, silent=False)[source]
Load LAS file(s), auto-create well if needed.
- Parameters:
filepath (Union[str, Path, list[Union[str, Path]]]) – Path to LAS file or list of paths to LAS files. 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 the LAS file(s) as ‘sampled’ type. Use this for core plug data or other point measurements.
combine (str, optional) – When loading multiple files, combine files from the same well 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) Files are automatically grouped by well name. If 4 files from 2 wells are loaded, 2 combined sources are created (one per well).
source_name (str, optional) – Name for combined source when combine is specified. If not specified, uses ‘combined_match’, ‘combined_resample’, or ‘combined_concat’. When files span multiple wells, the well name is prepended automatically.
silent (bool, default False) – If True, suppress debug output showing which sources were loaded. Useful when loading many files programmatically.
- Returns:
Self for method chaining
- Return type:
WellDataManager
- Raises:
LasFileError – If LAS file has no well name
Examples
>>> manager = WellDataManager() >>> manager.load_las("well1.las") >>> manager.load_las(["well2.las", "well3.las"])
>>> # Load core plug data >>> manager.load_las("core_data.las", sampled=True)
>>> # Load multiple files from same directory >>> manager.load_las( ... ["file1.las", "file2.las", "file3.las"], ... path="data/well_logs" ... )
>>> # Load and combine files (automatically groups by well) >>> manager.load_las( ... ["36_7-5_B_CorePerm.las", "36_7-5_B_CorePor.las", ... "36_7-4_CorePerm.las", "36_7-4_CorePor.las"], ... path="data/", ... combine="match", ... source_name="CorePlugs" ... ) Loaded sources: - Well 36/7-5 B: CorePlugs (2 files combined) - Well 36/7-4: CorePlugs (2 files combined)
See also
saveSave loaded wells to project directory.
loadLoad a previously saved project.
load_propertiesLoad properties from a DataFrame.
- load_tops(df, property_name='Well_Tops', source_name='Imported_Tops', well_col='Well identifier (Well name)', well_name=None, discrete_col='Surface', depth_col='MD', x_col='X', y_col='Y', z_col='Z', include_coordinates=False)[source]
Load formation tops data from a DataFrame into wells.
Supports three loading patterns: 1. Multi-well: well_col specified, groups DataFrame by well column 2. Single-well named: well_col=None, well_name specified, all data to that well 3. Single-well default: well_col=None, well_name=None, all data to generic “Well”
Automatically creates wells if they don’t exist, converts discrete values to discrete integers with labels, and adds the data as a source to each well.
- Parameters:
df (pd.DataFrame) – DataFrame containing tops data with columns for well name (optional), discrete values, and depth
property_name (str, default "Well_Tops") – Name for the discrete property (will be sanitized)
source_name (str, default "Imported_Tops") – Name for this source group (will be sanitized)
well_col (str, optional, default "Well identifier (Well name)") – Column name containing well names. Set to None for single-well loading.
well_name (str, optional) – Well name to use when well_col=None. If both well_col and well_name are None, defaults to generic “Well”.
discrete_col (str, default "Surface") – Column name containing discrete values (e.g., formation/surface names)
depth_col (str, default "MD") – Column name containing measured depth values
x_col (str, optional, default "X") – Column name for X coordinate (only used if include_coordinates=True)
y_col (str, optional, default "Y") – Column name for Y coordinate (only used if include_coordinates=True)
z_col (str, optional, default "Z") – Column name for Z coordinate (only used if include_coordinates=True)
include_coordinates (bool, default False) – If True, include X, Y, Z coordinates as additional properties
- Returns:
Self for method chaining
- Return type:
WellDataManager
Examples
>>> # Pattern 1: Multi-well loading (groups by well column) >>> import pandas as pd >>> df = pd.DataFrame({ ... 'Well identifier (Well name)': ['12/3-4 A', '12/3-4 A', '12/3-4 B'], ... 'Surface': ['Top_Brent', 'Top_Statfjord', 'Top_Brent'], ... 'MD': [2850.0, 3100.0, 2860.0] ... }) >>> manager = WellDataManager() >>> manager.load_tops(df) # Uses default well_col >>> >>> # Pattern 2: Single-well with explicit name (no well column needed) >>> df_single = pd.DataFrame({ ... 'Surface': ['Top_Brent', 'Top_Statfjord', 'Top_Cook'], ... 'MD': [2850.0, 3100.0, 3400.0] ... }) >>> manager.load_tops( ... df_single, ... well_col=None, ... well_name='12/3-4 A' # Load all tops to this well ... ) >>> >>> # Pattern 3: Single-well with default name "Well" (simplest) >>> manager.load_tops(df_single, well_col=None) >>> >>> # Access tops >>> well = manager.well_12_3_4_A >>> print(well.sources) # ['Imported_Tops'] >>> well.Imported_Tops.Well_Tops # Discrete property with formation names
- load_properties(df, source_name='external_df', well_col='Well', well_name=None, depth_col='DEPT', unit_mappings=None, type_mappings=None, label_mappings=None, resample_method=None)[source]
Load properties from a DataFrame into wells.
Supports three loading patterns: 1. Multi-well: well_col specified, groups DataFrame by well column 2. Single-well named: well_col=None, well_name specified, all data to that well 3. Single-well default: well_col=None, well_name=None, all data to generic “Well”
IMPORTANT: Depth grids must be compatible. If incompatible, you must specify a resampling method explicitly. This prevents accidental data loss.
- Parameters:
df (pd.DataFrame) – DataFrame containing properties with columns for well name (optional), depth, and properties
source_name (str, default "external_df") – Name for this source group (will be sanitized)
well_col (str, optional, default "Well") – Column name containing well names. Set to None for single-well loading.
well_name (str, optional) – Well name to use when well_col=None. If both well_col and well_name are None, defaults to generic “Well”.
depth_col (str, default "DEPT") – Column name containing measured depth values
unit_mappings (dict[str, str], optional) – Mapping of property names to units (e.g., {‘PHIE’: ‘v/v’, ‘SW’: ‘v/v’})
type_mappings (dict[str, str], optional) – Mapping of property names to types: ‘continuous’, ‘discrete’, or ‘sampled’ (e.g., {‘Zone’: ‘discrete’, ‘PHIE’: ‘continuous’})
label_mappings (dict[str, dict[int, str]], optional) – Label mappings for discrete properties (e.g., {‘Zone’: {0: ‘Top_Brent’, 1: ‘Top_Statfjord’}})
resample_method (str, optional) – Method to use if depth grids are incompatible: - None (default): Raises error if depths 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 Warning: Resampling sampled data (core plugs) may cause data loss.
- Returns:
Self for method chaining
- Return type:
WellDataManager
- Raises:
ValueError – If required columns are missing or if depths are incompatible and resample_method=None
Examples
>>> # Pattern 1: Multi-well loading (groups by well column) >>> import pandas as pd >>> df = pd.DataFrame({ ... 'Well': ['12/3-4 A', '12/3-4 A', '12/3-4 B'], ... 'DEPT': [2850.0, 2851.0, 2850.5], ... 'CorePHIE': [0.20, 0.22, 0.19], ... 'CorePERM': [150, 200, 120] ... }) >>> manager.load_properties( ... df, ... source_name='CoreData', ... well_col='Well', # Groups by this column ... unit_mappings={'CorePHIE': 'v/v', 'CorePERM': 'mD'}, ... type_mappings={'CorePHIE': 'sampled', 'CorePERM': 'sampled'} ... ) ✓ Loaded 2 properties into well '12/3-4 A' from source 'CoreData' ✓ Loaded 2 properties into well '12/3-4 B' from source 'CoreData'
>>> # Pattern 2: Single-well with explicit name (no well column needed) >>> df_single = pd.DataFrame({ ... 'DEPT': [2850.0, 2851.0, 2852.0], ... 'PHIE': [0.20, 0.22, 0.19] ... }) >>> manager.load_properties( ... df_single, ... well_col=None, ... well_name='12/3-4 A', # Load all data to this well ... source_name='Interpreted' ... ) ✓ Loaded 1 properties into well '12/3-4 A' from source 'Interpreted'
>>> # Pattern 3: Single-well with default name "Well" (simplest) >>> manager.load_properties( ... df_single, ... well_col=None, # No well column ... source_name='Analysis' ... ) ✓ Loaded 1 properties into well 'Well' from source 'Analysis'
>>> # Load with incompatible depths - requires explicit resampling >>> manager.load_properties( ... df, ... source_name='Interpreted', ... resample_method='linear' # Explicitly allow resampling ... )
>>> # Access the data >>> well = manager.well_12_3_4_A >>> print(well.sources) # ['Petrophysics', 'CoreData'] >>> well.CoreData.CorePHIE # Sampled property
- save(path=None)[source]
Save all wells and their sources to a project folder structure.
Creates a folder for each well (well_xxx format) and exports all sources as LAS files with well name prefix. Also saves templates to a templates/ folder at the project root. Also renames LAS files for any sources that were renamed using rename_source(), and deletes LAS files for any sources that were removed using remove_source(). If path is not provided, uses the path from the last load() call.
- Parameters:
path (Union[str, Path], optional) – Root directory path for the project. If None, uses path from last load().
- Raises:
ValueError – If path is None and no project has been loaded
- Return type:
None
Examples
>>> manager = WellDataManager() >>> manager.load_las(["well1.las", "well2.las"]) >>> manager.save("my_project") # Creates (hyphens preserved in filenames): # my_project/ # well_36_7_5_A/ # 36_7-5_A_Log.las # 36_7-5_A_CorePor.las # well_36_7_5_B/ # 36_7-5_B_Log.las # templates/ # reservoir.json # qc.json >>> >>> # After load(), can save without path >>> manager = WellDataManager() >>> manager.load("my_project") >>> # ... make changes ... >>> manager.save() # Saves to "my_project" >>> >>> # Rename and remove sources, then save >>> manager.well_36_7_5_A.rename_source("Log", "Wireline") >>> manager.well_36_7_5_A.remove_source("CorePor") >>> manager.save() # Renames 36_7-5_A_Log.las to 36_7-5_A_Wireline.las and deletes 36_7-5_A_CorePor.las
- load(path)[source]
Load all wells and templates from a project folder structure.
Automatically discovers and loads: - All LAS files from well folders (well_* format) - All template JSON files from templates/ folder
Stores the project path for subsequent save() calls. Clears any existing wells and templates before loading.
- Parameters:
path (Union[str, Path]) – Root directory path of the project
- Returns:
Self for method chaining
- Return type:
WellDataManager
Examples
>>> manager = WellDataManager() >>> manager.load("my_project") >>> print(manager.wells) # All wells from project >>> print(manager.list_templates()) # All templates from project >>> # ... make changes ... >>> manager.save() # Saves back to "my_project"
>>> # Load clears existing data >>> manager.load("other_project") # Replaces current wells and templates
- add_well(well_name)[source]
Create or get existing well.
- Parameters:
well_name (str) – Original well name
- Returns:
New or existing well instance
- Return type:
Well
Examples
>>> well = manager.add_well("12/3-2 B") >>> well.load_las("log1.las")
- property wells: list[str]
List of sanitized well names.
Examples
>>> manager.wells ['well_12_3_2_B', 'well_12_3_2_A']
- property saved_intervals: dict[str, list[str]]
List saved interval names for all wells.
- Returns:
Dictionary mapping well names to their saved interval names
- Return type:
Examples
>>> manager.saved_intervals {'well_A': ['Reservoir_Zones', 'Slump_Zones'], 'well_B': ['Reservoir_Zones']}
- get_intervals(name)[source]
Get saved filter intervals by name from all wells that have them.
- Parameters:
name (str) – Name of the saved filter intervals
- Returns:
Dictionary mapping well names to their interval definitions
- Return type:
- Raises:
KeyError – If no wells have intervals with the given name
Examples
>>> manager.get_intervals("Slump_Zones") {'well_A': [{'name': 'Zone_A', 'top': 2500, 'base': 2650}], 'well_B': [{'name': 'Zone_A', 'top': 2600, 'base': 2750}]}
- get_well(name)[source]
Get well by original or sanitized name.
- Parameters:
name (str) – Either original name (“36/7-5 A”), sanitized (“36_7_5_A”), or with
well_prefix (“well_36_7_5_A”)- Returns:
The requested well
- Return type:
Well
- Raises:
KeyError – If well not found
Examples
>>> well = manager.get_well("36/7-5 A") >>> well = manager.get_well("36_7_5_A") >>> well = manager.get_well("well_36_7_5_A")
- remove_well(name)[source]
Remove a well from the manager.
- Parameters:
name (str) – Well name (original, sanitized, or with
well_prefix).- Raises:
KeyError – If well not found.
- Return type:
None
Examples
>>> manager.remove_well("36/7-5 A") >>> manager.remove_well("well_36_7_5_A")
- add_template(template)[source]
Store a template using its built-in name.
- Parameters:
template (Template) – Template object (uses
template.nameas the key).- Return type:
None
Examples
>>> from logsuite import Template >>> >>> template = Template("reservoir") >>> template.add_track(track_type="continuous", logs=[...]) >>> manager.add_template(template) # Stored as "reservoir" >>> >>> # Use in WellView >>> view = well.WellView(template="reservoir")
- set_template(name, template)[source]
Store a template with a custom name (overrides template.name).
Use
add_template()for the simpler case where the template’s built-in name should be used.- Parameters:
- Return type:
None
Examples
>>> # Store with a different name than the template's built-in name >>> template = Template("reservoir") >>> manager.set_template("reservoir_v2", template)
- get_template(name)[source]
Get a stored template by name.
- Parameters:
name (str) – Template name
- Returns:
The requested template
- Return type:
- Raises:
KeyError – If template not found
Examples
>>> template = manager.get_template("reservoir") >>> print(template.tracks)
- list_templates()[source]
List all stored template names.
Examples
>>> manager.list_templates() ['reservoir', 'qc', 'basic']
- remove_template(name)[source]
Remove a stored template.
- Parameters:
name (str) – Template name to remove.
- Raises:
KeyError – If template not found.
- Return type:
None
Examples
>>> manager.remove_template("old_template")
- Crossplot(x=None, y=None, wells=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, show_regression_legend=True, show_regression_equation=True, show_regression_r2=True, regression=None, regression_by_color=None, regression_by_group=None)[source]
Create a multi-well crossplot.
- Parameters:
x (str) – Name of property for x-axis
y (str) – Name of property for y-axis
wells (list[str], optional) – List of well names to include. If None, uses all wells. Default: None (all wells)
shape (str, optional) – Property name for shape mapping. Use “well” to map shapes by well name. Default: “well” (each well gets different marker)
color (str, optional) – Property name for color mapping. Use “depth” to color by depth. Default: None (color by well if shape=”well”, else 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) – Base marker style (used when shape mapping is not “well”). 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. Default: True
show_regression_legend (bool, optional) – Show separate legend for regression lines in lower right corner. Default: True
show_regression_equation (bool, optional) – Include regression equation in regression legend labels. Default: True
show_regression_r2 (bool, optional) – Include R² value in regression legend labels. Default: True
regression (str or dict, optional) – Regression type to apply to all data points. Can be a string (e.g., “linear”) or dict with keys: type, line_color, line_width, line_style, line_alpha, x_range. Default: None
regression_by_color (str or dict, optional) – Regression type to apply separately for each color group in the plot. Creates separate regression lines based on what determines colors in the visualization: explicit color mapping if specified, otherwise shape groups (e.g., wells when shape=’well’). Accepts string or dict format. Default: None
regression_by_group (str or dict, optional) – Regression type to apply separately for each well. Creates separate regression lines for each well. Accepts string or dict format. Default: None
- Returns:
Crossplot visualization object
- Return type:
Examples
Multi-well crossplot with each well as different marker:
>>> plot = manager.Crossplot(x="RHOB", y="NPHI", shape="well") >>> plot.show()
Specific wells with color and size mapping:
>>> plot = manager.Crossplot( ... x="PHIE_2025", ... y="NetSand_2025", ... wells=["Well_A", "Well_B"], ... color="depth", ... size="Sw_2025", ... colortemplate="viridis", ... color_range=[2000, 2500], ... title="Multi-Well Cross Plot" ... ) >>> plot.show()
With regression analysis:
>>> plot = manager.Crossplot(x="RHOB", y="NPHI") >>> plot.add_regression("linear", line_color="red") >>> plot.add_regression("polynomial", degree=2, line_color="blue") >>> plot.show()
Notes
Deprecated since version ``WellDataManager.Crossplot()``: violates the layered-dependency rule (
managercannot importvisualization). Construct directly:Crossplot(manager.filter(wells=[...]), x=..., y=...)orCrossplot(manager, ...)for all wells.
- validate()[source]
Check data integrity across all wells.
Returns a dictionary mapping well names to lists of issue descriptions. An empty dict means no issues were found.
- Returns:
Well names mapped to lists of issue strings. Empty if all OK.
- Return type:
Examples
>>> issues = manager.validate() >>> if issues: ... for well, problems in issues.items(): ... print(f"{well}: {problems}")