Skip to content

Unroll everest controls#12857

Merged
yngve-sk merged 20 commits intoequinor:mainfrom
yngve-sk:26.02.unroll-everestcontrols
Feb 13, 2026
Merged

Unroll everest controls#12857
yngve-sk merged 20 commits intoequinor:mainfrom
yngve-sk:26.02.unroll-everestcontrols

Conversation

@yngve-sk
Copy link
Contributor

Issue
Resolves #12853

Approach
Short description of the approach

(Screenshot of new behavior in GUI if applicable)

  • PR title captures the intent of the changes, and is fitting for release notes.
  • Added appropriate release note label
  • Commit history is consistent and clean, in line with the contribution guidelines.
  • Make sure unit tests pass locally after every commit (git rebase -i main --exec 'just rapid-tests')

When applicable

  • When there are user facing changes: Updated documentation
  • New behavior or changes to existing untested code: Ensured that unit tests are added (See Ground Rules).
  • Large PR: Prepare changes in small commits for more convenient review
  • Bug fix: Add regression test for the bug
  • Bug fix: Add backport label to latest release (format: 'backport release-branch-name')

@yngve-sk yngve-sk force-pushed the 26.02.unroll-everestcontrols branch from 55643db to 8245761 Compare February 12, 2026 14:17
@codecov-commenter
Copy link

codecov-commenter commented Feb 12, 2026

Codecov Report

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

Files with missing lines Patch % Lines
src/ert/config/everest_control.py 87.50% 2 Missing ⚠️
src/ert/storage/migration/to26.py 96.87% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12857      +/-   ##
==========================================
- Coverage   90.64%   90.63%   -0.01%     
==========================================
  Files         440      441       +1     
  Lines       30460    30488      +28     
==========================================
+ Hits        27610    27634      +24     
- Misses       2850     2854       +4     
Flag Coverage Δ
cli-tests 37.17% <27.27%> (-0.05%) ⬇️
gui-tests 67.76% <27.27%> (-0.13%) ⬇️
performance-and-unit-tests 77.10% <78.72%> (+0.04%) ⬆️
test 37.29% <62.76%> (-0.07%) ⬇️

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.

berland and others added 7 commits February 13, 2026 09:18
…ummary of the changes:

  Summary

  I've successfully completed the refactoring requested in issue equinor#12853: Unroll input_keys in EverestControl. Here's what was changed:

  Key Changes:

   1. EverestControl Structure (src/ert/config/everest_control.py):
    - Changed from representing multiple variables (list fields) to a single scalar variable
    - Replaced list fields with scalar equivalents:
     - input_keys → input_key (single string)
     - types → control_type_ (single value)
     - initial_guesses → initial_guess (single float)
     - control_types → variable_type (single value)
     - enabled → is_enabled (single bool)
     - min → min_value (single float)
     - max → max_value (single float)
     - perturbation_types → perturbation_type (single value)
     - perturbation_magnitudes → perturbation_magnitude (single float)
     - scaled_ranges → scaled_range (single tuple)
     - samplers → sampler (single SamplerConfig or None)
    - Added group field to reference the control group name
    - Updated __len__() to always return 1
    - Updated create_storage_datasets() to work with a single value
    - Removed write_to_runpath() logic (now handled at higher level)
   2. ControlConfig.to_ert_parameter_config() (src/everest/config/control_config.py):
    - Changed return type from EverestControl to list[EverestControl]
    - Creates one EverestControl object per variable (unrolled structure)
    - Each control has its own unique name (e.g., "point.x", "point.y", "point.z")
    - All share the same group name (e.g., "point")
    - All share the same output_file
   3. Updated Optimizer Code:
    - everest2ropt.py: Updated to work with scalar fields instead of nested loops
    - opt_model_transforms.py: Updated to use scalar accessors
    - get_samplers(): Simplified to use scalar sampler field
   4. Updated Parameter File Writing (src/ert/run_models/_create_run_path.py):
    - Added special handling for EverestControl objects
    - Groups controls by output_file
    - Aggregates their values and writes a single JSON file per group
    - Preserves the nested JSON structure (e.g., {"x":

    1.5, "y": 2.5})
   5. Updated All Usages:
    - everest_run_model.py: Flattens the list of lists returned by to_ert_parameter_config()
    - All test files: Updated to use the new flattened structure

  Benefits:

   - Consistency with ERT: Each EverestControl now maps to one scalar value, making it consistent with other ERT parameter types like GenKwConfig
   - Simpler Structure: The refactored code is more straightforward - no nested loops over controls and their variables
   - Easier to Extend: Makes equinor#12647 easier to solve by having a cleaner 1:1 mapping
   - Backward Compatible: All existing tests pass without changes to user-facing configuration

  Test Results:

   - ✅ All test_everest_control.py tests pass (5/5)
   - ✅ All test_controls.py tests pass (17/17)
   - ✅ All test_ropt_initialization.py tests pass (14/14)

  The refactoring is complete and ready for review!
@yngve-sk yngve-sk force-pushed the 26.02.unroll-everestcontrols branch from 3355627 to 641dc22 Compare February 13, 2026 08:24
continue

