Skip to content
Draft
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
117 changes: 117 additions & 0 deletions examples-gallery/plot_multiphase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""
Multiphase Collision
====================

A block is sliding on a surface with Coulomb friction. Push it with a (limited)
rightward force until it hits a wall 1m on the right. When it hits the wall
enforce a Newtonian collision with a coefficient of restitution e so that the
block bounces off the wall and slides backwards to its original location. Apply
this force such that the block reaches its original location in the minimal
amount of time and that each phase has the same duration.

"""

import numpy as np
import sympy as sm
from opty import Problem
import matplotlib.pyplot as plt

# %%
# Start with defining the fixed duration and number of nodes.
num_nodes = 400

# %%
# Symbolic equations of motion, note that we make two sets: one before the #
# collision and one after.
m, e, mu, g, t, h = sm.symbols('m, e, mu, g, t, h', real=True)
xr, vr, xl, vl, F = sm.symbols('x_r, v_r, x_l, v_l, F', cls=sm.Function)

state_symbols = (xr(t), vr(t), xl(t), vl(t))
constant_symbols = (m, e, mu, g)
specified_symbols = (F(t),)

eom = sm.Matrix([
xr(t).diff(t) - vr(t),
m*vr(t).diff(t) + mu*m*g - F(t),
xl(t).diff(t) - vl(t),
m*vl(t).diff(t) - mu*m*g,
])
sm.pprint(eom)

# %%
# Specify the known system parameters.
par_map = {
m: 1.0,
e: 0.8,
mu: 0.6,
g: 9.81,
}


# %%
# Specify the objective function and it's gradient.
def obj(free):
"""Return h (always the last element in the free variables)."""
return free[-1]


def obj_grad(free):
"""Return the gradient of the objective."""
grad = np.zeros_like(free)
grad[-1] = 1.0
return grad


# %%
# Specify the symbolic instance constraints, i.e. initial and end conditions
# using node numbers 0 to N - 1
dur = (num_nodes - 1)*h
instance_constraints = (
xr(0*h) - 0.0,
xr(dur) - 1.0,
vr(0*h) - 0.0,
# TODO : figure out how to let the collision happen at any time, not just
# the halfway point in time
vl(0*h) + e*vr(dur),
xl(0*h) - 1.0,
xl(dur) - 0.0,
)
Copy link
Member Author

Choose a reason for hiding this comment

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

I could change the requirement that the block simply travels 2 meters total and is stationary at the start and the end. Maybe a solution that had varying periods of no motion at the start and end is possible, where these two stationary periods are not equal in time.

Copy link
Contributor

Choose a reason for hiding this comment

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

In your original set-up the block was not at rest at the end, you left its speed open. So I also did it in my simulation.
My set up mimicks exactly your original set-up, except I let opty decide the point of impact - and it does find a better one! :-)

Copy link
Contributor

@Peter230655 Peter230655 Mar 31, 2025

Choose a reason for hiding this comment

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

For clarification:
The plots above were created by moving the time of impact around, from early on in the journey to late in the journey.
But every time the time of impact was predetermined! I only wanted to see if the time of impact relative to total journey had an influence on the length of the journey - otherwise there would be nothing to optimize.
It does have an influence of the length of the journey, so there was a chance for opty to find a good time of impact - which it does.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see how you can move the time of impact in my example that requires the duration before impact to be the same as the duration after impact. That is is hardcoded in my example here.

Copy link
Contributor

@Peter230655 Peter230655 Mar 31, 2025

Choose a reason for hiding this comment

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

I did it like this:
The time of impact is still predetermined, just does not have to be the middle of the journey.

teiler = 1.667
zs = int((num_nodes - 1)/teiler) * h
dur = (num_nodes - 1) * h

instance_constraints = (
    xr(0*h) - 0.0,
    xr(zs) - 1.0,
    vr(0*h) - 0.0,
    vl(zs) + e*vr(zs),
    xl(zs) - x_impact,
    xl(dur) - 0.0,
)

But this is not the main point of my simulation. The main point is that opty decides the time of impact - which it seems to do.
x_impact is trhe location of the wall, x_impact=1.0 in your original example. Of no importance.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I understand, your problem's duration is much longer. It simulates xr past the impact (> ~ 0.11 seconds). That is nonsense for xr, because it shouldn't continue past 1 meter. If you then look at xl at t=0 it start at 2.5 meter (not 1 meter!). So it seems you are not solving the problem as stated.

Copy link
Contributor

Choose a reason for hiding this comment

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

YES, mine is much longer.
I think, it solves the physical problem, but one after another, not simultaneously so to speak.

Now I understand what you meant by different time intervals: To solve simultaneously with t_impact != (tf - t0)/2 one would need different length time intervals.

What is the advantage of doing it simultaneously? I guess saving CPU time?

Copy link
Member Author

@moorepants moorepants Mar 31, 2025

Choose a reason for hiding this comment

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

Ok, I now see what you did and that it is likely a valid solution. I would have never thought about simulating the equations of motion outside of the x = 0 to 1 meter range. But if you relax that restriction (a hack), then you can find a solution.

Copy link
Member Author

Choose a reason for hiding this comment

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

What is the advantage of doing it simultaneously? I guess saving CPU time?

You are solving the problem outside of the bounds of interest. So you have to also ensure that portion can solve. This will depend on CPU as well as your ability to get a solution. It is trivial for this test example, but would not be for a complex model.

Copy link
Contributor

@Peter230655 Peter230655 Mar 31, 2025

Choose a reason for hiding this comment

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

Even with this simple example, when I moved t_event too close to t0 or to tf, it would not find a solution easily.
(But I did not pay too much attention to this: for just about any simulation I have played around with, it took some trying different things before it would converge...)

From this step it was only a small step to let opty find t_impact. But again it took trying things before it would converge.
My feel is, it is hard to judge from the equations, whether a problem is hard: The brachistochrone were very simple equations, but hard to get a solution.


bounds = {
F(t): (0.0, 160.0),
h: (0.0, 0.5),
vr(t): (0.0, np.inf),
vl(t): (-np.inf, 0.0),
}

# %%
# Create an optimization problem.
prob = Problem(obj, obj_grad, eom, state_symbols, num_nodes, h,
known_parameter_map=par_map,
instance_constraints=instance_constraints,
time_symbol=t,
bounds=bounds)

# %%
# Use a zero as an initial guess.
initial_guess = np.zeros(prob.num_free)

# %%
# Find the optimal solution.
solution, info = prob.solve(initial_guess)
print(info['status_msg'])
print(info['obj_val'])

# %%
# Plot the optimal state and input trajectories.
prob.plot_trajectories(solution)

# %%
# Plot the constraint violations.
prob.plot_constraint_violations(solution)

# %%
# Plot the objective function as a function of optimizer iteration.
prob.plot_objective_value()

plt.show()
Loading