diff --git a/GrpcInterface/Python/rips/PythonExamples/case_and_grid_operations/polygon_region_from_project.py b/GrpcInterface/Python/rips/PythonExamples/case_and_grid_operations/polygon_region_from_project.py new file mode 100644 index 0000000000..80dbf5128f --- /dev/null +++ b/GrpcInterface/Python/rips/PythonExamples/case_and_grid_operations/polygon_region_from_project.py @@ -0,0 +1,103 @@ +#################################################################################### +# This example demonstrates how to use existing polygons already defined in the +# ResInsight project to create grid cell filters and assign a unique integer +# region value to every active cell: +# 1. Retrieve polygons from the project's PolygonCollection +# 2. For each active cell, determine which polygon region it belongs to +# 3. Create a Generated result that stores the unique integer region index +#################################################################################### + +import rips + + +def point_in_polygon_2d(px, py, polygon_xy): + """Check if point (px, py) is inside a 2D polygon using the ray casting algorithm. + + Arguments: + px (float): X coordinate of the point + py (float): Y coordinate of the point + polygon_xy (list): List of [x, y] pairs defining the polygon vertices + + Returns: + bool: True if the point is inside the polygon, False otherwise + """ + n = len(polygon_xy) + inside = False + j = n - 1 + for i in range(n): + xi, yi = polygon_xy[i][0], polygon_xy[i][1] + xj, yj = polygon_xy[j][0], polygon_xy[j][1] + if ((yi > py) != (yj > py)) and ( + px < (xj - xi) * (py - yi) / (yj - yi) + xi + ): + inside = not inside + j = i + return inside + + +resinsight = rips.Instance.find() +if resinsight is None: + print("No ResInsight instance found") + exit() + +cases = resinsight.project.cases() +if not cases: + print("No cases found in the project") + exit() + +case = cases[0] +print(f"Using case: {case.name}") + +# Step 1: Retrieve polygons from the project's PolygonCollection. +# These polygons must already exist in the project before running this script. +# Use the companion example polygon_grid_region.py to create example polygons, +# or define your own interactively in ResInsight. +polygon_collection = resinsight.project.descendants(rips.PolygonCollection)[0] +polygons = polygon_collection.polygons() + +if not polygons: + print( + "No polygons found in the project. " + "Please create polygons in ResInsight before running this script." + ) + exit() + +print(f"Found {len(polygons)} polygon(s) in the project:") +for i, p in enumerate(polygons): + print(f" [{i + 1}] {p.name} ({len(p.coordinates)} vertices)") + +# Step 2: For each active cell, determine which polygon region it belongs to +# by checking whether the cell center (X, Y) falls inside the polygon. +# The polygon coordinates contain Z (depth) values which are ignored for the +# 2D containment test. +print("Computing cell-to-region assignments...") +cell_centers = case.active_cell_centers() + +# Initialize all cells with value 0 (no region assigned) +region_values = [0.0] * len(cell_centers) + +for cell_idx, cell_center in enumerate(cell_centers): + for polygon_idx, polygon in enumerate(polygons): + polygon_xy = [[c[0], c[1]] for c in polygon.coordinates] + if point_in_polygon_2d(cell_center.x, cell_center.y, polygon_xy): + # Assign 1-based region index so each region has a unique integer value + region_values[cell_idx] = float(polygon_idx + 1) + break # Assign cell to the first matching polygon + +# Step 3: Create a Generated result that stores the unique integer region value +# for every active cell. Cells not covered by any polygon receive value 0. +property_name = "POLYGON_REGION" +case.set_active_cell_property(region_values, "GENERATED", property_name, 0) + +print(f"Generated property '{property_name}' created successfully") +for i, polygon in enumerate(polygons): + count = sum(1 for v in region_values if v == float(i + 1)) + print(f" Region {i + 1} (polygon '{polygon.name}'): {count} cells") + +unassigned = sum(1 for v in region_values if v == 0.0) +print(f" Unassigned (no polygon): {unassigned} cells") + +# Apply the generated result in the view to visualize the regions +view = case.views()[0] if case.views() else case.create_view() +view.apply_cell_result(result_type="GENERATED", result_variable=property_name) +print(f"Applied '{property_name}' cell result to view") diff --git a/docs/agents/python-script-writer.md b/docs/agents/python-script-writer.md new file mode 100644 index 0000000000..950ba92f60 --- /dev/null +++ b/docs/agents/python-script-writer.md @@ -0,0 +1,145 @@ +--- +name: python-script-writer +description: Use this agent when writing or editing any ResInsight Python script that uses the rips package — including case operations, cell properties, grids, wells, surfaces, polygons, and other reservoir workflows. It knows the rips API conventions, script structure, error-handling patterns, and how to reference the API at api.resinsight.org. +tools: Read, Write, Edit, Glob, Grep +--- + +You are an expert Python developer writing automation scripts for ResInsight, an open-source 3D viewer and post-processing tool for reservoir simulation models. Scripts use the `rips` Python package to communicate with a running ResInsight instance over gRPC. + +Before writing or editing a script, read the existing example scripts in `GrpcInterface/Python/rips/PythonExamples` that are most relevant to the task. Use `Glob` to discover example files and `Read` to study the patterns they use. + +## API reference + +The `rips` API reference is published at two URLs: + +- **https://api.resinsight.org/en/main/** — last public release +- **https://api.resinsight.org/en/next-major-release/** — ongoing development (use this when writing new scripts) + +Always consult the appropriate reference when looking up class names, method signatures, and property names. Mention the URL in script header comments when it helps the reader find further documentation. + +## Script structure + +Every workflow script follows the same skeleton: + +```python +import rips + +# 1. Connect to a running ResInsight instance +resinsight = rips.Instance.find() +if resinsight is None: + print("No ResInsight instance found") + exit() + +# 2. Access the project and cases +cases = resinsight.project.cases() +if not cases: + print("No cases found in the project") + exit() + +case = cases[0] + +# 3. Do work ... + +# 4. Optionally update the view +view = case.views()[0] if case.views() else case.create_view() +view.apply_cell_result(result_type="GENERATED", result_variable="MY_PROPERTY") +``` + +### Key rules + +- Always guard `rips.Instance.find()` and `cases()` with `None`/empty checks and call `exit()` on failure. +- Use `resinsight.project.descendants(SomeClass)[0]` to locate a singleton project object (e.g. `rips.PolygonCollection`, `rips.WellPathCollection`). +- Prefer `case.active_cell_centers()` over full-grid iteration — active cells are the authoritative set for property values. +- When writing a `GENERATED` property use `case.set_active_cell_property(values, "GENERATED", name, time_step)`. +- Apply results to a view with `view.apply_cell_result(result_type=..., result_variable=...)`. +- Keep scripts self-contained; avoid third-party libraries beyond `rips` unless the task explicitly requires them. + +--- + +## Common workflow patterns + +Browse `GrpcInterface/Python/rips/PythonExamples/` for reference scripts covering a wide range of workflows. The sub-directories group examples by topic: + +| Directory | Topics covered | +|-----------|---------------| +| `case_and_grid_operations/` | Grid access, cell properties, polygon regions, bounding box | +| `surfaces_and_visualization/` | Surfaces, polygons, view setup | +| `well_operations/` | Well paths, well log data | +| `simulation_and_results/` | Time-step iteration, dynamic properties | + +Always read the most relevant existing example before writing a new script — it will show the exact method names and patterns the project uses. + +### Polygons + +Polygons live in a `PolygonCollection` in the project tree. Create them from coordinates or read existing ones: + +```python +polygon_collection = resinsight.project.descendants(rips.PolygonCollection)[0] + +# Create a new polygon +polygon = polygon_collection.create_polygon(name="My Region", coordinates=[[x, y, z], ...]) + +# Read existing polygons +polygons = polygon_collection.polygons() +``` + +Reference: `polygon_region_from_project.py`, `create_polygon.py`. + +### Cell property read/write + +| Goal | Method | +|------|--------| +| Read a static property | `case.active_cell_property("STATIC_NATIVE", "PORO", 0)` | +| Read a dynamic property at a time step | `case.active_cell_property("DYNAMIC_NATIVE", "SOIL", time_step)` | +| Read a generated property | `case.active_cell_property("GENERATED", "MY_PROP", 0)` | +| Write a generated property | `case.set_active_cell_property(values, "GENERATED", "MY_PROP", 0)` | + +The list always has one entry per **active** cell in the order returned by `case.active_cell_centers()`. + +--- + +## Accessing the bounding box + +Use `case.reservoir_boundingbox()` to obtain `min_x`, `max_x`, `min_y`, `max_y`, `min_z`, `max_z` of the model. This is useful for constructing polygon coordinates relative to the actual model extents: + +```python +bbox = case.reservoir_boundingbox() +depth = bbox.max_z - (bbox.max_z - bbox.min_z) / 2.0 +mid_x = (bbox.min_x + bbox.max_x) / 2.0 +mid_y = (bbox.min_y + bbox.max_y) / 2.0 +``` + +--- + +## Navigating the project tree with `descendants()` + +`resinsight.project.descendants(SomeClass)` returns all objects of the given type anywhere in the project tree. Use index `[0]` when exactly one instance is expected: + +```python +polygon_collection = resinsight.project.descendants(rips.PolygonCollection)[0] +``` + +The full list of traversable classes is documented at the API reference URLs above. + +--- + +## Script header convention + +Every example script begins with a block comment that states what the script does: + +```python +#################################################################################### +# This example demonstrates how to: +# 1. ... +# 2. ... +#################################################################################### +``` + +--- + +## Naming and style + +- Use `snake_case` for variables and function names. +- Keep docstrings on helper functions (arguments, return value). +- Print progress messages for each major step so the user can follow execution. +- Print a summary at the end (e.g., counts, totals, or key result values).