How to use solve_ivp to integrate harmonic oscillator with static and kinetic friction zone in a robust way? - scipy

I would like to integrate the equation of motion for the harmonic oscillator with a static and kinetic friction zone (as depicted in the following image).
For x < a the mass slides without friction over the surface. For x > a static and kinetic friction takes effect, with its respective coefficients of friction mu0 (static) and mu (kinetic). I tried to implement this with scipy.integrate.solve_ivp in a while-loop, making use of solve_ivp's event handling, stopping and restarting the integration each time one of the following events gets triggered:
the oscillator's velocity is zero (mass_stops in code below)
the oscillator reaches x=a and has positive velocity, i.e. entering the friction zone (entering_friction_zone)
the oscillator reaches x=a and has negative velocity, i.e. leaving the friction zone (leaving_friction_zone)
Using this approach, I faced the same problem as other users already did, documented here and here, namely that the initial conditions (inits) trigger an event and the while-loop gets stuck.
Using one of the workarounds, as described in the second thread, it kind of works but isn't as robust as I would like it to be. To be more specific, I now integrate the equations (initially and after each occurrence of an event) for a very short time interval (1e-13) and use the final state of this integration (sol_in_between) as initial condition for the "real integration" (sol). Of course I don't know beforehand how short the time interval needs to be for it to work. Too short of an interval results in an infinite loop (as before), too large of an interval and the results become crude.
Now I'm wondering if there is a more straight forward and more importantly a more robust way to integrate the given system, ideally using solve_ivp but I'm open for other suggestions.
Code:
import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
import os, sys
def system(t,xv,params):
"""
INPUT: current time t, current position x & velocity v in list xv = [x, v],
current system parameter in dict params
OUTPUT: xdot and vdot in list f = [xdot, vdot]"""
x, v = xv
m, c, g, r, muq = [params[par] for par in ['m', 'c', 'g', 'r', 'muq']]
return [v, -c*x/m - r*muq*g]
def mass_stops(t,xv,params):
"""Event function for event 'velocity zero'"""
x, v = xv
return v
mass_stops.terminal = True
mass_stops.direction = 0
def leaving_friction_zone(t,xv,params):
"""Event function for event 'mass crosses border with positive velocity', i.e. leaving friction zone"""
x, v = xv
return x-params['a']
leaving_friction_zone.terminal = True
leaving_friction_zone.direction = 1
def entering_friction_zone(t,xv,params):
"""Event function for event 'mass crosses border with negative velocity', i.e. entering friction zone"""
x, v = xv
return x-params['a']
entering_friction_zone.terminal = True
entering_friction_zone.direction = -1
def event_number(events):
"""Helper function for identifying which event got triggered"""
for i, event in enumerate(events):
if event.size > 0: return i
def integrate_oscillator(kinet_friction_coeff=.4, static_friction_coeff=.4, tend=20, m=20, c=300, a=.5, g=9.81, inits=[1, 0]):
mu = kinet_friction_coeff
mu0 = static_friction_coeff
params = {'m': m, 'c': c, 'g': g, 'a': a}
tspan = [0, tend]
params['r'] = -1 if inits[0] > 0 else 1
params['muq'] = mu if inits[0] > a else 0
if params['muq'] > 0:
print('friction')
else:
print('no friction')
tout = list()
x = list()
v = list()
CONT = 1
while CONT == 1:
# we integrate the equation of motion for a short time interval without event handling, HOPING that the initial/last event doesn't get triggered again in sol
# we use the end state of this integration as initial condition for the "actual integration" and throw everything else away
sol_in_between = solve_ivp(system, [0, 1e-13], inits, args=(params,), rtol=1e-9, atol=1e-12)
inits = sol_in_between.y[:,-1]
sol = solve_ivp(system, tspan, inits, args=(params,), events=[mass_stops,leaving_friction_zone,entering_friction_zone], rtol=1e-9, atol=1e-12)
tout = np.concatenate([tout, sol.t])
x = np.concatenate([x, sol.y[0,:]]) #x.append(sol.y[0,:])
v = np.concatenate([v, sol.y[1,:]]) #y.append(sol.y[1,:])
tnow = tout[-1]
if tnow >= tend:
CONT = 0
else:
tspan = [tnow, tend]
inits = [x[-1], v[-1]]
#CONT = 0
event_num = event_number(sol.y_events)
if event_num == 0: # Event: "velocity zero"
print('Velocity zero')
if np.abs(c*x[-1]) < mu0*m*g and x[-1] > a: # spring force is smaller than static friction force...
CONT = 0 # ... therefore stop!
print(f'Mass stopped at time t={tout[-1]}')
tout = np.concatenate([tout, np.array([tend])])
x = np.concatenate([x, np.array([x[-1]])])
v = np.concatenate([v, np.array([0])])
elif x[-1] - a < 0: # mass moves to the right
print('to the right')
params['r'] = 1 # change direction of kinetic friction force to the left
else: # mass moves to the left
print('to the left')
params['r'] = -1 # change direction of kinetic friction force to the right
elif event_num == 1: # entering friction zone
print('entering friction zone')
params['muq'] = mu
elif event_num == 2: # leaving friction zone
print('leaving friction zone')
params['muq'] = 0
return [tout,x,v]
[t,x,v] = integrate_oscillator(kinet_friction_coeff=.4,static_friction_coeff=.8,tend=50,inits=[1,0])
plt.rcParams['font.size'] = 16
fig, (ax1, ax2) = plt.subplots(2,figsize=(12,9),sharex=True )
figsize=(9,6)
ax1.plot(t,x)
ax1.grid(True)
ax1.plot([0,t[-1]], [.5, .5])
ax2.plot(t,v)
ax2.grid(True)

