The Torsional Oscillator --  Comparing Linear and Air Drag fits

Clear["Global`*"]; DateString[]
SetDirectory[NotebookDirectory[]];

model[V0_, γ_, ω_, ϕ_, Voff_, t_] := V0 Exp[- γ t/2] Sin[ω t + ϕ] + Voff

(* Handy function to get CSV files output by LoggerPro, stripping off the title headers *)
getfile[filename_] := Select[Import[filename, "CSV"],
Length[#] == 2 && VectorQ[#, NumberQ] &]

damped = getfile["TO-20250220-damped-1.csv"];
airdrag   = getfile["TO-20250221-airdrag-1.csv"];

Damped Oscillations

dfit = NonlinearModelFit[damped, model[V0, γ, ω, ϕ, Voff, t], {V0, γ, ω, ϕ, Voff}, t]
dfit["ParameterConfidenceIntervalTable"] // Quiet
{ωv, δωv} = dfit["ParameterConfidenceIntervalTableEntries"][[3, {1, 2}]] // Quiet
Show[{ListPlot[damped, PlotStyle -> Red], Plot[dfit[t], {t, 0, 30}, PlotRange -> All]}, ImageSize -> Scaled[0.8],  LabelStyle -> Larger, Frame -> True, FrameLabel -> {"t (s)" , "V(t)"}, PlotRange -> All, PlotLabel -> "Damped"]

dresiduals = Table[{damped[[i, 1]], damped[[i, 2]] -dfit[damped[[i, 1]]]}, {i, 1, Length[damped]}];

ListPlot[{damped, dresiduals}, PlotRange -> {{5, 10}, {-0.1, 0.1}}, ImageSize -> Scaled[0.8], LabelStyle -> Larger, Frame -> True, FrameLabel -> {"t (s)" , "V(t)"}, PlotLabel -> "Damped Residuals"]

dampedRMSE = Sqrt[dfit["EstimatedVariance"]]

Air drag

afit = NonlinearModelFit[airdrag, model[V0, γ, ω, ϕ, Voff, t], {V0, γ, ω, ϕ, Voff}, t]
afit["ParameterConfidenceIntervalTable"] // Quiet
{ωa, δωa} = afit["ParameterConfidenceIntervalTableEntries"][[3, {1, 2}]] // Quiet
Show[{ListPlot[airdrag, PlotStyle -> Red, PlotLegends-> {"Data"}], Plot[afit[t], {t, 0, 30}, PlotRange -> All, PlotLegends -> {"Fit"}]}, ImageSize -> Scaled[0.8],  LabelStyle -> Larger, Frame -> True, FrameLabel -> {"t (s)" , "V(t)"}, PlotRange -> All, PlotLabel -> "Air Drag"]

The fit decays too rapidly, especially at late times.  This is because for small velocity, the v^2 damping of air drag is small compared to the v^1 linear damping of the fit.

Comparing models

Sqrt[dfit["EstimatedVariance"]]

Sqrt[afit["EstimatedVariance"]]

The larger typical error for the air drag is clear -- another sign that it’s just the wrong model.

Compare Residuals

Damped Residuals

dresiduals = Table[{damped[[i, 1]], damped[[i, 2]] -dfit[damped[[i, 1]]]}, {i, 1, Length[damped]}];

ListPlot[{damped, dresiduals}, PlotRange -> All, PlotLegends -> {"Data", "Residuals"},ImageSize -> Scaled[0.8], LabelStyle -> Larger, Frame -> True, FrameLabel -> {"t (s)" , "V(t)"}, PlotLabel -> "Damped Residuals"]

ListPlot[{damped, dresiduals}, PlotRange -> {{5, 10}, {-0.1, 0.1}}, PlotLegends -> {"Data", "Residuals"},ImageSize -> Scaled[0.8], LabelStyle -> Larger, Frame -> True, FrameLabel -> {"t (s)" , "V(t)"}, PlotLabel -> "Zoomed Damped Residuals"]

The residuals are all quite small.  There is a slight trend for larger residuals when the velocity is high (as the oscillator moves through the origin).

Air Drag Residuals

aresiduals = Table[{airdrag[[i, 1]], airdrag[[i, 2]] -afit[airdrag[[i, 1]]]}, {i, 1, Length[airdrag]}];

ListPlot[{airdrag, aresiduals}, PlotRange -> All, PlotLegends -> {"Data", "Residuals"},ImageSize -> Scaled[0.8], LabelStyle -> Larger, Frame -> True, FrameLabel -> {"t (s)" , "V(t)"}, PlotLabel -> "Air Drag Residuals"]

ListPlot[{airdrag, aresiduals}, PlotRange -> {All, {-0.1, 0.1}}, PlotLegends -> {"Data", "Residuals"},ImageSize -> Scaled[0.8], LabelStyle -> Larger, Frame -> True, FrameLabel -> {"t (s)" , "V(t)"}, PlotLabel -> "Zoomed Air Drag Residuals"]

A systematic pattern is clear here, and it is generally worse at the peaks, meaning the linear fit systematically gets the peak heights wrong.