mirror of
https://github.com/OPM/ResInsight.git
synced 2026-11-02 09:00:14 -06: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 .exception import RipsError as RipsError # noqa: E402
|
||||||
from .surface import RegularSurface as RegularSurface # noqa: E402
|
from .surface import RegularSurface as RegularSurface # noqa: E402
|
||||||
from . import well_events as well_events # noqa: F401, E402
|
from . import well_events as well_events # noqa: F401, E402
|
||||||
|
from . import category_mapping as category_mapping # noqa: F401, E402
|
||||||
|
|
||||||
__all__: List[str] = []
|
__all__: List[str] = []
|
||||||
for key in class_dict(): # noqa: F405
|
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.*]
|
[mypy-rips.surface.*]
|
||||||
disable_error_code = attr-defined, no-any-return
|
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_grpc
|
||||||
import KeyValueStore_pb2
|
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
|
from typing import Optional, List
|
||||||
|
|
||||||
@@ -167,6 +174,17 @@ def summary_cases(self):
|
|||||||
return self.descendants(SummaryCase)
|
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)
|
@add_method(Project)
|
||||||
def views(self):
|
def views(self):
|
||||||
"""Get a list of views belonging to a project"""
|
"""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