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

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)