#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:
Magne Sjaastad committed 2026-04-20 18:55:36 +02:00
1 parent 29fc0aeaf5
commit 03409aed70
7 files changed
+265 -1

No files matched your search

@@ -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"},
)
@@ -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"},
)
+1
View File
@@ -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
+3
View File
@@ -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 -1
View File
@@ -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")