mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-25 22:50:25 -05:00
#13806 Python: Add category mapping API for discrete properties
Introduce set_discrete_property_category_names on Case to bind integer result values to text labels and optional colors. Internally it creates a custom ColorLegend, appends one item per value (with an auto-assigned palette color when the caller does not provide one) and registers the legend as the default for the (case, resultName) pair. Also add Project.color_legend_collection() as a dedicated accessor for the project's ColorLegendCollection, pytest coverage for legend creation and the default-legend-for-result binding, and two PythonExamples demonstrating the end-to-end discrete property + category label flow. #13806 Python: Silence mypy warnings for category_mapping module
This commit is contained in:
1 parent
29fc0aeaf5
commit
03409aed70
7 files changed
+265
-1
No files matched your search
+28
@@ -0,0 +1,28 @@
|
||||
######################################################################
|
||||
# This script creates a discrete (integer) property for all active
|
||||
# cells in the first case in the project.
|
||||
#
|
||||
# Passing data_type="INTEGER" flags the property as discrete regardless
|
||||
# of the property name (previously this required the name to end with
|
||||
# "NUM").
|
||||
######################################################################
|
||||
import rips
|
||||
|
||||
resinsight = rips.Instance.find()
|
||||
|
||||
case = resinsight.project.case(case_id=0)
|
||||
|
||||
# Derive a discrete region id from a continuous property.
|
||||
poro = case.active_cell_property("STATIC_NATIVE", "PORO", 0)
|
||||
region_ids = [int(value * 100) % 4 for value in poro]
|
||||
|
||||
print("Applying discrete values to active cells")
|
||||
case.set_active_cell_property(
|
||||
region_ids, "GENERATED", "MY_REGION", 0, data_type="INTEGER"
|
||||
)
|
||||
|
||||
# Bind integer values to text labels so the legend shows names instead of numbers.
|
||||
case.set_discrete_property_category_names(
|
||||
property_name="MY_REGION",
|
||||
value_names={0: "Sand", 1: "Shale", 2: "Coal", 3: "Limestone"},
|
||||
)
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
######################################################################
|
||||
# This script creates a discrete (integer) grid property for all grid
|
||||
# cells in the first case in the project.
|
||||
#
|
||||
# A discrete property is visualized with a category legend, in contrast
|
||||
# to the continuous legend used for floating point properties. Passing
|
||||
# data_type="INTEGER" flags the property as discrete regardless of the
|
||||
# property name (previously this required the name to end with "NUM").
|
||||
######################################################################
|
||||
import rips
|
||||
|
||||
resinsight = rips.Instance.find()
|
||||
|
||||
case = resinsight.project.case(case_id=0)
|
||||
grid = case.grid()
|
||||
grid_cell_count = grid.cell_count()
|
||||
print("total cell count : " + str(grid_cell_count))
|
||||
|
||||
# Assign a simple region id based on IJK index.
|
||||
values = []
|
||||
for i in range(0, grid_cell_count):
|
||||
values.append(i % 4)
|
||||
|
||||
print("Applying discrete values to main grid")
|
||||
case.set_grid_property(values, "STATIC_NATIVE", "MY_REGION", 0, data_type="INTEGER")
|
||||
|
||||
# Bind integer values to text labels so the legend shows names instead of numbers.
|
||||
# Colors are optional — values without a color get an auto-assigned palette color.
|
||||
case.set_discrete_property_category_names(
|
||||
property_name="MY_REGION",
|
||||
value_names={0: "Sand", 1: "Shale", 2: "Coal", 3: "Limestone"},
|
||||
value_colors={0: "#e6c878", 1: "#646464"},
|
||||
)
|
||||
@@ -27,6 +27,7 @@ from .simulation_well import SimulationWell as SimulationWell # noqa: E402
|
||||
from .exception import RipsError as RipsError # noqa: E402
|
||||
from .surface import RegularSurface as RegularSurface # noqa: E402
|
||||
from . import well_events as well_events # noqa: F401, E402
|
||||
from . import category_mapping as category_mapping # noqa: F401, E402
|
||||
|
||||
__all__: List[str] = []
|
||||
for key in class_dict(): # noqa: F405
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
Category name/color binding for discrete (INTEGER) grid cell results.
|
||||
|
||||
Provides a convenience API on Case for labeling integer result values with
|
||||
human-readable names and optional colors. Internally, this reuses the existing
|
||||
ColorLegend infrastructure: a new custom ColorLegend is created and registered
|
||||
as the default legend for the (case, resultName) pair. When the property is
|
||||
shown in a 3D view, the legend's item names are used as the category labels.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from .pdmobject import add_method
|
||||
from .project import Project
|
||||
from .resinsight_classes import Case, ColorLegend
|
||||
|
||||
|
||||
# Palette of distinct colors used when the caller does not supply colors.
|
||||
# Mirrors RiaColorTables::categoryColors() in the C++ code.
|
||||
_DEFAULT_PALETTE: List[str] = [
|
||||
"#803e75",
|
||||
"#d41c84",
|
||||
"#f6768e",
|
||||
"#c10020",
|
||||
"#7f180d",
|
||||
"#f13a13",
|
||||
"#ff7a5c",
|
||||
"#817066",
|
||||
"#ff6800",
|
||||
"#593315",
|
||||
"#ff8e00",
|
||||
"#cea262",
|
||||
"#f4c800",
|
||||
"#93aa00",
|
||||
"#3b5417",
|
||||
"#007d34",
|
||||
"#367d7b",
|
||||
"#00538a",
|
||||
"#a6bdd7",
|
||||
"#2e4ce0",
|
||||
]
|
||||
|
||||
|
||||
@add_method(Case)
|
||||
def set_discrete_property_category_names(
|
||||
self: Case,
|
||||
property_name: str,
|
||||
value_names: Dict[int, str],
|
||||
value_colors: Optional[Dict[int, str]] = None,
|
||||
legend_name: Optional[str] = None,
|
||||
) -> Optional[ColorLegend]:
|
||||
"""Bind integer values of a discrete grid property to text labels.
|
||||
|
||||
Use this after uploading a discrete property via set_grid_property(...,
|
||||
data_type="INTEGER") or set_active_cell_property(..., data_type="INTEGER")
|
||||
to display text labels instead of raw integers in the 3D view legend.
|
||||
|
||||
Arguments:
|
||||
property_name (str): Name of the discrete property result.
|
||||
value_names (Dict[int, str]): Mapping from integer value to label.
|
||||
An empty dict removes any existing mapping for this property.
|
||||
value_colors (Optional[Dict[int, str]]): Optional per-value colors as
|
||||
strings accepted by QColor (e.g. "red", "#ff8800"). Values without
|
||||
a color entry get an auto-assigned palette color.
|
||||
legend_name (Optional[str]): Optional name for the color legend.
|
||||
Defaults to the property name.
|
||||
|
||||
Returns:
|
||||
The created ColorLegend, or None if value_names was empty.
|
||||
"""
|
||||
project = self.ancestor(Project)
|
||||
if project is None:
|
||||
raise RuntimeError("Could not find parent project")
|
||||
|
||||
collection = project.color_legend_collection()
|
||||
if collection is None:
|
||||
raise RuntimeError("Could not find ColorLegendCollection in project")
|
||||
|
||||
collection.delete_color_legend(case=self, result_name=property_name)
|
||||
|
||||
if not value_names:
|
||||
return None
|
||||
|
||||
name = legend_name if legend_name else property_name
|
||||
legend = collection.create_color_legend(name=name)
|
||||
|
||||
colors = value_colors or {}
|
||||
palette_index = 0
|
||||
for value, label in value_names.items():
|
||||
color = colors.get(value)
|
||||
if color is None:
|
||||
color = _DEFAULT_PALETTE[palette_index % len(_DEFAULT_PALETTE)]
|
||||
palette_index += 1
|
||||
legend.add_color_legend_item(
|
||||
category_value=value,
|
||||
category_name=label,
|
||||
color=color,
|
||||
)
|
||||
|
||||
collection.set_default_color_legend_for_result(
|
||||
case=self,
|
||||
result_name=property_name,
|
||||
color_legend=legend,
|
||||
)
|
||||
|
||||
return legend
|
||||
@@ -72,3 +72,6 @@ disable_error_code = no-any-return
|
||||
|
||||
[mypy-rips.surface.*]
|
||||
disable_error_code = attr-defined, no-any-return
|
||||
|
||||
[mypy-rips.category_mapping.*]
|
||||
disable_error_code = attr-defined, no-any-return
|
||||
@@ -19,7 +19,14 @@ import Project_pb2_grpc
|
||||
import KeyValueStore_pb2_grpc
|
||||
import KeyValueStore_pb2
|
||||
|
||||
from .resinsight_classes import Project, PlotWindow, WellPath, SummaryCase, Reservoir
|
||||
from .resinsight_classes import (
|
||||
ColorLegendCollection,
|
||||
PlotWindow,
|
||||
Project,
|
||||
Reservoir,
|
||||
SummaryCase,
|
||||
WellPath,
|
||||
)
|
||||
|
||||
from typing import Optional, List
|
||||
|
||||
@@ -167,6 +174,17 @@ def summary_cases(self):
|
||||
return self.descendants(SummaryCase)
|
||||
|
||||
|
||||
@add_method(Project)
|
||||
def color_legend_collection(self: Project) -> Optional[ColorLegendCollection]:
|
||||
"""Get the project's color legend collection
|
||||
|
||||
Returns:
|
||||
:class:`rips.generated.generated_classes.ColorLegendCollection`
|
||||
"""
|
||||
collections = self.descendants(ColorLegendCollection)
|
||||
return collections[0] if collections else None
|
||||
|
||||
|
||||
@add_method(Project)
|
||||
def views(self):
|
||||
"""Get a list of views belonging to a project"""
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(1, os.path.join(sys.path[0], "../../"))
|
||||
import rips
|
||||
|
||||
import dataroot
|
||||
|
||||
|
||||
def test_color_legend_collection_accessor(rips_instance, initialize_test):
|
||||
"""The project exposes the ColorLegendCollection via a dedicated method."""
|
||||
collection = rips_instance.project.color_legend_collection()
|
||||
assert collection is not None
|
||||
assert isinstance(collection, rips.ColorLegendCollection)
|
||||
|
||||
|
||||
def test_create_color_legend_and_items(rips_instance, initialize_test):
|
||||
collection = rips_instance.project.color_legend_collection()
|
||||
|
||||
legend = collection.create_color_legend(name="My Legend")
|
||||
assert legend is not None
|
||||
assert legend.color_legend_name == "My Legend"
|
||||
|
||||
red = legend.add_color_legend_item(
|
||||
category_value=1, category_name="one", color="#ff0000"
|
||||
)
|
||||
green = legend.add_color_legend_item(
|
||||
category_value=2, category_name="two", color="#00ff00"
|
||||
)
|
||||
blue = legend.add_color_legend_item(
|
||||
category_value=3, category_name="three", color="#0000ff"
|
||||
)
|
||||
|
||||
assert red.category_value == 1
|
||||
assert red.category_name == "one"
|
||||
assert red.color == "#ff0000"
|
||||
|
||||
assert green.category_value == 2
|
||||
assert green.category_name == "two"
|
||||
assert green.color == "#00ff00"
|
||||
|
||||
assert blue.category_value == 3
|
||||
assert blue.category_name == "three"
|
||||
assert blue.color == "#0000ff"
|
||||
|
||||
|
||||
def test_color_legend_item_default_color(rips_instance, initialize_test):
|
||||
"""When created via the command, an item reports the color it was given."""
|
||||
collection = rips_instance.project.color_legend_collection()
|
||||
legend = collection.create_color_legend(name="Named Color")
|
||||
|
||||
item = legend.add_color_legend_item(
|
||||
category_value=0, category_name="orange", color="orange"
|
||||
)
|
||||
# QColor normalizes named colors to their hex representation.
|
||||
assert item.color == "#ffa500"
|
||||
|
||||
|
||||
def test_set_and_delete_default_color_legend_for_result(rips_instance, initialize_test):
|
||||
case_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC/TEST10K_FLT_LGR_NNC.EGRID"
|
||||
case = rips_instance.project.load_case(path=case_path)
|
||||
assert case is not None
|
||||
|
||||
collection = rips_instance.project.color_legend_collection()
|
||||
legend = collection.create_color_legend(name="Result Legend")
|
||||
legend.add_color_legend_item(category_value=0, category_name="a", color="#112233")
|
||||
legend.add_color_legend_item(category_value=1, category_name="b", color="#445566")
|
||||
|
||||
# Should not raise for a known result name.
|
||||
collection.set_default_color_legend_for_result(
|
||||
case=case, result_name="SOIL", color_legend=legend
|
||||
)
|
||||
|
||||
# Deleting the binding should also succeed.
|
||||
collection.delete_color_legend(case=case, result_name="SOIL")
|
||||
Reference in new issue
Block a user