Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve handling of invalid experiments in GUI #9589

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

jonathan-eq
Copy link
Contributor

@jonathan-eq jonathan-eq commented Dec 18, 2024

Issue
Resolves #9493

Approach
The commit in this PR:

  • Makes storage re-validate when changing between ert modes (manage experiment, run experiment, plot experiment), and emit storage_changed if validity has changed.
  • Disables ensembles with invalid experiments from ensemble_selectors (error is shown as tooltip on hover)
  • Filters out invalid experiments from dark storage, so plotter won't attempt to plot it.
  • Reloads storage and re-validates on end of experiment, so ert wont crash if responses.json is deleted mid-run.

(Screenshot of new behavior in GUI if applicable)
image
(This is when the responses.json file is deleted mid run. Prior to this PR, ert would crash with ValueError: responses.json does not exist)

image
(This is when responses.json is deleted, and we open the plotter. The ensemble exists, but is not given as a option to plot due to it having an invalid experiment)

image
(This is when responses.json is deleted and we open manage-experiment. The experiment is shown, but cannot be selected. It is greyed out, and gives error as a tooltip when hovered)

  • 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 'pytest tests/ert/unit_tests -n logical -m "not integration_test"')

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: Create Backport PR to latest release

Copy link

codspeed-hq bot commented Dec 18, 2024

CodSpeed Performance Report

Merging #9589 will not alter performance

Comparing jonathan-eq:fix-missing_responses_error (8df6b4e) with main (1199e58)

Summary

✅ 25 untouched benchmarks

@jonathan-eq jonathan-eq force-pushed the fix-missing_responses_error branch from d644aee to 2fe1d74 Compare December 18, 2024 14:30
@codecov-commenter
Copy link

codecov-commenter commented Dec 18, 2024

Codecov Report

Attention: Patch coverage is 80.00000% with 15 lines in your changes missing coverage. Please review.

Project coverage is 91.83%. Comparing base (01156bb) to head (869f845).
Report is 20 commits behind head on main.

Files with missing lines Patch % Lines
.../ert/gui/tools/manage_experiments/storage_model.py 77.77% 6 Missing ⚠️
src/ert/gui/ertwidgets/ensembleselector.py 66.66% 4 Missing ⚠️
src/ert/storage/local_experiment.py 82.35% 3 Missing ⚠️
src/ert/storage/local_ensemble.py 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9589      +/-   ##
==========================================
- Coverage   91.85%   91.83%   -0.03%     
==========================================
  Files         433      433              
  Lines       26768    26879     +111     
==========================================
+ Hits        24587    24683      +96     
- Misses       2181     2196      +15     
Flag Coverage Δ
cli-tests 39.69% <22.66%> (-0.07%) ⬇️
everest-models-test 34.55% <18.66%> (-0.04%) ⬇️
gui-tests 72.13% <77.33%> (+0.02%) ⬆️
integration-test 38.71% <22.66%> (+1.51%) ⬆️
performance-tests 51.88% <36.00%> (-0.06%) ⬇️
test 40.02% <22.66%> (-0.43%) ⬇️
unit-tests 74.06% <48.00%> (-0.11%) ⬇️

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.

and not ensemble.experiment.is_valid()
):
index = self.count() - 1
model_item = model.item(index)
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it works well. Should I use ItemData instead?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure. I looked into this due to style/linting errors. If the error was resolved, this is probably fine.

@jonathan-eq jonathan-eq force-pushed the fix-missing_responses_error branch 2 times, most recently from abb616c to d6e1bdd Compare January 14, 2025 08:55
@@ -154,6 +154,7 @@ def right_clicked(self) -> None:
def select_central_widget(self) -> None:
actor = self.sender()
if actor:
self.notifier.refresh()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This refreshes storage to make sure the experiment files (responses, index, metadata, and parameters) are still valid.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could consider just re-running validation, and refresh if that fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I updated it so that if will only actually refresh when the validity has changed, when toggling between the panels. This also means that the storage won't be updated if new experiments are added and we toggle between the panels, but that is the same behavior as on main.

@jonathan-eq
Copy link
Contributor Author

There is a bug where it crashes if you are already in the manage experiment window, tab out to delete the responses.json file, and then put focus back on the ert window. I am trying to have ert refresh storage when gaining focus, but reimplementing focusInEvent on the main window does not work (it is never called). Is there a way I can make this work so that I can force refresh/revalidate storage before the child widgets start selecting invalid experiments @andreas-el?

@jonathan-eq jonathan-eq force-pushed the fix-missing_responses_error branch 3 times, most recently from 61e01c7 to 9203cd3 Compare January 21, 2025 08:55
@jonathan-eq jonathan-eq marked this pull request as ready for review January 21, 2025 08:56
@jonathan-eq
Copy link
Contributor Author

This PR contains the initial work towards making this part of the storage more robust. There is still an issue where ert crashes if you have selected an experiment in the manage-experiment panel, tab out and delete responses.json, and tab back in. It seems like the SelectionModel for the QListView tries reselecting the experiment (which is now invalid) before it has been revalidated.

@jonathan-eq jonathan-eq added release-notes:bug-fix Automatically categorise as bug fix in release notes release-notes:improvement Automatically categorise as improvement in release notes and removed release-notes:bug-fix Automatically categorise as bug fix in release notes labels Jan 21, 2025
@@ -94,6 +94,8 @@ def __init__(self) -> None:

@Slot(Experiment)
def setExperiment(self, experiment: Experiment) -> None:
if not experiment.is_valid():
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this even happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, when the selected experiment in manage-experiment becomes invalid while still selected. Apparently, when you tab out of ert while an experiment is selected, and tab in again; the same experiment is re-selected, and this signal is emitted. Very fun to debug 🔮