# Write aggregated EverestControl JSON files
start_time = time.perf_counter()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note2self/reviewer : This part ensures everest controls stay written "aggregated" even though they are specified as configs one-by-one. This can be removed once all everest forward models depending on the aggregated structure are refactored to use the flattened structure.

@yngve-sk yngve-sk force-pushed the 26.02.unroll-everestcontrols branch from ac7bf7a to d74bd89 Compare February 13, 2026 09:24
if isinstance(param, EverestControl):
# Collect EverestControls for later aggregated writing
everest_controls_by_file[param.output_file].append(param)
export_timings[param.type] += time.perf_counter() - start_time
Copy link
Contributor

Choose a reason for hiding this comment

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

This timing is probably meaningless, it takes zero time to add it to the dict

else:
data[key_without_group] = value

file_path.write_text(json.dumps(data), encoding="utf-8")
Copy link
Contributor

Choose a reason for hiding this comment

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

should add indent=2 to dumps for easier human reading of the files on disk.

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 13, 2026

Merging this PR will not alter performance

✅ 34 untouched benchmarks


Comparing yngve-sk:26.02.unroll-everestcontrols (a7854a3) with main (e624b66)

Open in CodSpeed

control group is never None for EverestControl

if param.name in scalar_data:
if isinstance(param, EverestControl):
# Collect EverestControls for later aggregated writing
Copy link
Contributor

Choose a reason for hiding this comment

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

superfluous comment

formatted_names = self.formatted_control_names
formatted_names_dotdash = self.formatted_control_names_dotdash

idx = 0
Copy link
Contributor

Choose a reason for hiding this comment

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

Can use for variable, idx in enumerate(self.variables) instead.

sampler=variable.sampler or self.sampler,
)
controls.append(control)
idx += 1
Copy link
Contributor

Choose a reason for hiding this comment

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

remove this if enumerate is used

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The counter goes over the nested list and must match with formatted_control_names_dotdash. Could leave it for later to refactor these out and switch to enumerate though, suggest separate issue

scaled_ranges=[(-1.0, 1.0), (-1.0, 1.0), (-1.0, 1.0)],
samplers=[None, None, None],
)
# Create three separate controls (refactored structure)
Copy link
Contributor

Choose a reason for hiding this comment

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

bogus comment

)
# Create three separate controls (refactored structure)
controls = [
EverestControl(
Copy link
Contributor

Choose a reason for hiding this comment

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

can use some comprehension here to save lines

mock_ensemble.load_parameters.return_value = mock_df

run_path = tmp_path / "runpath" / "realization-5"
# Create separate DataFrames for each control
Copy link
Contributor

Choose a reason for hiding this comment

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

bogus comment

run_path = tmp_path / "runpath" / "realization-5"

mock_ensemble.load_parameters.assert_called_once_with("point", 5)
# Simulate the aggregated file writing from _generate_parameter_files
Copy link
Contributor

Choose a reason for hiding this comment

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

bogus comment

)
# Create three separate controls with nested keys
# The input_key will be like "point.x.0" which after removing "point." becomes "x.0"
# This creates JSON structure {"x": {"0": 1.5, "1": 2.5, "2": 3.5}}
Copy link
Contributor

Choose a reason for hiding this comment

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

replace comment with a comprehension

)
mock_ensemble.load_parameters.return_value = mock_df

# Create separate DataFrames for each control
Copy link
Contributor

Choose a reason for hiding this comment

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

bogus comment

run_path = tmp_path / "runpath" / "realization-5"

mock_ensemble.load_parameters.assert_called_once_with("point", 5)
# Simulate the aggregated file writing from _generate_parameter_files
Copy link
Contributor

Choose a reason for hiding this comment

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

should we not use _generate_parameter_files instead of replicating it here???

control.write_to_runpath(run_path, 5, mock_ensemble)
run_path.mkdir(parents=True)

# Simulate the aggregated file writing from _generate_parameter_files
Copy link
Contributor

Choose a reason for hiding this comment

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

why simulate instead of actually test it?

control = EverestControl(
name="point",
input_keys=["point.x", "point.y", "point.z"],
name="point.x",
Copy link
Contributor

Choose a reason for hiding this comment

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

agree that it seems pointless to include y and z for the purpose of this test.

}

other_param = {"type": "gen_kw", "name": "P1", "distribution": {"name": "uniform"}}

Copy link
Contributor

Choose a reason for hiding this comment

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

Do not need an empty line between each statement. Maybe only around migrage(root).

Copy link
Contributor

@berland berland left a comment

Choose a reason for hiding this comment

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

Think this looks good! Time to squash and write a good commit message.

@yngve-sk yngve-sk self-assigned this Feb 13, 2026
@yngve-sk yngve-sk added the release-notes:skip If there should be no mention of this in release notes label Feb 13, 2026
@yngve-sk yngve-sk merged commit a8b69b1 into equinor:main Feb 13, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-notes:skip If there should be no mention of this in release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unroll input_keys in EverestControl

3 participants