Skip to content

Introduce NicknameSettingList for explicit efit tree cascade#559

Open
samc24 wants to merge 5 commits into
devfrom
sameerc/nickname-revamp
Open

Introduce NicknameSettingList for explicit efit tree cascade#559
samc24 wants to merge 5 commits into
devfrom
sameerc/nickname-revamp

Conversation

@samc24

@samc24 samc24 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds NicknameSettingList: user-specifiable cascade of efit tree candidates. Tries each via open_tree, catches mdsExceptions.TreeFOPENR (tree missing) and FetchDataError (nested cascade exhausted), returns the first that opens.
  • Resolver accepts lists (efit_nickname_setting=["efit21", "efit18", "analysis"]) and comma-separated CLI strings (--efit-tree "efit21,efit18,analysis"). Tokens that match registered keys (e.g. "disruption") dispatch to their NicknameSetting class inside the cascade.
  • Refactors DisruptionNicknameSetting._cmod_nickname to delegate to the new primitive -- replaces the disruption_time is None proxy with real fallback driven by tree-open errors. D3D path unchanged (its rundb SQL query is real functionality, not just cascade).
  • Phase 2 hook: MDSConnection.tree_nickname_cascades stores the originating NicknameSettingList so a follow-up PR can extend retry to data-path-missing errors inside an opened tree. Currently written for top-level NicknameSettingList only; nested cases (dict-wrapped, DisruptionNicknameSetting internals) are not yet surfaced.

Behavior change

For efit_nickname_setting="disruption" on CMOD, the legacy proxy disruption_time is None → return "analysis" is replaced with a real open_tree("efit18") attempt, falling back to "analysis" on TreeFOPENR.

Validated bit-identical output vs dev on two CMOD test shots:

  • 1150805012 (disrupting flat-top): both branches resolve to efit18, 83 idx points, identical data.
  • 1150805013 (no_disrup1_full, non-disrupting): old returns "analysis" via proxy; new tries efit18, gets TreeFOPENR, falls back to "analysis". Both produce 88 idx points, identical data.

Trade-off: one extra failed MDS open_tree per non-disrupting shot using "disruption", in exchange for a verbose log line documenting the fallback (Nickname cascade for shot N: selected 'analysis' after failed: efit18) and a path that handles unanticipated tree-availability patterns without proxy maintenance.

Out of scope (Phase 2 follow-up)

  • Cascade retry on FetchDataError when a tree opens but a data path inside it is missing.
  • Surfacing nested cascades (in NicknameSettingDict or inside DisruptionNicknameSetting._cmod_nickname) to MDSConnection.tree_nickname_cascades.
  • Changing the default efit_nickname_setting value.

Test plan

  • New unit tests in tests/test_nickname_setting.py (25 tests, pure-Python with a fake data_conn, no MDS required).
  • Lint suite green: make black && ruff check . && isort --check . && pylint (CI run 26377445449).
  • Manual CMOD smoke on mfews04 with shot 1150805012:
    • uv run disruption-py --efit-tree "doesnotexist,analysis" 1150805012 — cascade selects analysis after doesnotexist fails; verified via TRACE log.
    • uv run disruption-py --efit-tree "disruption,analysis" 1150805012 — first item resolves to DisruptionNicknameSetting, succeeds; 83 idx points (same as plain "disruption").
  • Bit-identical regression check vs dev on shots 1150805012 (disrupting) and 1150805013 (non-disrupting) under --efit-tree "disruption".

Signed-off-by: Sameer Chaturvedi <sameerc@mit.edu>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 introduces an explicit EFIT tree “cascade” nickname setting to improve robustness when a preferred EFIT tree is absent, and wires the selected cascade into the MDS connection for future retry enhancements.

Changes:

  • Added NicknameSettingList to try a sequence of tree candidates, selecting the first that successfully open_tree()’s.
  • Extended resolve_nickname_setting() to accept list cascades and comma-separated string cascades, including mapping registered tokens (e.g. "disruption", "analysis") to their NicknameSetting implementations.
  • Refactored CMOD DisruptionNicknameSetting to use the new cascade behavior and added unit tests for cascade resolution/fallback.