@@ -128,7 +129,9 @@ def data(
qapp = QApplication.instance()
assert isinstance(qapp, QApplication)
return qapp.palette().mid()

elif role == Qt.ItemDataRole.ToolTipRole:
if self._error:
Copy link
Contributor

Choose a reason for hiding this comment

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

Is is the same error shown twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am removing the one not on experimentmodel.

@override
def hasChildren(self, parent: QModelIndex | None = None) -> bool:
if parent is None or not parent.isValid():
return True
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't this be False? At least it sounds like it, but it might be my lack of understanding.

Copy link
Contributor

Choose a reason for hiding this comment

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

A person/item who has no parents, or not any valid parents, are guaranteed to have children?
I'm so confused here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It does not work without that line 😿
Is it that this is the topmost model (will never have a parent), and will always have children; but the number of children is dependent on the number of experiments?

Copy link
Contributor

Choose a reason for hiding this comment

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

What if you have no experiments? Is this still correct? I think you need to provide some comment or explanation here, since we don't understand why this is so.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think hasChildren is there only to check if the tree can be expanded. In our case, the root node (StorageModel) will always be expanded, but the number of rows (ExperimentModels) will determine how much it will be expanded. I can change it to return len(self._children) > 0, but hardcoding it to true would probably be fine too.

@andreas-el
Copy link
Contributor

I will argue that you should alter the title of this PR. The PR does not specifically target the missing file, but rather handling invalid experiments.


def check_plot_tool(expected_number_of_cases: int) -> None:
find_and_click_button("button_Create_plot")
# Due to the fact that we create new instances of PlotWindow on tab change, QtBot is defaulting to the first child
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have to test the plotter as the missing responses.json bug also occurred there

@jonathan-eq
Copy link
Contributor Author

I will argue that you should alter the title of this PR. The PR does not specifically target the missing file, but rather handling invalid experiments.

Yes, I was hit by scope creep...

Comment on lines +80 to +86
def _validate_files(self) -> None:
self.valid_parameters = (self._path / self._parameter_file).exists()
self.valid_responses = (self._path / self._responses_file).exists()
self.valid_metadata = (self._path / self._metadata_file).exists()

def is_valid(self) -> bool:
return self.valid_parameters and self.valid_responses and self.valid_metadata
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense to combine these, and check for existence of files in is_valid ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Then it becomes more difficult to see if the validity of a specific experiment has changed. I think that is why I chose to have it in two separate steps.

Comment on lines +71 to +74
self._storage.refresh()
self.storage_changed.emit(self._storage)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this emit regardless of outcome?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We only call this method when we know it will be changed when refreshing. Either if the validity of an experiment has changed; or if an experiment has finished/started running

Copy link
Contributor

Choose a reason for hiding this comment

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

But if you call the refresh function, this will emit regardless. I see that your statement is true when using revalidate_storage, due to the check there that looks at state changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, but we only call it when we already know something has changed, this check is done upstream in both places where it's used 🤔

Comment on lines 24 to +27
show_only_undefined: bool = False,
show_only_no_children: bool = False,
show_only_with_valid_experiment: bool = False,
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these flags mutually exclusive? I.e. only one is ever set to true?
This could have been converted to an enum if so.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, they are two completely separate filters

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should spend a couple of minutes looking at this to see if we can alter this. I think this filtering thing have grown outside reasonable scope of multiple booleans.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we need all those filters, because this component is re-used with a lot of different context

Comment on lines +80 to +81
show_only_undefined=True,
show_only_with_valid_experiment=True,
Copy link
Contributor

Choose a reason for hiding this comment

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

Show only valid and undefined ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They are separate, so it think they should have their own flags.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

show_only_undefined is for the ensembles in the experiment; whilst the other flag is for the experiment itself.

@jonathan-eq jonathan-eq force-pushed the fix-missing_responses_error branch from 9203cd3 to 53e9bac Compare January 22, 2025 14:00
@jonathan-eq jonathan-eq changed the title Improve handling of missing responses.json Improve handling of invalid experiments in GUI Jan 23, 2025
@jonathan-eq jonathan-eq force-pushed the fix-missing_responses_error branch from 53e9bac to 033426e Compare January 23, 2025 07:54
@xjules
Copy link
Contributor

xjules commented Jan 24, 2025

Not sure if it is related but this one hangs: https://github.com/equinor/ert/actions/runs/12924655010/job/36044056095?pr=9589

@jonathan-eq jonathan-eq force-pushed the fix-missing_responses_error branch 2 times, most recently from 32addbb to 4cbc09d Compare January 27, 2025 11:46
self.addItem(
f"{ensemble.experiment.name} : {ensemble.name}", userData=ensemble
)
if (
self._show_only_with_valid_experiment
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you really need to check for _show_only_with_valid_experiment here?
Does it not make sense to just do this for all experiments that are not valid?

This commit improves the handling of invalid experiments in the gui,
in the case of missing responses.json file. The handling of the missing
file should in the future be extended to also handle the other experiment
files in a similar manner (index.json, metadata.json, and parameters.json)

This commit:
* Makes storage reload and re-validate when changing between ert modes
  (manage experient, run experiment, and plot tool)
* Disables ensemble with invalid experiments from ensemble_selectors
  (error is shown as tooltip on hover)
* Filters out invalid experiments from dark storage, so plotter won't
  attempt to plot them.
* Reloads and re-validates storage on end of experiment, so ert won't
  crash if responses.json is deleted mid-run.
@jonathan-eq jonathan-eq force-pushed the fix-missing_responses_error branch from 92032e8 to 8df6b4e Compare January 28, 2025 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes:improvement Automatically categorise as improvement in release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Missing responses.json when opening ert
4 participants