Search code examples
rsplinesmoothing

How can I fit a smooth hysteresis in R?


I have a measurment of which should fit an hysteresis. For visualisation purpose I would like to plot a line approximating the hysteresis to help explain this pattern.

I created an example in the following image using the code below. enter image description here

I would like to have an output similar to the green curve - however I don't have this data directly available, and I don't care whether it is pointy.

However most smoothing functions such as smooth.spline which I plotted in blue - allow no loops. The closest I can find is from the bezier library - plotted in red. Not nicely visible here but it produces a loop, however it fits poorly (and gives some warnings and takes quite some time).

Can you suggest a method?

set.seed(12345)
up <- seq(0,1,length.out=100)^3
down <- sqrt(seq(1,0,length.out=100))
x <- c(seq(0,1,length.out=length(up)),
       seq(1,0, length.out=length(down)))

data <- data.frame(x=x, y=c(up,down),
                   measuredx=x + rnorm(length(x))*0.01,
                   measuredy=c(up,down) + rnorm(length(up)+length(down))*0.03)


with(data,plot(measuredx,measuredy, type = "p"))
with(data,lines(x,y, col='green'))

sp <- with(data,smooth.spline(measuredx, measuredy))
with(sp, lines(x,y, col="blue"))


library(bezier)
bf <- bezierCurveFit(as.matrix(data[,c(1,3)]))
lines(bezier(t=seq(0, 1, length=500), p=bf$p), col="red", cex=0.25)

UPDATE

As it turns out my actual problem is slightly different I ask another question to reflect my actual issue in the question: How to fit a smooth hysteresis in a poorly distributed data set?


Solution

  • set.seed(12345)
    up <- seq(0,1,length.out=100)^3
    down <- sqrt(seq(1,0,length.out=100))
    x <- c(seq(0,1,length.out=length(up)),
           seq(1,0, length.out=length(down)))
    
    data <- data.frame(x=x, y=c(up,down),
                       measuredx=x + rnorm(length(x))*0.01,
                       measuredy=c(up,down) + rnorm(length(up)+length(down))*0.03)
    

    Instead of smoothing data$measuredy directly over data$measuredx, do two separate smoothing, by smoothing each against a time stamp variable. Then combine the fitted values from two smoothing. This is a general way for smoothing a closed curve or a loop. (See also Q & A: Smoothing Continuous 2D Points)

    t <- seq_len(nrow(data) + 1)
    xs <- smooth.spline(t, c(data$measuredx, data$measuredx[1]))$y
    ys <- smooth.spline(t, c(data$measuredy, data$measuredy[1]))$y
    with(data, plot(measuredx, measuredy))
    lines(xs, ys)
    

    c(data$measuredx, data$measuredx[1]) for example is just to ensure that the last value in the vector agrees with the first, so that it completes a cycle.


    The curve is not really closed at the bottom left corner, because smooth.spline is doing smoothing not interpolation, so even if we have ensure that data vector completes a cycle, the fitted one may not be a closed one. A practical workaround is to use weighted regression, imposing heavy weight on this spot to make it closed.

    t <- seq_len(nrow(data) + 1)
    
    w <- rep(1, length(t))  ## initially identical weight everywhere
    w[c(1, length(w))] <- 100000  ## give heavy weight
    
    xs <- smooth.spline(t, c(data$measuredx, data$measuredx[1]), w)$y
    ys <- smooth.spline(t, c(data$measuredy, data$measuredy[1]), w)$y
    with(data, plot(measuredx, measuredy), col = 8)
    lines(xs, ys, lwd = 2)