Reviewed changes

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

File Description
tests/test_nickname_setting.py Adds unit tests for cascade parsing, validation, and fallback behavior.
disruption_py/settings/nickname_setting.py Implements NicknameSettingList, extends resolver to accept cascades, refactors CMOD disruption tree selection.
disruption_py/inout/mds.py Stores nickname cascade objects on MDSConnection for follow-up retry logic.
disruption_py/core/retrieval_manager.py Passes EFIT nickname cascade metadata into MDSConnection.add_tree_nickname_funcs().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +555 to +556
if isinstance(nickname_setting, list):
return NicknameSettingList(nickname_setting)
Comment on lines +240 to +251
class NicknameSettingList(NicknameSetting):
"""
Cascade nickname setting. Tries each item in order via ``open_tree``,
returning the first that opens successfully. Falls back to the next on
``mdsExceptions.TreeFOPENR``.

Parameters
----------
items : list
Cascade of tree-name strings or NicknameSetting instances. Tried in
order; the first item that opens wins.
"""
Comment on lines +268 to +278
@staticmethod
def _resolve_item(item):
"""Validate a cascade item: must be a string or NicknameSetting."""
if isinstance(item, NicknameSetting):
return item
if isinstance(item, str):
return item
raise ValueError(
"NicknameSettingList items must be str or NicknameSetting, "
f"got {type(item).__name__}: {item!r}"
)
Comment on lines +379 to +383
def add_tree_nickname_funcs(
self,
tree_nickname_funcs: Dict[str, Callable],
tree_nickname_cascades: Dict[str, Any] = None,
):
Comment on lines +91 to +95
def test_resolve_list_cascade():
"""A list dispatches to NicknameSettingList."""
out = resolve_nickname_setting(["efit21", "analysis"])
assert isinstance(out, NicknameSettingList)

Comment thread tests/test_nickname_setting.py Outdated
Comment on lines +103 to +109
def test_resolve_dict_value_can_be_cascade():
"""A NicknameSettingDict value may itself be a cascade list."""
out = resolve_nickname_setting({Tokamak.CMOD: ["efit21", "efit18", "analysis"]})
assert isinstance(out, NicknameSettingDict)
inner = out.resolved_nickname_setting_dict[Tokamak.CMOD]
assert isinstance(inner, NicknameSettingList)
assert inner.resolved_items == ["efit21", "efit18", "analysis"]
  - Resolve registered keys in list-cascade inputs (e.g. `["disruption", "analysis"]`), matching the existing comma-string
  behavior. Shared via a new `_map_cascade_item` helper.
  - `NicknameSettingList` docstring now lists both `TreeFOPENR` and `FetchDataError` as fallback triggers.
  - `_resolve_item` strips strings and rejects empty / whitespace-only items.
  - `MDSConnection.add_tree_nickname_funcs`: `tree_nickname_cascades` typed as `Optional[Dict[str, Any]]`.
  - Tests updated for list-cascade key resolution; added `test_list_empty_string_rejected`.

Signed-off-by: Sameer Chaturvedi <sameerc@mit.edu>
@samc24

samc24 commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