Related

Velocity per second for a free falling object

Edit: MATLAB.
I'm trying to create a function which returns an array of the velocity per second for a free falling object. The input argument for the function is the height h. Also, if the object hits the ground before 1 second, it should return the velocity for the time it hits the ground.
Example: If the object falls from 80 meters, the function should return
v =
9.8 19.6 29.4 39.2
My attempt looks like this:
function freefall(h)
g = 9.8; % gravity acceleration
h_t = linspace(0,h); % the height should start from 0 to input height h
t = sqrt(2.*h_t/g); % time for the free fall
n=0;
if t < 1
velocity = sqrt(2*g*h)
disp(velocity)
else
for time = n+1 % An attempt to try making t into integers(tried floor(t) but didn't work)
v = g.*time
while time <= t % Don't want the time to exceed the amount of time it takes for object to land
disp(v)
end
end
end
end
The output just becomes v = 9.82, and I'm out of ideas to try and make this work.
Is this all you are trying to do?
tfinal = sqrt(2*h/g); % time to fall to ground
if tfinal < 1
velocity = g*tfinal; % velocity when hitting ground is less than 1 sec
else
velocity = g*(1:tfinal); % velocities at 1 sec intervals, not including hitting ground
end
If you always wanted to include the final velocity in the output, you could do this instead:
tfinal = sqrt(2*h/g); % time to fall to ground
t = 1:tfinal; % range of integer times spaced by 1 sec
if tfinal ~= fix(tfinal) % if final time is not an integer
t = [t,tfinal]; % append final time to time array
end
velocity = g*t; % velocities at integer times plus final time
I don't have matlab or scilab at the moment so I fiddled around using online scilab to write some test code. Here what I came up with:
function freefall(h)
g = 9.8;
t = floor(sqrt(2*h/g));
if t < 1
t = 1;
t_mat = linspace(1, t, t);
t_mat = g * t_mat;
return t_mat;
end
You can then call that function this way:
disp(freefall(80));
What's missing on your code is rather to limit the increments of linspace, for some odd reason you tried to compare a vector to a scalar (that if t < 1 on your code), and finally your function did not return an answer, instead it prints it.
¯\(ツ)/¯ correct me if I'm wrong, I haven't write any matlab code for like 6 years.
Note: that online scilab doesn't seem to support function so, you have to use bare code without the function block. You can replace that return with disp(t_mat).

Use VectorContinuousCallback to take values of array to zero in Julia

I’m having trouble getting VectorContinuousCallback to work as desired and I’m not sure what I’m doing wrong. I have a large system of equations, and essentially, any time any of the values cross some threshold value (in my system it’s 10e-30 but in this reprex 0.05), I want the value to go to zero.
That is, if at any point the values of u go below 0.05, I want the callback to take the value to zero, but right now, the solver seems to just almost ignore the callback? Not any of the crosses of the threshold are recognized.
A reprex:
using DifferentialEquations, Plots
function biomass_sim!(du, u, p, t)
# change = growth + gain from eating - loss from eating - loss
du[1] = 0.2*u[1] + (0.1*u[2] + 0.15*u[3]) - (0.2*u[4]) - 0.9*u[1]
du[2] = 0.2*u[2] + (0.1*u[1] + 0.05*u[3]) - (0.1*u[1] + 0.4*u[4]) - 0.5*u[2]
du[3] = 1.2*u[3] + 0 - (0.15*u[1] + 0.005*u[2]) - 1.3*u[3]
du[4] = 0.2*u[4] + (0.2*u[1] + 0.4*u[2]) - 1.9*u[1]
end
# set up extinction callback
function extinction_threshold(out,u,t,integrator)
# loop through all species to make the condition check all of them
for i in 1:4
out[i] = 0.05 - u[i]
end
end
function extinction_affect!(integrator, event_idx)
# loop again through all species
for i in 1:4
if event_idx == i
integrator.u[i] = 0
end
end
end
extinction_callback =
VectorContinuousCallback(extinction_threshold,
extinction_affect!,
4,
save_positions = (true, true),
interp_points = 1000
)
tspan = (0.0, 10.0)
u0 = [10, 10, 10, 10]
prob = ODEProblem(biomass_sim!,
u0,
tspan)
sol= solve(prob,
Tsit5(),
abstol = 1e-15,
reltol = 1e-10,
callback = extinction_callback,
progress = true,
progress_steps = 1)
plot(sol)
What I want to see here is that the two values that DO cross the threshold to be < 0.05 at least visually (u3 and u4 clearly go below zero), I want those values to become zero.
The application of this is an extinction to a species, so if they go below some threshold, I want to consider them extinct and therefore not able to be consumed by other species.
I’ve tried changing the tolerances, using different solvers (I’m not married to Tsit5()), but have yet to find a way to do this.
Any help much appreciated!!
The full output:
retcode: Success
Interpolation: specialized 4th order "free" interpolation
t: 165-element Vector{Float64}:
0.0
0.004242012928189618
0.01597661154828718
0.03189297583643294
0.050808376324350105
⋮
9.758563212772982
9.850431863368996
9.94240515017787
10.0
u: 165-element Vector{Vector{Float64}}:
[10.0, 10.0, 10.0, 10.0]
[9.972478301795496, 9.97248284719235, 9.98919421326857, 9.953394015118882]
[9.89687871881005, 9.896943262844019, 9.95942008072302, 9.825050883392004]
[9.795579619066798, 9.795837189358107, 9.919309541156514, 9.652323759097303]
[9.677029343447844, 9.67768414040866, 9.872046236050455, 9.449045554530718]
⋮
[43.86029800419986, 110.54225328286441, -12.173991695732434, -186.40702483057268]
[45.33660997599057, 114.35164541304869, -12.725800474246844, -192.72257104623995]
[46.86398454218351, 118.2922142830212, -13.295579652115606, -199.25572621838901]
[47.84633546050675, 120.82634905853745, -13.661479860003494, -203.45720035095707]
Answered in https://github.com/SciML/DifferentialEquations.jl/issues/843. This was a "user error". When you check the callback:
function extinction_affect!(integrator, event_idx)
# loop again through all species
#show integrator.u,event_idx
for i in 1:4
if event_idx == i
integrator.u[i] = 0
end
end
biomass_sim!(get_tmp_cache(integrator)[1], integrator.u, integrator.p, integrator.t)
#show get_tmp_cache(integrator)[1]
end
It is definitely called and does exactly as intended.
(integrator.u, event_idx) = ([5.347462662161639, 5.731062469090074, 7.64667777801325, 0.05000000000000008], 4)
(get_tmp_cache(integrator))[1] = [-2.0231159499021523, -1.3369848518263594, -1.5954424894710222, -6.798261538038756]
(integrator.u, event_idx) = ([12.968499097445866, 30.506371944743357, 0.050000000000001314, -53.521085634736835], 3)
(get_tmp_cache(integrator))[1] = [4.676904953209599, 12.256522670471728, -2.0978067243405967, -20.548116814707996]
but what this also shows is that, even if u[4] = 0, du[4] < 0 and so it's clear why it goes negative: that's due to how the ODE is defined. You should flip a parameter or something to make the derivative = 0 if you want to keep it at zero past the callback point.

Kalman Filter (pykalman): Value for obs_covariance and model without intercept

I am looking at the KalmanFilter from pykalman shown in examples:
pykalman documentation
Example 1
Example 2
and I am wondering
observation_covariance=100,
vs
observation_covariance=1,
the documentation states
observation_covariance R: e(t)^2 ~ Gaussian (0, R)
How should the value be set here correctly?
Additionally, is it possible to apply the Kalman filter without intercept in the above module?
The observation covariance shows how much error you assume to be in your input data. Kalman filter works fine on normally distributed data. Under this assumption you can use the 3-Sigma rule to calculate the covariance (in this case the variance) of your observation based on the maximum error in the observation.
The values in your question can be interpreted as follows:
Example 1
observation_covariance = 100
sigma = sqrt(observation_covariance) = 10
max_error = 3*sigma = 30
Example 2
observation_covariance = 1
sigma = sqrt(observation_covariance) = 1
max_error = 3*sigma = 3
So you need to choose the value based on your observation data. The more accurate the observation, the smaller the observation covariance.
Another point: you can tune your filter by manipulating the covariance, but I think it's not a good idea. The higher the observation covariance value the weaker impact a new observation has on the filter state.
Sorry, I did not understand the second part of your question (about the Kalman Filter without intercept). Could you please explain what you mean?
You are trying to use a regression model and both intercept and slope belong to it.
---------------------------
UPDATE
I prepared some code and plots to answer your questions in details. I used EWC and EWA historical data to stay close to the original article.
First of all here is the code (pretty the same one as in the examples above but with a different notation)
from pykalman import KalmanFilter
import numpy as np
import matplotlib.pyplot as plt
# reading data (quick and dirty)
Datum=[]
EWA=[]
EWC=[]
for line in open('data/dataset.csv'):
f1, f2, f3 = line.split(';')
Datum.append(f1)
EWA.append(float(f2))
EWC.append(float(f3))
n = len(Datum)
# Filter Configuration
# both slope and intercept have to be estimated
# transition_matrix
F = np.eye(2) # identity matrix because x_(k+1) = x_(k) + noise
# observation_matrix
# H_k = [EWA_k 1]
H = np.vstack([np.matrix(EWA), np.ones((1, n))]).T[:, np.newaxis]
# transition_covariance
Q = [[1e-4, 0],
[ 0, 1e-4]]
# observation_covariance
R = 1 # max error = 3
# initial_state_mean
X0 = [0,
0]
# initial_state_covariance
P0 = [[ 1, 0],
[ 0, 1]]
# Kalman-Filter initialization
kf = KalmanFilter(n_dim_obs=1, n_dim_state=2,
transition_matrices = F,
observation_matrices = H,
transition_covariance = Q,
observation_covariance = R,
initial_state_mean = X0,
initial_state_covariance = P0)
# Filtering
state_means, state_covs = kf.filter(EWC)
# Restore EWC based on EWA and estimated parameters
EWC_restored = np.multiply(EWA, state_means[:, 0]) + state_means[:, 1]
# Plots
plt.figure(1)
ax1 = plt.subplot(211)
plt.plot(state_means[:, 0], label="Slope")
plt.grid()
plt.legend(loc="upper left")
ax2 = plt.subplot(212)
plt.plot(state_means[:, 1], label="Intercept")
plt.grid()
plt.legend(loc="upper left")
# check the result
plt.figure(2)
plt.plot(EWC, label="EWC original")
plt.plot(EWC_restored, label="EWC restored")
plt.grid()
plt.legend(loc="upper left")
plt.show()
I could not retrieve data using pandas, so I downloaded them and read from the file.
Here you can see the estimated slope and intercept:
To test the estimated data I restored the EWC value from the EWA using the estimated parameters:
About the observation covariance value
By varying the observation covariance value you tell the Filter how accurate the input data is (normally you just describe your confidence in the observation using some datasheets or your knowledge about the system).
Here are estimated parameters and the restored EWC values using different observation covariance values:
You can see the filter follows the original function better with a bigger confidence in observation (smaller R). If the confidence is low (bigger R) the filter leaves the initial estimate (slope = 0, intercept = 0) very slowly and the restored function is far away from the original one.
About the frozen intercept
If you want to freeze the intercept for some reason, you need to change the whole model and all filter parameters.
In the normal case we had:
x = [slope; intercept] #estimation state
H = [EWA 1] #observation matrix
z = [EWC] #observation
Now we have:
x = [slope] #estimation state
H = [EWA] #observation matrix
z = [EWC-const_intercept] #observation
Results:
Here is the code:
from pykalman import KalmanFilter
import numpy as np
import matplotlib.pyplot as plt
# only slope has to be estimated (it will be manipulated by the constant intercept) - mathematically incorrect!
const_intercept = 10
# reading data (quick and dirty)
Datum=[]
EWA=[]
EWC=[]
for line in open('data/dataset.csv'):
f1, f2, f3 = line.split(';')
Datum.append(f1)
EWA.append(float(f2))
EWC.append(float(f3))
n = len(Datum)
# Filter Configuration
# transition_matrix
F = 1 # identity matrix because x_(k+1) = x_(k) + noise
# observation_matrix
# H_k = [EWA_k]
H = np.matrix(EWA).T[:, np.newaxis]
# transition_covariance
Q = 1e-4
# observation_covariance
R = 1 # max error = 3
# initial_state_mean
X0 = 0
# initial_state_covariance
P0 = 1
# Kalman-Filter initialization
kf = KalmanFilter(n_dim_obs=1, n_dim_state=1,
transition_matrices = F,
observation_matrices = H,
transition_covariance = Q,
observation_covariance = R,
initial_state_mean = X0,
initial_state_covariance = P0)
# Creating the observation based on EWC and the constant intercept
z = EWC[:] # copy the list (not just assign the reference!)
z[:] = [x - const_intercept for x in z]
# Filtering
state_means, state_covs = kf.filter(z) # the estimation for the EWC data minus constant intercept
# Restore EWC based on EWA and estimated parameters
EWC_restored = np.multiply(EWA, state_means[:, 0]) + const_intercept
# Plots
plt.figure(1)
ax1 = plt.subplot(211)
plt.plot(state_means[:, 0], label="Slope")
plt.grid()
plt.legend(loc="upper left")
ax2 = plt.subplot(212)
plt.plot(const_intercept*np.ones((n, 1)), label="Intercept")
plt.grid()
plt.legend(loc="upper left")
# check the result
plt.figure(2)
plt.plot(EWC, label="EWC original")
plt.plot(EWC_restored, label="EWC restored")
plt.grid()
plt.legend(loc="upper left")
plt.show()

Time Varying Transfer Function

I have a discrete transfer function whose numerator and denominator are coming from an input port. At every sample time these numerator and denominator vectors change.
E.g.
# t == 0
den == [1 0 -1]
# t == 1
den == [1 0 0 -1]
What do I have to do in order for the transfer function to work with this?
I have tried:
Having a variable length signal.
SIMULINK did not like this and refused to run, complaining that the discrete transfer function block could not handle variable sized input.
Padding the vector with many leading zeros
This led to periodic spikes in the signal. Additionally, Simulink does not let you do this if you enter the values by hand, rather than as an input, so I don't think this is the way to do it either.
Any help is much appreciated.
This is solved trivially with a single Simulink S-Function file sfuntvf.m, like this.
In there, a custom fixed vector state with y and us history is saved, and the time varying part is simply applied over the time history (depicted in cyan and yellow); here a FIR Filter with a random order (depicted in magenta).
Other build-in implementations shall consider the sucessive initial conditions between calls, and require the verification of the initial condition structure. The shown implementation is the easiest to deploy and understand.
The A and B vectors can be modified accordingly.
function [sys,x0,str,ts] = sfuntvf(t,x,u,flag)
N=20;
n=randi(N-2)+1;
A=[1;zeros(N-1,1)];
B=[1/n*ones(n,1);zeros(N-n,1)];
switch flag,
case 0, [sys,x0,str,ts] = mdlInitializeSizes(N);
case 2, sys = mdlUpdate(t,x,u,N,A,B);
case 3, sys = mdlOutputs(t,x,u,N,n);
case 9, sys = [];
otherwise DAStudio.error('Simulink:blocks:unhandledFlag',num2str(flag));
end
function [sys,x0,str,ts] = mdlInitializeSizes(N)
sizes = simsizes;
sizes.NumContStates = 0;
sizes.NumDiscStates = 2*N;
sizes.NumOutputs = 2;
sizes.NumInputs = 1;
sizes.DirFeedthrough = 0;
sizes.NumSampleTimes = 1;
sys = simsizes(sizes);
x0 = zeros(2*N,1);
str = [];
ts = [1 0];
function sys = mdlUpdate(t,x,u,N,A,B)
un=x(1:N,1);
yn=x(N+1:2*N,1);
y=-A(2:end)'*yn(2:end)+B'*un;
sys = [u;un(1:end-1);y;yn(1:end-1)];
function sys = mdlOutputs(t,x,u,N,n)
sys = [x(N+1);n];

BVP4c solve for unknown boundary

I am trying to use bvp4c to solve a system of 4 odes. The issue is that one of the boundaries is unknown.
Can bvp4c handle this? In my code L is the unknown I am solving for.
I get an error message printed below.
function mat4bvp
L = 8;
solinit = bvpinit(linspace(0,L,100),#mat4init);
sol = bvp4c(#mat4ode,#mat4bc,solinit);
sint = linspace(0,L);
Sxint = deval(sol,sint);
end
% ------------------------------------------------------------
function dtdpdxdy = mat4ode(s,y,L)
Lambda = 0.3536;
dtdpdxdy = [y(2)
-sin(y(1)) + Lambda*(L-s)*cos(y(1))
cos(y(1))
sin(y(1))];
end
% ------------------------------------------------------------
function res = mat4bc(ya,yb,L)
res = [ ya(1)
ya(2)
ya(3)
ya(4)
yb(1)];
end
% ------------------------------------------------------------
function yinit = mat4init(s)
yinit = [ cos(s)
0
0
0
];
end
Unfortunately I get the following error message ;
>> mat4bvp
Not enough input arguments.
Error in mat4bvp>mat4ode (line 13)
-sin(y(1)) + Lambda*(L-s)*cos(y(1))
Error in bvparguments (line 105)
testODE = ode(x1,y1,odeExtras{:});
Error in bvp4c (line 130)
bvparguments(solver_name,ode,bc,solinit,options,varargin);
Error in mat4bvp (line 4)
sol = bvp4c(#mat4ode,#mat4bc,solinit);
One trick to transform a variable end point into a fixed one is to change the time scale. If x'(t)=f(t,x(t)) is the differential equation, set t=L*s, s from 0 to 1, and compute the associated differential equation for y(s)=x(L*s)
y'(s)=L*x'(L*s)=L*f(L*s,y(s))
The next trick to employ is to transform the global variable into a part of the differential equation by computing it as constant function. So the new system is
[ y'(s), L'(s) ] = [ L(s)*f(L(s)*s,y(s)), 0 ]
and the value of L occurs as additional free left or right boundary value, increasing the number of variables = dimension of the state vector to the number of boundary conditions.
I do not have Matlab readily available, in Python with the tools in scipy this can be implemented as
from math import sin, cos
import numpy as np
from scipy.integrate import solve_bvp, odeint
import matplotlib.pyplot as plt
# The original function with the interval length as parameter
def fun0(t, y, L):
Lambda = 0.3536;
#print t,y,L
return np.array([ y[1], -np.sin(y[0]) + Lambda*(L-t)*np.cos(y[0]), np.cos(y[0]), np.sin(y[0]) ]);
# Wrapper function to apply both tricks to transform variable interval length to a fixed interval.
def fun1(s,y):
L = y[-1];
dydt = np.zeros_like(y);
dydt[:-1] = L*fun0(L*s, y[:-1], L);
return dydt;
# Implement evaluation of the boundary condition residuals:
def bc(ya, yb):
return [ ya[0],ya[1], ya[2], ya[3], yb[0] ];
# Define the initial mesh with 5 nodes:
x = np.linspace(0, 1, 3)
# This problem has multiple solutions. Try two initial guesses.
L_a=8
L_b=9
y_a = odeint(lambda y,t: fun1(t,y), [0,0,0,0,L_a], x)
y_b = odeint(lambda y,t: fun1(t,y), [0,0,0,0,L_b], x)
# Now we are ready to run the solver.
res_a = solve_bvp(fun1, bc, x, y_a.T)
res_b = solve_bvp(fun1, bc, x, y_b.T)
L_a = res_a.sol(0)[-1]
L_b = res_b.sol(0)[-1]
print "L_a=%.8f, L_b=%.8f" % ( L_a,L_b )
# Plot the two found solutions. The solution are in a spline form, use this to produce a smooth plot.
x_plot = np.linspace(0, 1, 100)
y_plot_a = res_a.sol(x_plot)[0]
y_plot_b = res_b.sol(x_plot)[0]
plt.plot(L_a*x_plot, y_plot_a, label='L=%.8f'%L_a)
plt.plot(L_b*x_plot, y_plot_b, label='L=%.8f'%L_b)
plt.legend()
plt.xlabel("t")
plt.ylabel("y")
plt.grid(); plt.show()
which produces
Trying different initial values for L finds other solutions on quite different scales, among them
L=0.03195111
L=0.05256775
L=0.05846539
L=0.06888907
L=0.08231966
L=4.50411522
L=6.84868060
L=20.01725616
L=22.53189063