Description
In Phase.load_case (phase.py:3040), the values are in the recorded timeseries units but options['lower']/['upper'] are in the state's declared units:
prev_state_val = prev_vars[prev_state_path]['val'] # prev_state_units
prev_state_val = prev_state_val.clip(options['lower'], options['upper']) # declared units
self.set_state_val(state_name, vals=..., units=prev_state_units)
A bounded state recorded in non-native units is clipped dimensionally wrong, and the restored trajectory is silently flattened. State declared rad with lower/upper=±0.15 and add_timeseries_output(..., units='deg'), flown 0..0.05 rad — well inside the bounds — restores as 0..0.002618 rad, i.e. the constant 0.15 deg. Repro snippet below.
Unbounded states are fine. Controls take the same clip at phase.py:3069, but are repaired afterwards by Problem.load_case re-applying recorded nodal values. The deprecated module-level dymos.load_case has no clip and restores correctly.
Fix: convert the bounds into prev_state_units before clipping (guarding None for one-sided bounds); same at phase.py:3069.
Found on dymos 1.14.0 / OM 3.40.0; identical output on 1.15.1 / OM 3.45.0
(phase.py:3041) and current master 1.15.2-dev @ 5bfbe39 / OM 3.45.0
(phase.py:3043). Python 3.12, clean venvs.
Example
"""dymos Phase.load_case: bounded state recorded in non-native units is clipped
against the bounds as if the recorded values were in the state's declared units."""
import dymos as dm
import openmdao.api as om
DB = "repro.db"
class ConstantRateODE(om.ExplicitComponent):
def initialize(self):
self.options.declare("num_nodes", types=int)
def setup(self):
nn = self.options["num_nodes"]
self.add_input("theta", shape=(nn,), units="rad")
self.add_output("theta_dot", shape=(nn,), units="rad/s")
self.declare_partials("theta_dot", "theta", dependent=False)
def compute(self, inputs, outputs):
outputs["theta_dot"][:] = 1.0e-3
def build():
p = om.Problem()
traj = p.model.add_subsystem("traj", dm.Trajectory())
phase = traj.add_phase(
"phase0",
dm.Phase(ode_class=ConstantRateODE, transcription=dm.Radau(num_segments=4, order=3)),
)
phase.set_time_options(fix_initial=True, fix_duration=True, units="s")
phase.add_state(
"theta", rate_source="theta_dot", units="rad", fix_initial=True, lower=-0.15, upper=0.15
) # bounds in rad
phase.add_timeseries_output("theta", units="deg") # recorded in deg
p.setup()
phase.set_time_val(initial=0.0, duration=50.0)
phase.set_state_val("theta", [0.0, 0.05])
return p
Fly once (no driver) and record the solution.
p1 = build()
dm.run_problem(p1, run_driver=False, simulate=False, solution_record_file=DB)
flown = p1.get_val("traj.phase0.timeseries.theta", units="rad").ravel()
Restart a fresh copy of the same problem from that case.
case = om.CaseReader(p1.get_outputs_dir() / DB).get_case("final")
p2 = build()
p2.final_setup()
p2.load_case(case)
restored = p2.get_val("traj.phase0.states:theta", units="rad").ravel()
print(f"flown theta : {flown.min():+.6f} .. {flown.max():+.6f} rad (bounds +-0.15 rad)")
print(f"restored theta : {restored.min():+.6f} .. {restored.max():+.6f} rad")
print("0.15 deg in rad:", f"{0.15 * 3.14159265358979 / 180:+.6f}")
Dymos Version
1.15.2-dev
Relevant environment information
No response
Description
In
Phase.load_case(phase.py:3040), the values are in the recorded timeseries units butoptions['lower']/['upper']are in the state's declared units:A bounded state recorded in non-native units is clipped dimensionally wrong, and the restored trajectory is silently flattened. State declared
radwithlower/upper=±0.15andadd_timeseries_output(..., units='deg'), flown 0..0.05 rad — well inside the bounds — restores as 0..0.002618 rad, i.e. the constant 0.15 deg. Repro snippet below.Unbounded states are fine. Controls take the same clip at
phase.py:3069, but are repaired afterwards byProblem.load_casere-applying recorded nodal values. The deprecated module-leveldymos.load_casehas no clip and restores correctly.Fix: convert the bounds into
prev_state_unitsbefore clipping (guardingNonefor one-sided bounds); same atphase.py:3069.Found on dymos 1.14.0 / OM 3.40.0; identical output on 1.15.1 / OM 3.45.0
(
phase.py:3041) and currentmaster1.15.2-dev @5bfbe39/ OM 3.45.0(
phase.py:3043). Python 3.12, clean venvs.Example
"""dymos Phase.load_case: bounded state recorded in non-native units is clipped
against the bounds as if the recorded values were in the state's declared units."""
import dymos as dm
import openmdao.api as om
DB = "repro.db"
class ConstantRateODE(om.ExplicitComponent):
def initialize(self):
self.options.declare("num_nodes", types=int)
def build():
p = om.Problem()
traj = p.model.add_subsystem("traj", dm.Trajectory())
phase = traj.add_phase(
"phase0",
dm.Phase(ode_class=ConstantRateODE, transcription=dm.Radau(num_segments=4, order=3)),
)
phase.set_time_options(fix_initial=True, fix_duration=True, units="s")
phase.add_state(
"theta", rate_source="theta_dot", units="rad", fix_initial=True, lower=-0.15, upper=0.15
) # bounds in rad
phase.add_timeseries_output("theta", units="deg") # recorded in deg
p.setup()
phase.set_time_val(initial=0.0, duration=50.0)
phase.set_state_val("theta", [0.0, 0.05])
return p
Fly once (no driver) and record the solution.
p1 = build()
dm.run_problem(p1, run_driver=False, simulate=False, solution_record_file=DB)
flown = p1.get_val("traj.phase0.timeseries.theta", units="rad").ravel()
Restart a fresh copy of the same problem from that case.
case = om.CaseReader(p1.get_outputs_dir() / DB).get_case("final")
p2 = build()
p2.final_setup()
p2.load_case(case)
restored = p2.get_val("traj.phase0.states:theta", units="rad").ravel()
print(f"flown theta : {flown.min():+.6f} .. {flown.max():+.6f} rad (bounds +-0.15 rad)")
print(f"restored theta : {restored.min():+.6f} .. {restored.max():+.6f} rad")
print("0.15 deg in rad:", f"{0.15 * 3.14159265358979 / 180:+.6f}")
Dymos Version
1.15.2-dev
Relevant environment information
No response