Good catches, CoPilot

  1. resolve_nickname_setting() now resolves registered keys in Python list inputs (the comma-string path mapped keys but the list path didn't). Both paths share a new _map_cascade_item helper so they stay consistent.
  2. Updated the NicknameSettingList docstring to document both TreeFOPENR and FetchDataError as fallback triggers.
  3. _resolve_item now strips strings and rejects empty / whitespace-only items with a clear error.
  4. Typed tree_nickname_cascades as Optional[Dict[str, Any]].
  5. & 6. Updated test_resolve_list_cascade to assert key resolution; flipped the inner-list assertion in
    test_resolve_dict_value_can_be_cascade to check for DefaultNicknameSetting. Also added test_list_empty_string_rejected to cover the new validation.

Local pre-push: 28/28 nickname tests pass, ruff/isort/pylint clean.

@yumouwei yumouwei left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I tested the new efit_nickname_setting=List and it works. Would it be possible to add the names of the EFIT trees used by each shot to the dataset attributes (e.g. as a dictionary)?

@gtrevisan

Copy link
Copy Markdown
Member

I think that will have to first be moved over into the configuration, and then be dumped into a JSON or TOML next to the output -- in any case, that will have to happen in a different PR but yes! absolutely, it's on the to-do list.

can you provide some useful snippets for eg testing something that has efit21 and efit18 and analysis, vs only efit21 and analysis, and so forth? we need to understand the implications before merging.
I'd imagine maybe a small script that gets data and asserts based on the length of the output? that is indeed the primary difference between those efit trees...

@yumouwei

yumouwei commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

We can do something really simple like this:

from disruption_py.settings import RetrievalSettings
from disruption_py.workflow import get_shots_data

# 1150805012: disruptive shot, has efit18, analysis, and efit21
# 1150805013: non-disruptive shot, has analysis and efit21

# 1: cascade
retrieval_settings_1 = RetrievalSettings(
    efit_nickname_setting=['efit18', 'analysis', 'efit21'],
    run_columns=['li'],
    time_setting="efit",
)
shot_data_1 = get_shots_data(
    shotlist_setting=[1150805012, 1150805013],
    retrieval_settings=retrieval_settings_1,
)
# 2: efit18
retrieval_settings_2 = RetrievalSettings(
    efit_nickname_setting='efit18',
    run_columns=['li'],
    time_setting="efit",
)
shot_data_2 = get_shots_data(
    shotlist_setting=[1150805012, 1150805013],
    retrieval_settings=retrieval_settings_2,
)
# 3: analysis
retrieval_settings_3 = RetrievalSettings(
    efit_nickname_setting='analysis',
    run_columns=['li'],
    time_setting="efit",
)
shot_data_3 = get_shots_data(
    shotlist_setting=[1150805012, 1150805013],
    retrieval_settings=retrieval_settings_3,
)

# Compare 3 datasets
assert shot_data_1.sel(shot=1150805012).equals(shot_data_2.sel(shot=1150805012))        # efit18 == efit18
assert not shot_data_1.sel(shot=1150805012).equals(shot_data_3.sel(shot=1150805012))    # efit18 != analysis
assert 1150805013 not in shot_data_2.shot.values                                        # 1150805013 does not have efit18
assert shot_data_1.sel(shot=1150805013).equals(shot_data_3.sel(shot=1150805013))        # analysis == analysis

We can't test this on D3D because of their EFIT tree name allocation for DIS & DISPY, which is really annoying.

@samc24

samc24 commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @yumouwei — built your snippet into a runnable validation script (scripts/nickname_cascade_validation.py) plus a CMOD-gated regression test (tests/test_nickname_cascade.py), and ran both on the cluster. They exercise the [efit18, analysis, efit21] cascade against both shots plus each single-tree setting, asserting on output length + equality so the implications are pinned:

setting 1150805012 1150805013
cascade [efit18, analysis, efit21] 83 rows (efit18) 88 rows (fell through → analysis)
efit18 only 83 0 (absent)
analysis only 62 88
efit21 only 1237 1723

Assertions (all pass):

  • len(efit18[1150805012]) != len(analysis[1150805012]) (83 vs 62) — timeslice count is the primary difference between trees.
  • cascade[1150805012] == efit18[1150805012] — efit18 present, so the cascade short-circuits to it (bit-identical).
  • cascade[1150805012] != analysis[1150805012] — efit18 and analysis are genuinely distinct equilibria.
  • efit18[1150805013] absent — the non-disruptive shot has no efit18 tree.
  • cascade[1150805013] == analysis[1150805013] — so the cascade falls through to analysis (88 == 88).

Verbose log confirms the fallback: Nickname cascade for shot 1150805013: selected 'analysis' after failed: efit18. The test follows the existing on-prem data-test convention (@skip_on_fast_execution), so it runs via make test on a CMOD workstation rather than in CI.

@gtrevisan — re: tagging the selected tree onto the dataset attrs, agreed that belongs in the config→JSON/TOML-next-to-output work, not here. Tracking it for that PR.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants