Skip to content

Bug: _try_relax_step triggers infinite logging loop when step size < minStep #75

@GLY2024

Description

@GLY2024

Environment

opstool version: 1.0.26
Python version: 3.12
OS: Windows 10

Description

When using SmartAnalyze for transient analysis, if the step size converges to a value smaller than minStep, the _try_relax_step strategy enters a state where it repeatedly prints the failure message without properly exiting or falling back to other strategies. This causes the outer loop to call TransientAnalyze indefinitely, generating massive redundant logs.

Reproduction Steps

  1. Create an OpenSees model with convergence difficulties.
  2. Run transient analysis using SmartAnalyze.
  3. Observe the output once the solver struggles to converge.

Actual Behavior (Logs)

🚀 OPSTOOL::SmartAnalyze:  14%|██████▍ | 1390/10000 [05:03<5:11:49, 2.17s/ step]
>>> ✳️ OPSTOOL::SmartAnalyze:: Current step 3.432e-04 is below the min step 4.304e-04.
🚀 OPSTOOL::SmartAnalyze: 100%|█████████████████████████████████████████████████| 1390/1390 [05:06<00:00, 4.53 step/s]
>>> ❌ OPSTOOL::SmartAnalyze:: Analyze failed. Time consumption: 306.680 s.
>>> ✳️ OPSTOOL::SmartAnalyze:: Current step 3.432e-04 is below the min step 4.304e-04.
>>> ❌ OPSTOOL::SmartAnalyze:: Analyze failed. Time consumption: 310.019 s.
... (Infinite repetition)

Root Cause

In _smart_analyze.py (approx. lines 788-797), the _try_relax_step method returns -1 immediately when step_try < min_step without attempting a final try at the min_step limit. This causes the strategy to fail early and "trap" the outer loop in a repetitive failure path.

Expected Behavior

  1. If step_try < minStep, it should be clamped to minStep and attempted one last time.
  2. If the analysis still fails at minStep, the strategy should be abandoned to allow _analyze() to try fallback methods (e.g., _try_loose_test_tol).
  3. The final sub-step (step_remaining < minStep) should be allowed to run without the minStep restriction.

Suggested Fix

Modify the logic in _try_relax_step as follows:

ok = -1
while step_remaining > self.eps:
    # Clamp step_try to min_step, but allow final sub-step (step_remaining < min_step) to be smaller
    clamped_to_min = False
    if step_try < min_step and step_remaining >= min_step:
        if verbose:
            print(f">>> ✳️ {self.logo} Step {step_try:.3e} below min step, clamping to {min_step:.3e}.")
        step_try = min_step
        clamped_to_min = True

    if step_try > step_remaining:
        step_try = step_remaining  # avoid overshooting

    # Try to run one substep
    ok = self._analyze_one_step(step_try, verbose=verbose)

    if ok == 0:
        step_remaining -= step_try
        step_try = step_remaining
        # ... verbose output ...
    else:
        # If we already clamped to min_step and still failed, give up this strategy
        if clamped_to_min:
            if verbose:
                print(f">>> ✳️ {self.logo} Failed at min step {min_step:.3e}, giving up relax_step strategy.")
            return -1
        # Shrink step_try and continue
        step_try *= alpha
        # ... verbose output ...
return ok

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions