Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/condor/contrib.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,10 @@ def resample(self, dt, include_output=True, include_events=True, max_deg=3):
# TODO figure out how to get root info

ts_to_call = new_self.t[idx0:idx1]
xs[idx0:idx1] = x_interp_segment(ts_to_call)
interp_result = x_interp_segment(ts_to_call)
if interp_result.ndim == 1:
interp_result = interp_result[:, np.newaxis]
xs[idx0:idx1] = interp_result
if include_output:
for idx, t, x in zip(range(idx0, idx1), ts_to_call, xs[idx0:idx1]):
ys[idx, None] = dynamic_output(p, t, x).T
Expand Down
104 changes: 104 additions & 0 deletions tests/test_trajectory_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,107 @@ class Sim(odesys.TrajectoryAnalysis):
tf = 10

Sim(wn=10, u_hold=0.8)


class TestResampleSingleState:
"""Tests for TrajectoryAnalysis.resample with single-state ODEs (issue #70)."""

def test_resample_single_state(self):
"""Exact reproduction of issue #70: single-state ODE resample crashes."""
class ODE(co.ODESystem):
a = parameter()
x = state()
dot[x] = -a * x

class Traj(ODE.TrajectoryAnalysis):
tf = 10
initial[x] = 1

sim = Traj(a=0.5)
resampled = sim.resample(1.0, include_events=False)
assert resampled.t.size > 0

def test_resample_single_state_values(self):
"""Verify resampled values follow exponential decay."""
class ODE(co.ODESystem):
a = parameter()
x = state()
dot[x] = -a * x

class Traj(ODE.TrajectoryAnalysis):
tf = 10
initial[x] = 1

sim = Traj(a=0.5)
resampled = sim.resample(1.0, include_events=False)
expected = np.exp(-0.5 * resampled.t)
np.testing.assert_allclose(resampled.x, expected, rtol=1e-4)

def test_resample_single_state_no_output(self):
class ODE(co.ODESystem):
a = parameter()
x = state()
dot[x] = -a * x

class Traj(ODE.TrajectoryAnalysis):
tf = 10
initial[x] = 1

sim = Traj(a=0.5)
resampled = sim.resample(1.0, include_events=False, include_output=False)
assert resampled.t.size > 0
Comment on lines +419 to +431

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

include_output=False path is not truly exercised in this test.

At Line 430 you pass include_output=False, but the ODE in Lines 420-423 has no dynamic_output. In src/condor/contrib.py (Lines 693-750), include_output is gated by model.dynamic_output._count, so this ends up on the same effective path as the basic single-state test.

Suggested adjustment
     def test_resample_single_state_no_output(self):
         class ODE(co.ODESystem):
             a = parameter()
             x = state()
+            dynamic_output.velocity = -a * x
             dot[x] = -a * x

         class Traj(ODE.TrajectoryAnalysis):
             tf = 10
             initial[x] = 1

         sim = Traj(a=0.5)
         resampled = sim.resample(1.0, include_events=False, include_output=False)
         assert resampled.t.size > 0
🧰 Tools
🪛 Ruff (0.15.6)

[error] 421-421: Undefined name parameter

(F821)


[error] 422-422: Undefined name state

(F821)


[error] 423-423: Undefined name dot

(F821)


[error] 427-427: Undefined name initial

(F821)


[error] 427-427: Undefined name x

(F821)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_trajectory_analysis.py` around lines 419 - 431, The test
test_resample_single_state_no_output currently passes include_output=False but
the ODE class has no dynamic_output, so the include_output=False branch in
resample isn't exercised; modify the test's ODE (the ODE class used by Traj) to
declare at least one dynamic_output (use the dynamic_output symbol) so that when
you call Traj(...).resample(..., include_output=False, include_events=False) the
code path that checks model.dynamic_output._count and skips building outputs is
actually exercised; ensure the Traj initial/parameter setup remains the same and
keep the assertion on resampled.t.size.


def test_resample_single_state_with_dynamic_output(self):
"""Single-state ODE with dynamic_output — tests .T reshape path."""
class ODE(co.ODESystem):
a = parameter()
x = state()
dynamic_output.velocity = -a * x
dot[x] = -a * x

class Traj(ODE.TrajectoryAnalysis):
tf = 10
initial[x] = 1
vel = dynamic_output.velocity

sim = Traj(a=0.5)
resampled = sim.resample(1.0, include_events=False, include_output=True)
assert resampled.t.size > 0
# velocity = -a * x = -0.5 * exp(-0.5 * t)
expected_vel = -0.5 * np.exp(-0.5 * resampled.t)
np.testing.assert_allclose(resampled.velocity, expected_vel, rtol=1e-4)

def test_resample_single_state_small_dt(self):
"""Smaller dt produces more points."""
class ODE(co.ODESystem):
a = parameter()
x = state()
dot[x] = -a * x

class Traj(ODE.TrajectoryAnalysis):
tf = 10
initial[x] = 1

sim = Traj(a=0.5)
r1 = sim.resample(2.0, include_events=False)
r2 = sim.resample(0.5, include_events=False)
assert r2.t.size > r1.t.size

def test_resample_multi_state_still_works(self):
"""Ensure multi-state ODEs are not broken by the ndim fix."""
class ODE(co.ODESystem):
x = state()
v = state()
dot[x] = v
dot[v] = -x

class Traj(ODE.TrajectoryAnalysis):
tf = 10
initial[x] = 1
initial[v] = 0

sim = Traj()
resampled = sim.resample(0.5, include_events=False)
assert resampled.t.size > 0
# Simple harmonic oscillator: x(t) = cos(t)
np.testing.assert_allclose(resampled.x, np.cos(resampled.t), rtol=1e-3)