Skip to content

Conversation

@yngve-sk
Copy link
Contributor

@yngve-sk yngve-sk commented Jan 20, 2026

Prep for attaching observation declarations to runmodel config, but still getting most of the validations in the ERT config parsing step (even though _create_observation_dataframes is no longer to be called in the ERT config parsing step)

@yngve-sk yngve-sk force-pushed the 26.01.move-observations-parsing-to-runmodel branch 2 times, most recently from 81523f8 to ec16db9 Compare January 20, 2026 09:06
@codecov-commenter
Copy link

codecov-commenter commented Jan 20, 2026

Codecov Report

❌ Patch coverage is 97.22222% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.63%. Comparing base (7238d60) to head (b880347).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/ert/config/_observations.py 97.91% 1 Missing ⚠️
src/ert/config/observation_config_migrations.py 95.23% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12675      +/-   ##
==========================================
- Coverage   90.66%   90.63%   -0.03%     
==========================================
  Files         430      431       +1     
  Lines       29814    30085     +271     
==========================================
+ Hits        27032    27269     +237     
- Misses       2782     2816      +34     
Flag Coverage Δ
cli-tests 37.30% <56.94%> (-0.30%) ⬇️
gui-tests 68.69% <61.11%> (-0.65%) ⬇️
performance-and-unit-tests 74.06% <97.22%> (+0.11%) ⬆️
test 37.53% <19.44%> (-0.54%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@yngve-sk yngve-sk force-pushed the 26.01.move-observations-parsing-to-runmodel branch 2 times, most recently from bfe5ea3 to a8197ff Compare January 20, 2026 10:54
@codspeed-hq
Copy link

codspeed-hq bot commented Jan 20, 2026

Merging this PR will not alter performance

✅ 22 untouched benchmarks


Comparing yngve-sk:26.01.move-observations-parsing-to-runmodel (b880347) with main (6995ffc)

Open in CodSpeed

@yngve-sk yngve-sk force-pushed the 26.01.move-observations-parsing-to-runmodel branch from 990acc7 to bdb7483 Compare January 20, 2026 14:18
@yngve-sk yngve-sk force-pushed the 26.01.move-observations-parsing-to-runmodel branch from bdb7483 to b880347 Compare January 20, 2026 14:20
@yngve-sk yngve-sk changed the title Move observations parsing to runmodel Move some validation errors to _observations.py Jan 20, 2026
@yngve-sk yngve-sk added this to SCOUT Jan 20, 2026
@yngve-sk yngve-sk moved this to In Progress in SCOUT Jan 20, 2026
@berland berland self-requested a review January 20, 2026 14:27
@berland berland moved this from In Progress to Ready for Review in SCOUT Jan 20, 2026
@berland berland moved this from Ready for Review to In Progress in SCOUT Jan 20, 2026
@berland berland requested a review from Copilot January 20, 2026 14:30
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors observation configuration validation by moving error handling and validation logic from _create_observation_dataframes.py to _observations.py. This prepares for attaching observation declarations to runmodel config while maintaining validations during the ERT config parsing step.

Changes:

  • Migrated observation dataclasses to Pydantic BaseModel classes with discriminator support
  • Moved _parse_date function and error mode validation logic from _create_observation_dataframes.py to _observations.py
  • Moved validation checks for GENERAL_OBSERVATION and error calculations for SUMMARY_OBSERVATION to parsing time

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/ert/config/_observations.py Converted observation classes from dataclasses to Pydantic BaseModel, added type discriminator fields, moved _parse_date function, integrated error mode calculation and validation into from_obs_dict methods
src/ert/config/_create_observation_dataframes.py Removed _handle_error_mode and _parse_date functions (now in _observations.py), simplified error handling to use pre-computed error values from observation instances
src/ert/config/observation_config_migrations.py Added ObservationError dataclass and _legacy_handle_error_mode function for legacy history observation migrations
tests/ert/unit_tests/config/observations_generator.py Updated as_obs_config_content to use Pydantic's model_fields API and fixed isinstance syntax for type checking

Comment on lines +127 to +136
if error_mode is not None:
match error_mode:
case ErrorModes.ABS:
error = np.abs(input_error)
case ErrorModes.REL:
error = np.abs(value) * input_error
case ErrorModes.RELMIN:
error = np.maximum(np.abs(value) * input_error, error_min)
case default:
assert_never(default)
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition if error_mode is not None: is always true because error_mode is initialized to ErrorModes.ABS at line 68 and can only be reassigned to another ErrorModes value at line 94. This makes the conditional check redundant. Consider removing the if statement and always executing the match block, or changing the logic to make error_mode Optional[ErrorModes] if None is a valid state.

Suggested change
if error_mode is not None:
match error_mode:
case ErrorModes.ABS:
error = np.abs(input_error)
case ErrorModes.REL:
error = np.abs(value) * input_error
case ErrorModes.RELMIN:
error = np.maximum(np.abs(value) * input_error, error_min)
case default:
assert_never(default)
match error_mode:
case ErrorModes.ABS:
error = np.abs(input_error)
case ErrorModes.REL:
error = np.abs(value) * input_error
case ErrorModes.RELMIN:
error = np.maximum(np.abs(value) * input_error, error_min)
case default:
assert_never(default)

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +60
def _parse_date(date_str: str) -> datetime:
try:
return datetime.fromisoformat(date_str)
except ValueError:
try:
date = datetime.strptime(date_str, "%d/%m/%Y")
except ValueError as err:
raise ObservationConfigError.with_context(
f"Unsupported date format {date_str}. Please use ISO date format",
date_str,
) from err
else:
ConfigWarning.warn(
f"Deprecated time format {date_str}."
" Please use ISO date format YYYY-MM-DD",
date_str,
)
return date
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _parse_date function was moved from _create_observation_dataframes.py to _observations.py. While this is part of the refactoring, consider adding a docstring to document its behavior, especially the support for both ISO format and the deprecated DD/MM/YYYY format, and that it emits a warning for the deprecated format.

Copilot uses AI. Check for mistakes.
if summary_key is None:
raise _missing_value_error(observation_dict["name"], "KEY")
if "ERROR" not in float_values:
raise _missing_value_error(observation_dict["name"], "ERROR")
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses an assertion to check if date is not None, but there's no validation that ensures DATE is provided in the observation_dict. If DATE is missing, this will cause an AssertionError instead of a proper ObservationConfigError. Consider adding an explicit check similar to the ones for VALUE, KEY, and ERROR (lines 110-115) to raise a proper validation error if DATE is missing.

Suggested change
raise _missing_value_error(observation_dict["name"], "ERROR")
raise _missing_value_error(observation_dict["name"], "ERROR")
if date is None:
raise _missing_value_error(observation_dict["name"], "DATE")

Copilot uses AI. Check for mistakes.
# Bypass pydantic discarding context
# only relevant for ERT config surfacing validation errors
# irrelevant for runmodels etc.
instance.name = observation_dict["name"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a little bit smelly. Will take a second look on this.

location_y=localization_values.get("y"),
location_range=localization_values.get("range"),
date=date,
date=standardized_date,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do parsed_date.date().isoformat() instead, always better to avoid strftime.


assert date is not None
# Raise errors if the date is off
parsed_date = _parse_date(date)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense to type this assignment for readability:
parsed_date: datetime = _parse_date(date)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants