WellDataManager

class logsuite.manager.data_manager.WellDataManager(project=None)[source]

Bases: object

Global 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)

wells

List of sanitized well names

Type:

list[str]

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. With where, .data() outputs are post-filtered to rows matching the allowed values, while statistical methods raise NotImplementedError (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. None returns a view containing every well.

  • where (dict, optional) – Mapping of column name -> allowed value(s). Special key "well" selects wells (intersected with wells); 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

save

Save loaded wells to project directory.

load

Load a previously saved project.

load_properties

Load 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.

Returns:

List of well names (sanitized for attribute access)

Return type:

list[str]

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:

dict[str, list[str]]

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:

dict[str, list[dict]]

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.name as 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:
  • name (str) – Template name for reference (overrides template.name).

  • template (Union[Template, dict]) – Template object or dictionary configuration.

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:

Template

Raises:

KeyError – If template not found

Examples

>>> template = manager.get_template("reservoir")
>>> print(template.tracks)
list_templates()[source]

List all stored template names.

Returns:

List of template names

Return type:

list[str]

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

  • layers (dict[str, list[str]] | None)

Returns:

Crossplot visualization object

Return type:

Crossplot

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 (manager cannot import visualization). Construct directly: Crossplot(manager.filter(wells=[...]), x=..., y=...) or Crossplot(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:

dict[str, list[str]]

Examples

>>> issues = manager.validate()
>>> if issues:
...     for well, problems in issues.items():
...         print(f"{well}: {problems}")