-
Notifications
You must be signed in to change notification settings - Fork 0
Add resonance flag diagnostics #39
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| """Resonance diagnostics for the linearised bend theory. | ||
|
|
||
| The reduced model has a resonant aspect ratio ``beta_R`` at which the | ||
| fundamental mode's streamwise decay rate changes sign. Below it the flow | ||
| response is downstream-dominated (sub-resonant); above it, upstream-dominated | ||
| (super-resonant). Which side a run sits on controls the qualitative behaviour | ||
| of bend migration, so it is worth reporting rather than leaving implicit. | ||
|
|
||
| ``beta_R`` depends on the other physical inputs. For flagbed = 2, r = 0.5: | ||
|
|
||
| theta0 = 0.20 beta_R ~ 8.97 | ||
| theta0 = 0.30 beta_R ~ 9.68 | ||
| theta0 = 0.50 beta_R ~ 9.50 | ||
| ds = 0.002 beta_R ~ 11.34 | ||
| ds = 0.005 beta_R ~ 9.68 | ||
| ds = 0.020 beta_R ~ 7.19 | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import numpy as np | ||
|
|
||
| from .flowfield import _precompute_modes | ||
| from .resistance import resistance_function_flagbed | ||
|
|
||
| #: Relative distance from beta_R within which a run is reported as near-resonant. | ||
| NEAR_RESONANCE_BAND = 0.02 | ||
|
|
||
|
|
||
| def fundamental_decay_rate( | ||
| beta: float, | ||
| theta0: float, | ||
| ds: float, | ||
| rpic_0: float, | ||
| flagbed: int = 2, | ||
| Mdat: int = 6, | ||
| ) -> float: | ||
| """Return ``Re(lambda2)`` for the fundamental lateral mode. | ||
|
|
||
| Negative values are sub-resonant; positive values are super-resonant. | ||
| """ | ||
| rpic, cf0, ct, cd, phit, phid, f0 = resistance_function_flagbed( | ||
| int(flagbed), float(theta0), float(ds), float(rpic_0) | ||
| ) | ||
| result = _precompute_modes( | ||
| cf0, | ||
| ct, | ||
| cd, | ||
| phit, | ||
| phid, | ||
| float(beta), | ||
| rpic, | ||
| float(theta0), | ||
| f0, | ||
| int(Mdat), | ||
| ) | ||
| lamb2 = result[2] | ||
| if len(lamb2) == 0: | ||
| return float("nan") | ||
| return float(np.real(lamb2[0])) | ||
|
|
||
|
|
||
| def resonant_aspect_ratio( | ||
| theta0: float, | ||
| ds: float, | ||
| rpic_0: float, | ||
| flagbed: int = 2, | ||
| Mdat: int = 6, | ||
| bracket: tuple[float, float] = (2.0, 200.0), | ||
| tolerance: float = 1.0e-6, | ||
| ) -> float | None: | ||
| """Bisect for the ``beta`` at which the fundamental decay rate vanishes. | ||
|
|
||
| Returns ``None`` when the bracket does not contain a sign change. | ||
| """ | ||
| lo, hi = float(bracket[0]), float(bracket[1]) | ||
| args = (theta0, ds, rpic_0, flagbed, Mdat) | ||
| f_lo = fundamental_decay_rate(lo, *args) | ||
| f_hi = fundamental_decay_rate(hi, *args) | ||
| if not np.isfinite(f_lo) or not np.isfinite(f_hi) or f_lo * f_hi > 0.0: | ||
| return None | ||
|
|
||
| while (hi - lo) > tolerance * max(1.0, lo): | ||
| mid = 0.5 * (lo + hi) | ||
| f_mid = fundamental_decay_rate(mid, *args) | ||
| if not np.isfinite(f_mid): | ||
| return None | ||
| if f_mid * f_lo > 0.0: | ||
| lo = mid | ||
| f_lo = f_mid | ||
| else: | ||
| hi = mid | ||
|
|
||
| return 0.5 * (lo + hi) | ||
|
|
||
|
|
||
| def resonance_report( | ||
| beta: float, | ||
| theta0: float, | ||
| ds: float, | ||
| rpic_0: float, | ||
| flagbed: int = 2, | ||
| Mdat: int = 6, | ||
| ) -> dict: | ||
| """Summarise where a run sits relative to resonance.""" | ||
| beta = float(beta) | ||
| args = (theta0, ds, rpic_0, flagbed, Mdat) | ||
| decay = fundamental_decay_rate(beta, *args) | ||
| beta_r = resonant_aspect_ratio(*args) | ||
|
|
||
| if decay < 0.0: | ||
| state = "sub-resonant" | ||
| elif decay > 0.0: | ||
| state = "super-resonant" | ||
| else: | ||
| state = "resonant" | ||
|
|
||
| distance = None | ||
| if beta_r is not None and beta_r > 0.0: | ||
| distance = beta / beta_r - 1.0 | ||
| if abs(distance) <= NEAR_RESONANCE_BAND: | ||
| state = "near-resonant" | ||
|
|
||
| if decay == 0.0: | ||
| influence_length = float("inf") | ||
| elif np.isfinite(decay): | ||
| influence_length = 1.0 / abs(decay) | ||
| else: | ||
| influence_length = float("nan") | ||
|
|
||
| return { | ||
| "state": state, | ||
| "flag": 1 if decay < 0.0 else -1, | ||
| "beta": beta, | ||
| "resonant_beta": beta_r, | ||
| "relative_distance_to_resonance": distance, | ||
| "fundamental_decay_rate": decay, | ||
| "influence_length_half_widths": float(influence_length), | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| """The resonance flag must track the fundamental mode, not mode 6. | ||
|
|
||
| ``_compute_flag`` previously read ``lamb2[5]``. ``Re(lambda2)`` for mode 6 is | ||
| roughly -5 to -13 across realistic aspect ratios, so the flag reported | ||
| "sub-resonant" unconditionally. The fundamental mode is the one that changes | ||
| sign at the resonant aspect ratio. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import numpy as np | ||
| import pytest | ||
|
|
||
| from ldsfl.flowfield import _compute_flag, _precompute_modes | ||
| from ldsfl.flowfield_periodic import _compute_flag as _compute_periodic_flag | ||
| from ldsfl.resistance import resistance_function_flagbed | ||
| from ldsfl.resonance import ( | ||
| fundamental_decay_rate, | ||
| resonance_report, | ||
| resonant_aspect_ratio, | ||
| ) | ||
|
|
||
| THETA0 = 0.3 | ||
| DS = 0.005 | ||
| RPIC0 = 0.5 | ||
|
|
||
|
|
||
| def _lamb2(beta: float, mdat: int = 6): | ||
| rpic, cf0, ct, cd, phit, phid, f0 = resistance_function_flagbed(2, THETA0, DS, RPIC0) | ||
| return _precompute_modes(cf0, ct, cd, phit, phid, beta, rpic, THETA0, f0, mdat)[2] | ||
|
|
||
|
|
||
| def test_free_and_periodic_flags_change_sign_across_the_resonant_aspect_ratio(): | ||
| beta_r = resonant_aspect_ratio(THETA0, DS, RPIC0) | ||
| assert beta_r is not None | ||
|
|
||
| below = _lamb2(beta_r * 0.9) | ||
| above = _lamb2(beta_r * 1.1) | ||
|
|
||
| assert _compute_flag(below) == 1, "expected sub-resonant below beta_R" | ||
| assert _compute_flag(above) == -1, "expected super-resonant above beta_R" | ||
| assert _compute_periodic_flag(below) == 1, "periodic flag should use the same convention" | ||
| assert _compute_periodic_flag(above) == -1, "periodic flag should use the same convention" | ||
|
|
||
|
|
||
| def test_flag_is_not_constant_over_the_realistic_aspect_ratio_range(): | ||
| """The previous implementation returned +1 for every one of these.""" | ||
| flags = {_compute_flag(_lamb2(beta)) for beta in (4.0, 6.0, 9.0, 12.0, 20.0, 40.0)} | ||
| assert flags == {1, -1} | ||
|
|
||
|
|
||
| def test_mode_six_decay_rate_is_uninformative(): | ||
| """Documents why indexing mode 6 could never work.""" | ||
| for beta in (4.0, 9.0, 20.0, 40.0): | ||
| assert _lamb2(beta)[5].real < -1.0 | ||
|
|
||
|
|
||
| def test_resonant_aspect_ratio_matches_the_sign_change(): | ||
| beta_r = resonant_aspect_ratio(THETA0, DS, RPIC0) | ||
| assert beta_r is not None | ||
| assert fundamental_decay_rate(beta_r * 0.99, THETA0, DS, RPIC0) < 0.0 | ||
| assert fundamental_decay_rate(beta_r * 1.01, THETA0, DS, RPIC0) > 0.0 | ||
|
|
||
|
|
||
| def test_resonant_aspect_ratio_moves_with_grain_size(): | ||
| coarse = resonant_aspect_ratio(THETA0, 0.02, RPIC0) | ||
| fine = resonant_aspect_ratio(THETA0, 0.002, RPIC0) | ||
| assert coarse is not None and fine is not None | ||
| assert fine > coarse, "beta_R should increase as relative grain size decreases" | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "beta, expected", | ||
| [ | ||
| (6.0, "sub-resonant"), | ||
| (9.0, "sub-resonant"), | ||
| (12.0, "super-resonant"), | ||
| (20.0, "super-resonant"), | ||
| ], | ||
| ) | ||
| def test_resonance_report_states(beta, expected): | ||
| assert resonance_report(beta, THETA0, DS, RPIC0)["state"] == expected | ||
|
|
||
|
|
||
| def test_influence_length_grows_towards_resonance(): | ||
| """A domain must be long compared with 1/|Re(lambda)|, which diverges at beta_R.""" | ||
| beta_r = resonant_aspect_ratio(THETA0, DS, RPIC0) | ||
| assert beta_r is not None | ||
| far = resonance_report(6.0, THETA0, DS, RPIC0)["influence_length_half_widths"] | ||
| near = resonance_report(beta_r * 0.999, THETA0, DS, RPIC0)["influence_length_half_widths"] | ||
| assert np.isfinite(far) | ||
| assert near > 10.0 * far | ||
|
|
||
|
|
||
| def test_resonance_report_is_included_in_run_summary(tmp_path): | ||
| from ldsfl.main import run_case | ||
|
|
||
| input_dir = tmp_path / "Input" | ||
| input_dir.mkdir() | ||
| (input_dir / "Parameter.csv").write_text( | ||
| "Id,Beta,ds,Thetha,flagbed,r,Mdat,flagbed=1 plane; flagbed=2 dunes\n" | ||
| "1,9.0,0.005,0.3,2,0.5,6,2\n", | ||
| encoding="utf-8", | ||
| ) | ||
| (input_dir / "xy.csv").write_text( | ||
| "0,0\n1,0.01\n2,0.03\n3,0.06\n4,0.08\n5,0.09\n", | ||
| encoding="utf-8", | ||
| ) | ||
|
|
||
| result = run_case(tmp_path, 1, max_steps=2, Nprint=2, do_plots=False) | ||
|
|
||
| report = result["resonance"] | ||
| assert report["state"] in {"sub-resonant", "near-resonant", "super-resonant", "resonant"} | ||
| assert report["flag"] in {1, -1} | ||
| assert report["resonant_beta"] is None or report["resonant_beta"] > 0.0 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For every
run_casesummary this line now bisectsbeta_R, which callsfundamental_decay_ratedozens of times; each of those calls recomputes the resistance parameters and runs_precompute_modes, including the expensivek0123vertical integration. This adds seconds of post-processing even to very short CLI/GUI runs and scales across multi-case projects, although the bisection only needs the same fixed physical coefficients and the first lateral mode. Consider reusing the precomputed coefficients or a lighter single-mode/root function for the diagnostic.Useful? React with 👍 / 👎.