# 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 - Keep the commit message short and concise: a one-line title, and only as much body text as needed to explain the "why" — avoid long, exhaustive explanations - 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 ` before committing (see `docs/agents/coding-style.md`) ### PR Conventions - Do not include a test plan in the PR description - Keep the PR description short and concise: a few short sections (e.g. Problem / Fix / Testing) with 1-3 sentences each, not an exhaustive report ## 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 `_`, e.g. `RimCase_replaceGrid`, `RimEclipseView_exportVisibleCells`. - Parameters are `CAF_PDM_InitScriptableField` fields. Pass **objects, not ids**: use `caf::PdmPtrField` / `caf::PdmPtrArrayField` (generated as `T` / `List[T]` in Python). Enum parameters use `caf::PdmField>` and are generated as Python `StrEnum`s; pin the Python class name with `caf::PdmScriptEnumNameRegistry::registerName( "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( 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` 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( attribute ); if ( myAttr ) { myAttr->someMember = true; } ``` **New pattern:** ```cpp // In field initialization or elsewhere m_field.uiCapability()->setAttribute( caf::PdmUiLineEditor::Keys::NOTIFY_WHEN_TEXT_IS_EDITED, true ); ``` ### 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 here" ); std::function 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.