Closed juanit0o closed 1 year ago
Great question. A few points:
I think there is a bigger issue with trying Autograd -- I think you need to do SINDy symbolically so that you can compute all the derivatives symbolically. So you would need to build a fair bit of extra functionality, including symbolic libraries rather than numeric ones. Kadierdan Kaheman has done some work using symbolic SINDy. This would be really cool but might be a lot of work.
First of all, thank you so much for your time answering, it really is something that has been bugging me for quite a bit of time. Considering that SINDy with the weak form should be able to find second order PDEs with minimal problems, my use case doesn't really need autograd :). I will try to be as concise as possible on explaining my problem and what I have done to try to solve it to see if there is anything I'm doing wrong which might affect negatively the results.
I'm working with a dataset with the positions and velocities of sattelites that consists of 6 columns (3 for the position in X,Y,Z and 3 for the velocities in X,Y,Z) and an extra column with the corresponding times where it has been measured. This dataset has 537 measurements (1 measurement per day).
odeint()
to generate an analitically correct dataset given initial data and the functions aforementioned (with the same dimensions as the "real' dataset. From here on, I used this new dataset generated with odeint
which should be easy for SINDy to identify the equations.Here is what I tried to do. Due to knowing the terms that will have to be on the equations, I defined the function library with the appropriate candidate functions. I've read all of the optimization-related examples on this github as well as the pysindy-doc website and tried to apply all that would make sense here with this kind of data as well as tinkering with their respective hyperparameters but unfortunately it didn't result in anything. Given the code bellow, am I possibly making something wrong that might affect the results? Considering I'm already using basically "perfect" data generated by odeint, I don't really understand how is SINDy not able to identify those equations. I'm sorry for the wall of text and I appreciate any help, thank you once again for your attention.
Generating "artificial" data with odeint
def functionToIntegrate(valuesList, time):
posX = valuesList[0]
posY = valuesList[1]
posZ = valuesList[2]
velX = valuesList[3]
velY = valuesList[4]
velZ = valuesList[5]
standardGravitationalParameter = 3.986004418 * 10**(14)
return [velX,
velY,
velZ,
-standardGravitationalParameter * posX/(posX**2 + posY**2 + posZ**2)**(3/2),
-standardGravitationalParameter * posY/(posX**2 + posY**2 + posZ**2)**(3/2),
-standardGravitationalParameter * posZ/(posX**2 + posY**2 + posZ**2)**(3/2)]
#first line of the dataset (positions and velocities)
intialState = dataSindy.iloc[0].to_list()
#Time generated from the first timestamp onwards (during 3 hours)
time = t_train.copy().flatten().tolist()[0] + np.linspace(0, 10800, 573)
#rows = timesteps
#columns = 6 variables
sol = odeint(functionToIntegrate, y0=intialState, t = time, tfirst=False)
Feeding that artificial data to weak SINDy
#positionX, positionY, positionZ (573 rows)
dataSindy = sol.copy()[:,:3]
#time for each measurement
t_train = time.copy()
############## Functions ################
library_functions = [lambda a, b, c : a/(a**2 + b**2 + c**2)**(3/2),
lambda a, b, c : b/(a**2 + b**2 + c**2)**(3/2),
lambda a, b, c : c/(a**2 + b**2 + c**2)**(3/2)]
library_function_names = [lambda a, b, c: a + "/((" + a + "**2 + " + b + "**2 + " + c + "**2)**(3/2))",
lambda a, b, c: b + "/((" + a + "**2 + " + b + "**2 + " + c + "**2)**(3/2))",
lambda a, b, c: c + "/((" + a + "**2 + " + b + "**2 + " + c + "**2)**(3/2))"]
#####################################
pde_lib = ps.WeakPDELibrary(
library_functions=library_functions,
function_names=library_function_names,
spatiotemporal_grid=t_train,
derivative_order=2,
is_uniform=True,
K=1000
)
############# Optimizers ################
optimizerTest = ps.SSR()
model2nd = ps.SINDy(feature_names=["posX", "posY", "posZ"], optimizer= optimizerTest,
feature_library=pde_lib)
model2nd.fit(dataSindy, t=t_train, ensemble=True, quiet = True)
model2nd.print()
OUTPUT
(posX)' = 14754721393892263251450789888.000 posX/((posX**2 + posY**2 + posZ**2)**(3/2)) + -20511467901215643540140326912.000 posY/((posX**2 + posY**2 + posZ**2)**(3/2)) + -3554508483856439226157498368.000 posZ/((posX**2 + posY**2 + posZ**2)**(3/2))
(posY)' = 11399607816129243415742251008.000 posX/((posX**2 + posY**2 + posZ**2)**(3/2)) + -15847313111951032907328389120.000 posY/((posX**2 + posY**2 + posZ**2)**(3/2)) + -2746239770531404375132209152.000 posZ/((posX**2 + posY**2 + posZ**2)**(3/2))
(posZ)' = -4535334905196806730346921984.000 posX/((posX**2 + posY**2 + posZ**2)**(3/2)) + 6304854822750467758405713920.000 posY/((posX**2 + posY**2 + posZ**2)**(3/2)) + 1092591718067720505161416704.000 posZ/((posX**2 + posY**2 + posZ**2)**(3/2))
EXPECTED OUTPUT
(velX)' = -398600441595072.188 posX/((posX**2 + posY**2 + posZ**2)**(3/2))
(velY)' = -398600440967606.688 posY/((posX**2 + posY**2 + posZ**2)**(3/2))
(velZ)' = -398600441014742.500 posZ/((posX**2 + posY**2 + posZ**2)**(3/2))
Going to take some time to work through this, but first error I think is that "derivative_order=2" means that the library will have derivative terms (integrated into the weak formulation) up to second order (which I don't think you are interested in), NOT that you are trying to fit a second order differential equation! To use second-order derivatives on the left-hand-side of the SINDy fit, you need to pass them during "model2nd.fit(dataSindy, x_dot=dataSINDy_second_derivative, t=t_train, ensemble=True, quiet = True)." I recommend trying this first with the regular CustomLibrary before trying it with the WeakPDELibrary, if this is perfect data, since the WeakPDELibrary solution is more difficult and you don't seem to be using derivative terms in your equations.
This is because I believe (though I'm not sure) you will need to pass the weak-form version of the second derivative to model.fit, which must be computed and passed to model.fit as the 'x_dot' parameter. Unfortunately, we need @znicolaou for this because I'm not entirely sure how to compute higher order weak form terms in the new weak form notation we have been using.
Another thing to consider: the gravitational constant is ginormous, so large that it might be affecting numerical precision in various places. I recommend rescaling this toy example so that it is equal to 1 or something.
Hey @juanit0o, just to chime in quickly now, I agree with @akaptano that you ought to try the CustomLibrary first, if only just to learn about how to implement things before trying the WeakPDELibrary. It won't be very hard to do the second-order model in either case, but you may as well try simpler first.
That being said, there is both a general approach and a specialized approach for the WeakPDELibrary.
For the general approach, you need to augment your system with auxiliary variables, as one does with momentum say in Hamiltonian mechanics. So if your data is in a matrix $m\times n$ matrix $X$ (with $m$ time samples and $n$ coordinates) and you expect a model like $\ddot{X} = \Theta \cdot X$, then define a new $m \times 2n$ matrix $Y=(X,\dot{X})$, and look for a model $\dot{Y}=\Psi \cdot Y$. The first $n$ equations in the SINDy model should be trivial $\dot{Y} i = Y {2i}$, and the last $n$ will encode the second-order model.
For the specific solution, you can use implicit_terms=True
in the WeakPDELibrary, which will then include second-order temporal derivatives when you use derivative_order=2
. That solution is more technical and there may be pitfalls, so be careful in interpreting results if you try it.
We can give more detailed feedback if you have trouble implementing these approaches.
For what it's worth, _differentiate
is called here
Basically, it allows any object inheriting BaseDifferentiation
to be used as a function.
One reason WeakPDELibrary
sticks to finite difference for spatial derivatives rather than other differentiators is that most other methods smooth the coordinates implicitly. Smoothing in multiple directions breaks the assumptions of the derivative method.
In general, consider adding a new differentiation class if you have an example of a data trajectory - leaving out the SINDy part - that the existing derivative methods fail on.
Thank you all so much. I was really mistaken on the meaning of the derivative_order
argument in the library used. I also understand the difficulty of calculating the second order derivative with its weak form to pass it as an argument for the fit
function. I will try to see if I can discover the equations I want with implicit_terms
as True. Concerning that _differentiate
call, I now understand that it didn't work with the WeakPDE because the finite difference _differentiate
is always called but using the CustomLibrary I should be able to invoke a new differentiation class I define. Concerning the high order magnitude coefficient, I will also try to reduce its magnitude for testing purposes.
I will try to implement it but once again thank you for your help and guidance.
Trying to calculate the second order derivative I'm facing an error that I'm not sure where it might arise from. Making use of the x_dot
argument (https://pysindy.readthedocs.io/en/latest/tips.html#numerical-differentiation) in the fit()
function, I am getting a shape error that I'm not sure where it might come from as the shapes seem correct. How is it possible to overcome this problem? Thank you once again.
pde_lib2ndDeriv = ps.WeakPDELibrary(
library_functions=library_functions2ndDeriv,
function_names=library_function_names2ndDeriv,
spatiotemporal_grid=time,
derivative_order=2,
implicit_terms=True,
is_uniform=True,
K=1000
)
optimizerTest = ps.SSR()
model2nd = ps.SINDy(feature_names=["posX", "posY", "posZ"], optimizer= optimizerTest, feature_library=pde_lib2ndDeriv)
secondOrderDeriv = ps.FiniteDifference(order=1)._differentiate(firstOrderDeriv, t=time)
#fit raises error: "operands could not be broadcast together with shapes (1000,) (573,) "
#but np.shape(dataSindy) = (573, 3), np.shape(secondOrderDeriv) = (573, 3), np.shape(time) = (573, )
# I dont understand where 1000 comes from
model2nd.fit(dataSindy, t=time, ensemble=True, quiet = True, x_dot = secondOrderDeriv)
model2nd.print()
I also noticed that there is a function for simulating with the equations given by SINDy which can be used directly with odeint but applying it to the first derivative case where everything is correct I'm getting an index out bounds error.
optimizerKms = ps.FROLS(kappa=1e-29)
model = ps.SINDy(feature_names=["posX", "posY", "posZ", "velX", "velY", "velZ"], optimizer= optimizerKms, feature_library=pde_lib1stDeriv)
model.fit(sol1stDerivativeKm, t=time, ensemble=True, quiet=True)
model.print()
#OUTPUT
#(posX)' = 1.000 *velX
#(posY)' = 1.000 *velY
#(posZ)' = 1.000 *velZ
#(velX)' = -398600.442 *posX/((posX**2 + posY**2 + posZ**2)**(3/2))
#(velY)' = -398600.441 *posY/((posX**2 + posY**2 + posZ**2)**(3/2))
#(velZ)' = -398600.441 *posZ/((posX**2 + posY**2 + posZ**2)**(3/2))
#ERROR
#--> 842 self.x_k = [x[np.ix_(*self.inds_k[k])] for k in range(self.K)]
# 843
# 844 # library function terms
#IndexError: index 280 is out of bounds for axis 0 with size 1
model.simulate(x0=sol1stDerivativeKm[0], t=time, integrator='odeint')
#x0 = [-9.06200977e+02 -1.80616337e+03 6.66092629e+03 -6.09630503e+00 -4.05100447e+00 -1.92916516e+00]
#time = float array of times with length 573
Can you provide the full traceback?
Also as a hint: if you're worried about conciseness & posting long messages, there's a spoiler tag that may be helpful:
<details>
<summary>
Title
</summary>
Hidden long traceback (can be formatted with backticks, but need newlines beforehand)
</details>
Appears as:
Surely. I am not sure why the problem arises but if I remove the feature library (from using weak to not using that extra argument) the code works (although giving unpleasant equations) so it has something to do with the weak library. Both the .simulate and extra x_dot argument errors disappear when I don't use the feature library argument. Below is the traceback for when I try to use the x_dot argument. Thank you.
Hi @juanit0o, I think you need to refer to some of the publications on the weak form, such as Reinbold, Patrick AK, Daniel R. Gurevich, and Roman O. Grigoriev. "Using noisy or incomplete data to discover models of spatiotemporal dynamics." Physical Review E 101, no. 1 (2020): 010203. The reason you get shape errors is because in the weak formulation, the supplied x_dot needs to be calculated as the weak features, e.g. using the calc_trajectory
function from a WeakPDELibrary
object. The weak features are integrals of the input data over K
randomly placed spatiotemporal subdomains, so there are only K
rows in the weak features rather than the number of spatiotemporal points in the differential formulation. If you really delve into the details of the library, you can edit the convert_u_dot_integral
function to calculate second-order temporal derivative weak feature if you want, but you'll have to understand how the weak formulation works first.
Thanks for posting the complete traceback - it helped knowing which function was raising the error. I also made a mistake - the summary tags should be inside the details tag.
I'm doing some tests with SINDy with both first and second order derivatives and fortunately, applying SINDy with Weak Library to discover 1st order PDEs works perfectly. However, I am having a lot of difficulties (impossible until now) on finding the correct 2nd order derivatives for the data I'm using. Due to my understanding, I believe this might be related to the fact that SINDy only uses numeric based differentiation methods.
Firstly, I tried the various numerical differentiation methods (spectral, smoothed finite diff, spline..) that are available with SINDy but the results didn't show any particular improvements. My idea was to implement another differentiation method using autograd. However, when trying to do so, I'm confused because the
_differentiate
method is never called (for any of the methods) and I'm not sure how can I do so.I have already added to
__init__.py
the newAutogradDerivative
class (which extendsBaseDifferentiation
) and implemented the_differentiate
method as obliged but as it isn't being called anywhere, there isn't much use to this. I have also looked into the code ofweak_pde_library.py
where I noticed it was callingFiniteDifference
and tried to change it to my newly created class but nothing happened.I'm not sure if this is the best place to ask this but I've searched everywhere and didn't find anything that could help. I would be greatly appreciated with any help on how to implement this new differentiation_method. Thank you.