Files
ResInsight/docs/agents/core.md
T
Magne Sjaastad b1f093fa7d Add issue policy and script to close stale issues
* Add issue policy and script to close stale issues
* Update README with current features, input formats and requirements
2026-09-29 13:19:12 +02:00

427 lines
17 KiB
Markdown

# Core Agent Guidelines
This document provides core guidelines that all AI agents working on ResInsight should follow.
## Project Overview
ResInsight is a 3D visualization and post-processing tool for reservoir simulation data built with a modular architecture.
## Architecture Overview
### Core Framework Structure
- **Visualization Framework (Fwk/VizFwk/)**: Core 3D rendering using Qt/OpenGL
- LibCore, LibGeometry, LibRender, LibViewing, LibGuiQt
- **Application Framework (Fwk/AppFwk/)**: UI and data management framework
- Project Data Model (PDM) system for serialization and UI generation
- Command framework for undo/redo operations
- User interface components built on Qt
- **Application Code (ApplicationLibCode/)**: Domain-specific functionality
- Commands/, ResultStatisticsCache/, GeoMech/ modules
- Integration with Eclipse, ODB, and other reservoir simulation formats
### Key Dependencies
- **Qt6**: Application framework and UI (Core, Gui, OpenGL, Network, Widgets)
- **Third-party libraries**: resdata (ERT), OPM libraries, qwt plotting, OpenVDS seismic data
- **vcpkg**: Package manager for most dependencies
### Data Sources Supported
- Eclipse binary files (*.GRID, *.EGRID with *.INIT, *.XNNN, *.UNRST)
- ABAQUS ODB files (when RESINSIGHT_ODB_API_DIR is configured)
- OpenVDS seismic data
- Various well and completion data formats
### Build Configuration Options
Key CMake options:
- `RESINSIGHT_ENABLE_GRPC`: Enable gRPC scripting framework
- `RESINSIGHT_USE_ODB_API`: Enable ABAQUS ODB support
- `RESINSIGHT_ENABLE_UNITY_BUILD`: Experimental build speedup
- `RESINSIGHT_ENABLE_HDF5`: Enable HDF5 library support
### Python Integration
ResInsight includes Python integration via gRPC when `RESINSIGHT_ENABLE_GRPC=ON`:
- Python API modules in GrpcInterface/Python/
- Two-way data exchange capabilities
- Script automation support
## Development Notes
- **Version**: Current version defined in `ResInsightVersion.cmake` (2025.04.4-dev)
- **Git Workflow**: Main development on `dev` branch, releases are defined by tags (e.g. `v2026.09.1`)
- **Dependencies**: Extensive third-party integration via git submodules in `ThirdParty/`
- **Build Output**: Goes to `CMAKE_RUNTIME_OUTPUT_DIRECTORY` (CMAKE_CURRENT_BINARY_DIR)
- **Qt Integration**: Uses Qt's resource system (.qrc files) for UI resources
- **Entry Point**: Main executable and supporting tools built from `ApplicationExeCode/`
- **Testing**: Cross-platform automated testing on RHEL, Ubuntu, and Windows 11
- **Build Configuration**: Key option `RESINSIGHT_TREAT_WARNINGS_AS_ERRORS` available in CMake presets
## Common File Locations
- Main CMake configuration: `/workspace/CMakeLists.txt`
- Application entry point: `ApplicationExeCode/RiaMain.cpp`
- Version info: `ResInsightVersion.cmake`
- Third-party dependencies: `ThirdParty/` with individual CMakeLists.txt files
- Python API: `GrpcInterface/Python/`
## Git
### Commit Conventions
- When creating a git commit for a feature request use the issue number at the start of the title, e.g "#12773 Python: Add API for creating valve templates"
- When creating a commit use git conventions
- Always run python formatting/check on changed files before commits
- When you change a CMake file and `cmakelang` is available, run `cmake-format -c cmake/cmake-format.py -i <file>` before committing (see `docs/agents/coding-style.md`)
### PR Conventions
- Do not include a test plan in the PR description
## Making PDM Objects Scriptable for Python GRPC Interface
To make a PDM (Project Data Model) object available in the Python GRPC interface, you need to make it "scriptable":
### 1. Convert PDM Object to Scriptable
**In the header file (.h):**
- No changes needed to the class declaration
- The object inherits from `caf::PdmObject` as usual
**In the source file (.cpp):**
```cpp
// Add required includes
#include "cafPdmFieldScriptingCapability.h"
#include "cafPdmObjectScriptingCapability.h"
// Change object initialization
CAF_PDM_InitScriptableObject("Display Name", ":/Icon.png", "", "ScriptKeyword");
```
### 2. Convert PDM Fields to Scriptable
**Change field initialization from:**
```cpp
CAF_PDM_InitField(&m_fieldName, "FieldName", defaultValue, "Display Name");
```
**To:**
```cpp
CAF_PDM_InitScriptableField(&m_fieldName, "ScriptFieldName", defaultValue, "Display Name");
```
### 3. Field Naming Conventions for Python
- Use camelCase for script field names (e.g., "StartMd", "EndMd")
- Python generator converts to snake_case automatically (start_md, end_md)
- Avoid abbreviations like "MD" - use "Md" instead
### 4. Class Keyword Requirements
The class keyword used in `CAF_PDM_SOURCE_INIT` must be unique and descriptive:
```cpp
CAF_PDM_SOURCE_INIT(RimClassName, "ClassKeyword");
```
### 5. Build and Python Generation
After making objects scriptable:
1. Build the project: `ninja -C build`
2. Python classes are automatically generated in `build/Python/rips/generated/generated_classes.py`
3. The scriptable class will appear as a Python class with appropriate attributes
### 6. GRPC Method Integration
To add methods that return scriptable objects:
```cpp
// In GRPC interface method
QString RimcClassName_method::classKeywordReturnedType() const
{
return RimTargetClass::classKeywordStatic();
}
```
### Example: SegmentInterval
**Before (not scriptable):**
```cpp
CAF_PDM_InitObject("Segment Interval", ":/Icon.png");
CAF_PDM_InitField(&m_startMD, "StartMD", 0.0, "Start MD");
```
**After (scriptable):**
```cpp
CAF_PDM_InitScriptableObject("Segment Interval", ":/Icon.png", "", "SegmentInterval");
CAF_PDM_InitScriptableField(&m_startMD, "StartMd", 0.0, "Start MD");
```
This enables the class to be used from Python:
```python
segments = well_path.segment_collection()
interval = segments.add_segment_interval(start_md=100, end_md=200)
print(f"Start: {interval.start_md}, End: {interval.end_md}")
```
## Scriptable Object Methods (Rimc) and the Legacy Command File Interface (Ricf)
ResInsight has two command systems. New functionality goes into **object methods**; the legacy
command file interface is a thin forwarding layer kept for backward compatibility.
### Object methods (`ApplicationLibCode/ProjectDataModelCommands/Rimc*.{h,cpp}`)
- Subclass `caf::PdmVoidObjectMethod` (no return), `caf::PdmObjectCreationMethod` (returns a new
persistent object) or `caf::PdmObjectMethod` (returns a transient container such as
`RimcDataContainerString`).
- Register with `CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimTargetClass, RimTargetClass_methodName, "methodName" )`.
Use **camelCase** keywords; the Python generator converts them to snake_case.
- Name the class `<TargetClass>_<methodName>`, e.g. `RimCase_replaceGrid`, `RimEclipseView_exportVisibleCells`.
- Parameters are `CAF_PDM_InitScriptableField` fields. Pass **objects, not ids**: use
`caf::PdmPtrField<T*>` / `caf::PdmPtrArrayField<T*>` (generated as `T` / `List[T]` in Python).
Enum parameters use `caf::PdmField<caf::AppEnum<E>>` and are generated as Python `StrEnum`s; pin the
Python class name with `caf::PdmScriptEnumNameRegistry::registerName<E>( "Name" )`.
- Set the `whatsThis` text (third variadic argument of `CAF_PDM_InitScriptableField`) - it becomes the
argument description in the generated Python docstring.
- Paths are **explicit parameters**. Object methods must not read `RicfCommandFileExecutor` state
(`getExportPath()`, `getLastProjectPath()`) or depend on `CommandFileInterface/` at all.
- Return errors with `std::unexpected( QString )`; do not log-and-return-nullptr.
- Provide typed public setters (`setExportFolder( const QString& )` ...) so C++ callers (in particular
the Ricf forwarders) can populate the method without going through gRPC.
- Guard GUI-only code with `RiaGuiApplication::isRunning()`; methods must work in headless mode.
- Child objects (`PdmChildField`) are **not** supported as method parameters over gRPC.
### Legacy command file commands (`ApplicationLibCode/CommandFileInterface/Ricf*.{h,cpp}`)
`RicfCommandObject` subclasses are the command-file syntax (`--commandFile`) and the handlers for the
legacy `Commands.proto` gRPC service. Their keywords, fields and `Commands.proto` **must not change**.
A Ricf `execute()` should only:
1. Resolve ids to objects with the helpers in `RicfCommandForwarding.h`
(`findCase`, `findView`, `findCaseGroup`, `findFractureTemplate`, `findWellLogPlot`, ...).
2. Resolve executor state (export folders, last project path) into explicit values.
3. Construct the Rimc method, call its setters, call `execute()`, and convert the result with
`RicfForwarding::toScriptResponse()` / `errorResponse()`.
4. Loop when the legacy command accepts multiple ids and the object method is single-object.
```cpp
caf::PdmScriptResponse RicfExportMsw::execute()
{
const QString commandName = classKeyword();
auto rimCase = RicfForwarding::findCase( m_caseId() );
if ( !rimCase ) return RicfForwarding::errorResponse( rimCase.error(), commandName );
auto* eclipseCase = dynamic_cast<RimEclipseCase*>( rimCase.value() );
if ( !eclipseCase ) return RicfForwarding::errorResponse( "Not an Eclipse case", commandName );
QString exportFolder = RicfCommandFileExecutor::instance()->getExportPath( RicfCommandFileExecutor::ExportType::COMPLETIONS );
RimEclipseCase_exportMswCompletions method( eclipseCase );
method.setWellPaths( { wellPath } );
method.setExportFolder( exportFolder );
return RicfForwarding::toScriptResponse( method.execute(), commandName );
}
```
Commands that only manipulate application or executor state (`openProject`, `closeProject`,
`saveProject`, `setStartDir`, `setExportFolder`, `setMainWindowSize`, `setPlotWindowSize`,
`exportMultiCaseSnapshots`, `replaceMultipleCases`) intentionally remain Ricf-only.
### Python wrappers (`GrpcInterface/Python/rips/*.py`)
- Prefer the generated method. Remove a hand-written wrapper when the generated method has the same
name and a compatible signature.
- A generated method on a **subclass** (e.g. `Reservoir`) shadows a hand-written wrapper on the base
class (`Case`) through normal attribute lookup. A deprecation shim on the base class is then never
reached; either remove the wrapper or give the object method a different keyword.
- When the old wrapper must stay (different argument names, id-based lookups, client-side
conveniences), keep it thin, forward to the generated method, and emit
`warnings.warn( "...", DeprecationWarning, stacklevel=3 )` (`add_method` adds one frame).
## Field Validation in Python GRPC Interface
Python field updates are automatically validated when calling `obj.update()`. This ensures data integrity by preventing invalid values from being set.
### Validation Types
1. **Type Validation**: Ensures field value types match (int, float, string, etc.)
- Parsing errors are caught during type conversion
- Example: Setting a string "hello" to an integer field is rejected
2. **Range Validation**: Checks values against min/max constraints defined with `setRange()`, `setMinValue()`, or `setMaxValue()`
- Fields with range constraints reject out-of-bounds values
- Example: A field with `setMinValue(0)` rejects negative values
3. **Object Validation**: Validates cross-field constraints via `validate()` override
- Custom business logic can enforce relationships between fields
- Example: Ensuring end date is after start date
### Setting Field Ranges in C++
To add range validation to a PDM field:
```cpp
RimMyClass::RimMyClass()
{
CAF_PDM_InitScriptableField(&m_percentage, "Percentage", 50.0, "Percentage");
m_percentage.setRange(0.0, 100.0); // Validates 0-100 range
CAF_PDM_InitScriptableField(&m_count, "Count", 1, "Count");
m_count.setMinValue(0); // Only validates minimum
m_count.setMaxValue(100); // Only validates maximum
CAF_PDM_InitScriptableField(&m_wellBoreFluidPvtTable, "WellBoreFluidPvtTable", 0, "Wellbore Fluid PVT Table");
m_wellBoreFluidPvtTable.setMinValue(0); // Must be non-negative
}
```
### Python Behavior
Invalid values are **rejected** and field values remain unchanged:
```python
# Example: Setting invalid value
completions_settings = well_path.completion_settings()
completions_settings.well_bore_fluid_pvt_table = -5 # Invalid: below minimum of 0
try:
completions_settings.update() # Triggers validation
except Exception as e:
print(f"Validation failed: {e}")
# Output:
# Validation failed: <_InactiveRpcError of RPC that terminated with:
# status = StatusCode.INVALID_ARGUMENT
# details = "Validation failed for WellPathCompletionSettings:
# WellBoreFluidPvtTable: Value -5 is below minimum 0"
# >
# The field retains its previous valid value (rollback occurred)
current_value = well_path.completion_settings().well_bore_fluid_pvt_table
print(f"Value remains: {current_value}") # Previous valid value
```
### Error Handling
Validation errors raise gRPC exceptions with descriptive messages:
```python
try:
obj.update()
except Exception as e:
error_msg = str(e)
if "Validation failed" in error_msg:
# Handle validation error
print("Please correct the following fields:")
print(error_msg)
else:
# Other error
raise
```
### Multiple Field Errors
All validation errors are aggregated and returned together:
```python
# Set multiple invalid values
settings.field1 = invalid_value1
settings.field2 = invalid_value2
try:
settings.update()
except Exception as e:
# Error message lists all validation failures:
# "Validation failed for ClassName:
# field1: Value ... exceeds maximum ...
# field2: Value ... is below minimum ..."
pass
```
### Validation Guarantees
- **Atomic Updates**: If any field fails validation, no fields are updated (all-or-nothing)
- **Rollback**: Failed updates don't modify field values - they remain at their previous valid state
- **Type Safety**: Cannot assign values of wrong type (string to int, etc.)
- **Clear Messages**: Error messages include field names and specific validation failures
### Implementation Notes
- Validation occurs automatically on `update()` - no manual validation needed
- Built on commit f342ca77572c840eadf22fe0a3fd2935c62fd04b validation infrastructure
- Uses C++23 `std::expected<void, QString>` for clean error handling
- Returns `grpc::INVALID_ARGUMENT` status with validation details
## PDM UI Editor Attributes - Modern setAttribute Pattern
Note: Always use () when accessing a field's value. This is required for proper type management. Use `m_myTextField()` instead of `m_myTextField`.
The CAF (Command Application Framework) PDM (Project Data Model) system has migrated from the old `defineEditorAttribute` pattern to a modern `setAttribute` pattern using type-safe Keys structs.
### New Pattern: setAttribute with Keys Struct
**Setting attributes using the new pattern:**
```cpp
// Modern approach - use Keys constants with implicit type deduction
m_comboBoxField.uiCapability()->setAttribute( caf::PdmUiComboBoxEditor::Keys::ADJUST_WIDTH_TO_CONTENTS, true );
m_comboBoxField.uiCapability()->setAttribute( caf::PdmUiComboBoxEditor::Keys::MINIMUM_CONTENTS_LENGTH, 15 );
m_comboBoxField.uiCapability()->setAttribute( caf::PdmUiComboBoxEditor::Keys::BUTTON_TEXT, "Click Me" );
```
### Benefits of the New Pattern
1. **Type Safety**: Compile-time checking of attribute names prevents typos
2. **IDE Support**: Auto-completion for attribute keys
3. **Self-Documenting**: Keys struct clearly shows available attributes for each editor
4. **Maintainable**: Single source of truth for attribute names
5. **Validation**: Automatic warnings for unsupported attributes
### Migration from Old Pattern
**Old pattern (deprecated):**
```cpp
// In defineEditorAttribute() override
auto* myAttr = dynamic_cast<caf::PdmUiComboBoxEditorAttribute*>( attribute );
if ( myAttr )
{
myAttr->adjustWidthToContents = true;
myAttr->minimumContentsLength = 15;
}
```
**New pattern:**
```cpp
// In field initialization or elsewhere
m_field.uiCapability()->setAttribute( caf::PdmUiComboBoxEditor::Keys::ADJUST_WIDTH_TO_CONTENTS, true );
m_field.uiCapability()->setAttribute( caf::PdmUiComboBoxEditor::Keys::MINIMUM_CONTENTS_LENGTH, 15 );
```
### Common Attribute Examples
```cpp
// Line Editor - notify on text changes
m_textField.uiCapability()->setAttribute( caf::PdmUiLineEditor::Keys::NOTIFY_WHEN_TEXT_IS_EDITED, true );
// Label Editor - hyperlink with callback
m_labelField.uiCapability()->setAttribute( caf::PdmUiLabelEditor::Keys::LINK_TEXT,
"Click <a href='link'>here</a>" );
std::function<void(const QString&)> callback = [](const QString& link) { /* handler */ };
m_labelField.uiCapability()->setAttribute( caf::PdmUiLabelEditor::Keys::LINK_ACTIVATED_CALLBACK,
QVariant::fromValue( callback ) );
// List Editor - set height hint
m_listField.uiCapability()->setAttribute( caf::PdmUiListEditor::Keys::HEIGHT_HINT, 150 );
// Push Button - set button text
m_buttonField.uiCapability()->setAttribute( caf::PdmUiPushButtonEditor::Keys::BUTTON_TEXT, "Execute" );
// Combo Box - enable previous/next buttons
m_comboField.uiCapability()->setAttribute( caf::PdmUiComboBoxEditor::Keys::SHOW_PREVIOUS_AND_NEXT_BUTTONS, true );
```
### Backward Compatibility
The old `defineEditorAttribute` pattern still works but is discouraged for new code. Both patterns can coexist during migration, with the new `setAttribute` pattern taking precedence when both are used.