We are given a scalar time series $y_t$, $t = 1,\dots,n$, assumed to consist of an underlying slowly varying trend $x_t$ and a more rapidly varying random component $z_t$. Our goal is to estimate the trend component $x_t$ or, equivalently, estimate the random component $z_t = y_t - x_t$. This can be considered as an optimization problem with two competing objectives: We want $x_t$ to be smooth, and we want $z_t$ (our estimate of the random component, sometimes called the residual) to be small. In some contexts, estimating $x_t$ is called smoothing or filtering.
For example, Samsung Electronics stock prices over the past 10 years are loaded below.
!pip install yfinance
Requirement already satisfied: yfinance in /usr/local/lib/python3.11/dist-packages (0.2.55) Requirement already satisfied: pandas>=1.3.0 in /usr/local/lib/python3.11/dist-packages (from yfinance) (2.2.2) Requirement already satisfied: numpy>=1.16.5 in /usr/local/lib/python3.11/dist-packages (from yfinance) (2.0.2) Requirement already satisfied: requests>=2.31 in /usr/local/lib/python3.11/dist-packages (from yfinance) (2.32.3) Requirement already satisfied: multitasking>=0.0.7 in /usr/local/lib/python3.11/dist-packages (from yfinance) (0.0.11) Requirement already satisfied: platformdirs>=2.0.0 in /usr/local/lib/python3.11/dist-packages (from yfinance) (4.3.7) Requirement already satisfied: pytz>=2022.5 in /usr/local/lib/python3.11/dist-packages (from yfinance) (2025.1) Requirement already satisfied: frozendict>=2.3.4 in /usr/local/lib/python3.11/dist-packages (from yfinance) (2.4.6) Requirement already satisfied: peewee>=3.16.2 in /usr/local/lib/python3.11/dist-packages (from yfinance) (3.17.9) Requirement already satisfied: beautifulsoup4>=4.11.1 in /usr/local/lib/python3.11/dist-packages (from yfinance) (4.13.3) Requirement already satisfied: soupsieve>1.2 in /usr/local/lib/python3.11/dist-packages (from beautifulsoup4>=4.11.1->yfinance) (2.6) Requirement already satisfied: typing-extensions>=4.0.0 in /usr/local/lib/python3.11/dist-packages (from beautifulsoup4>=4.11.1->yfinance) (4.12.2) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.11/dist-packages (from pandas>=1.3.0->yfinance) (2.8.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.11/dist-packages (from pandas>=1.3.0->yfinance) (2025.1) Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.11/dist-packages (from requests>=2.31->yfinance) (3.4.1) Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.11/dist-packages (from requests>=2.31->yfinance) (3.10) Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.11/dist-packages (from requests>=2.31->yfinance) (2.3.0) Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.11/dist-packages (from requests>=2.31->yfinance) (2025.1.31) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.11/dist-packages (from python-dateutil>=2.8.2->pandas>=1.3.0->yfinance) (1.17.0)
import yfinance as yf
import datetime as dt
end = dt.datetime.today()
start = dt.datetime(end.year-10, end.month, end.day)
df = yf.download('005930.KS', start, end)
df
[*********************100%***********************] 1 of 1 completed
Price | Close | High | Low | Open | Volume |
---|---|---|---|---|---|
Ticker | 005930.KS | 005930.KS | 005930.KS | 005930.KS | 005930.KS |
Date | |||||
2015-03-27 | 22333.130859 | 22757.476062 | 22238.831925 | 22238.831925 | 16065800 |
2015-03-30 | 22443.142578 | 22537.441497 | 22191.678796 | 22395.993119 | 8955500 |
2015-03-31 | 22647.451172 | 22820.332479 | 22474.569865 | 22773.183031 | 9842650 |
2015-04-01 | 22364.560547 | 22584.591360 | 22317.411087 | 22584.591360 | 7266050 |
2015-04-02 | 22537.439453 | 22631.738363 | 22364.558118 | 22537.439453 | 6903400 |
... | ... | ... | ... | ... | ... |
2025-03-20 | 60200.000000 | 60300.000000 | 58500.000000 | 59200.000000 | 34989004 |
2025-03-21 | 61700.000000 | 61700.000000 | 60400.000000 | 60900.000000 | 40155612 |
2025-03-24 | 60500.000000 | 61600.000000 | 60500.000000 | 61200.000000 | 14088094 |
2025-03-25 | 59800.000000 | 61100.000000 | 59500.000000 | 60900.000000 | 17259455 |
2025-03-26 | 61400.000000 | 61400.000000 | 59700.000000 | 59800.000000 | 16431645 |
2453 rows × 5 columns
import numpy as np
import matplotlib.pyplot as plt
plt.figure(figsize=(14,9), dpi=100)
plt.plot(df['Close'], label='Samsung Electronics')
plt.xlabel('date')
plt.ylabel('stock price')
plt.grid()
plt.legend()
plt.show()
We will take the Close price from which we will try to read the trend, and will normalize the data. The plot follows below. Can you read the trend from it?
import numpy as np
import matplotlib.pyplot as plt
y = df['Close'].values
y -= np.mean(y)
y /= np.std(y)
y = y.flatten()
n = len(y)
plt.figure(figsize=(14,9), dpi=100)
plt.plot(y, label='Samsung Electronics')
plt.xlabel('days')
plt.ylabel('normalized price')
plt.grid()
plt.legend()
plt.show()
Reading the trend out from given time-series data can be formulated by following. The underlying trend $x_t$ should explain the observed data $y_t$, at the same time, we expect the trend (inclination) wouldn't change frequently, so we would like to find $x_1,\dots,x_n$ such that
for which we formulate the optimization problem by
$$ \begin{aligned} \underset{x_1,\dots,x_n}{\minimize} \quad & \sum_{t=1}^{n}\left(y_t-x_t\right)^2 + \lambda\sum_{t=1}^{n-2}\left(x_{t+2}-2x_{t+1}+x_t\right)^2 \end{aligned} $$with some positive $\lambda$. This is equivalent to
$$ \begin{aligned} \underset{x}{\minimize} \quad & \|x-y\|_2^2 + \lambda\|Dx\|_2^2 \end{aligned} $$where $x = (x_1, \dots, x_n)$, $y = (y_1, \dots, y_n )$, with
$$ \begin{aligned} D &= \bmat{ 1 & -2 & 1 & & \\ & \ddots & \ddots & \ddots & \\ & & 1 & -2 & 1 \\ } \end{aligned} $$This is again equivalent to
$$ \begin{aligned} \underset{x}{\minimize} \quad & \left\|\bmat{I \\ \sqrt{\lambda}D}x-\bmat{y\\0}\right\|_2^2 \end{aligned} $$whose optimal solution $x^*$ is given by
$$ x^*=\bmat{I \\ \sqrt{\lambda}D}^\dagger \bmat{y\\0} $$or $$ x^*=\left(I+\lambda D^TD\right)^{-1}y $$
lam = 1e5
A = np.eye(n)
D = np.zeros((n-2,n))
for i in range(D.shape[0]):
D[i,i:i+3] = [1, -2, 1]
A_tilde = np.vstack((A,np.sqrt(lam)*D))
y_tilde = np.hstack((y,np.zeros(n-2)))
xhat = np.linalg.lstsq(A_tilde,y_tilde,rcond=None)[0]
plt.figure(figsize=(14,9), dpi=100)
plt.plot(y, alpha=0.4, label='Samsung Electronics')
plt.plot(xhat,linewidth=3, label=r'$\ell_2$ trend filter')
plt.xlabel('days')
plt.ylabel('normalized price')
plt.grid()
plt.legend()
plt.show()
The code below is equal to the above, but the below computes the optimal solution way faster than the above, since it facilitates the sparsity of $D$.
import scipy.sparse as ssp
import scipy.sparse.linalg as sla
lam = 1e5
As = ssp.eye(n)
Ds = ssp.spdiags([np.ones(n), -2*np.ones(n), np.ones(n)], [0, 1, 2], n-2, n)
As_tilde = ssp.vstack((As, np.sqrt(lam)*Ds))
y_tilde = np.hstack((y, np.zeros(n-2)))
xhats = sla.lsqr(As_tilde,y_tilde)[0]
plt.figure(figsize=(14,9), dpi=100)
plt.plot(y, alpha=0.4, label='Samsung Electronics')
plt.plot(xhats,linewidth=3, label=r'$\ell_2$ trend filter (sparse)')
plt.xlabel('days')
plt.ylabel('normalized price')
plt.grid()
plt.legend()
plt.show()
Looks like the below finds something that better explains the underlying trend with intuitive line segments, though the solution is not obtained from the least squares sense. In fact the below defines the problem just by replacing the $\ell_2$ norm with the $\ell_1$ norm as follows:
$$ \begin{aligned} \underset{x_1,\dots,x_n}{\minimize} \quad & \sum_{t=1}^{n}| y_t-x_t | + \lambda\sum_{t=1}^{n-2} | x_{t+2}-2x_{t+1}+x_t| \end{aligned} $$which is equivalent to solving:
$$ \begin{aligned} \underset{x}{\minimize} \quad & \|x-y\|_1 + \lambda\|Dx\|_1 \end{aligned} $$We haven't discussed how we could solve this, but we will soon come back to this at some point in this class.
import cvxpy as cp
x = cp.Variable(n)
obj = cp.Minimize( cp.norm(y-As@x,1) + 200*cp.norm(Ds@x,1) )
prob = cp.Problem(obj)
prob.solve(verbose=True)
plt.figure(figsize=(14,9), dpi=100)
plt.plot(y, alpha=0.4, label='Samsung Electronics')
plt.plot(xhats,linewidth=4, alpha=0.6, label=r'$\ell_2$ trend filter')
plt.plot(x.value,linewidth=3, label=r'$\ell_1$ trend filter')
plt.xlabel('days')
plt.ylabel('normalized price')
plt.grid()
plt.legend()
plt.show()
=============================================================================== CVXPY v1.6.4 =============================================================================== (CVXPY) Mar 27 04:17:13 AM: Your problem has 2453 variables, 0 constraints, and 0 parameters. (CVXPY) Mar 27 04:17:13 AM: It is compliant with the following grammars: DCP, DQCP (CVXPY) Mar 27 04:17:13 AM: (If you need to solve this problem multiple times, but with different data, consider using parameters.) (CVXPY) Mar 27 04:17:13 AM: CVXPY will first compile your problem; then, it will invoke a numerical solver to obtain a solution. (CVXPY) Mar 27 04:17:13 AM: Your problem is compiled with the CPP canonicalization backend. ------------------------------------------------------------------------------- Compilation ------------------------------------------------------------------------------- (CVXPY) Mar 27 04:17:13 AM: Compiling problem (target solver=CLARABEL). (CVXPY) Mar 27 04:17:13 AM: Reduction chain: Dcp2Cone -> CvxAttr2Constr -> ConeMatrixStuffing -> CLARABEL (CVXPY) Mar 27 04:17:13 AM: Applying reduction Dcp2Cone (CVXPY) Mar 27 04:17:13 AM: Applying reduction CvxAttr2Constr (CVXPY) Mar 27 04:17:13 AM: Applying reduction ConeMatrixStuffing (CVXPY) Mar 27 04:17:13 AM: Applying reduction CLARABEL (CVXPY) Mar 27 04:17:13 AM: Finished problem compilation (took 6.916e-02 seconds). ------------------------------------------------------------------------------- Numerical solver ------------------------------------------------------------------------------- (CVXPY) Mar 27 04:17:13 AM: Invoking solver CLARABEL to obtain a solution. ------------------------------------------------------------- Clarabel.rs v0.10.0 - Clever Acronym (c) Paul Goulart University of Oxford, 2022 ------------------------------------------------------------- problem: variables = 7357 constraints = 9808 nnz(P) = 0 nnz(A) = 29420 cones (total) = 1 : Nonnegative = 1, numel = 9808 settings: linear algebra: direct / qdldl, precision: 64 bit max iter = 200, time limit = Inf, max step = 0.990 tol_feas = 1.0e-8, tol_gap_abs = 1.0e-8, tol_gap_rel = 1.0e-8, static reg : on, ϵ1 = 1.0e-8, ϵ2 = 4.9e-32 dynamic reg: on, ϵ = 1.0e-13, δ = 2.0e-7 iter refine: on, reltol = 1.0e-13, abstol = 1.0e-12, max iter = 10, stop ratio = 5.0 equilibrate: on, min_scale = 1.0e-4, max_scale = 1.0e4 max iter = 10 iter pcost dcost gap pres dres k/t μ step --------------------------------------------------------------------------------------------- 0 -8.8546e-13 +1.8208e-13 1.07e-12 7.29e-01 1.05e-01 1.00e+00 8.87e+01 ------ 1 +2.3357e+01 +2.3370e+01 5.82e-04 2.90e-02 3.11e-03 4.19e-02 2.64e+00 9.70e-01 2 +4.2235e+01 +4.2238e+01 7.47e-05 8.53e-03 4.52e-04 7.25e-03 3.94e-01 8.51e-01 3 +8.4010e+01 +8.4042e+01 3.87e-04 4.90e-03 2.52e-04 3.48e-02 2.21e-01 5.33e-01 4 +1.1962e+02 +1.1964e+02 2.20e-04 2.78e-03 1.36e-04 2.75e-02 1.21e-01 5.09e-01 5 +1.5627e+02 +1.5629e+02 9.89e-05 1.30e-03 6.11e-05 1.60e-02 5.55e-02 5.86e-01 6 +1.8089e+02 +1.8090e+02 5.61e-05 6.36e-04 2.88e-05 1.04e-02 2.70e-02 5.98e-01 7 +1.9675e+02 +1.9676e+02 2.93e-05 3.15e-04 1.37e-05 5.91e-03 1.33e-02 5.69e-01 8 +2.0183e+02 +2.0184e+02 2.19e-05 2.15e-04 9.22e-06 4.51e-03 9.10e-03 4.07e-01 9 +2.0562e+02 +2.0562e+02 1.63e-05 1.41e-04 5.96e-06 3.41e-03 5.98e-03 4.85e-01 10 +2.0957e+02 +2.0957e+02 9.69e-06 7.26e-05 3.01e-06 2.06e-03 3.07e-03 6.77e-01 11 +2.1043e+02 +2.1043e+02 8.18e-06 5.51e-05 2.27e-06 1.75e-03 2.33e-03 5.89e-01 12 +2.1300e+02 +2.1300e+02 3.56e-06 2.32e-05 9.42e-07 7.68e-04 9.80e-04 7.35e-01 13 +2.1409e+02 +2.1410e+02 1.57e-06 1.01e-05 4.07e-07 3.40e-04 4.26e-04 6.83e-01 14 +2.1434e+02 +2.1434e+02 1.08e-06 6.84e-06 2.76e-07 2.35e-04 2.89e-04 5.24e-01 15 +2.1472e+02 +2.1472e+02 3.89e-07 2.45e-06 9.89e-08 8.46e-05 1.04e-04 6.93e-01 16 +2.1485e+02 +2.1485e+02 1.49e-07 9.27e-07 3.73e-08 3.23e-05 3.92e-05 7.53e-01 17 +2.1489e+02 +2.1489e+02 6.85e-08 4.23e-07 1.70e-08 1.49e-05 1.79e-05 7.41e-01 18 +2.1491e+02 +2.1491e+02 3.10e-08 1.89e-07 7.61e-09 6.74e-06 7.99e-06 9.90e-01 19 +2.1492e+02 +2.1492e+02 9.99e-09 6.08e-08 2.44e-09 2.17e-06 2.57e-06 7.44e-01 20 +2.1492e+02 +2.1492e+02 3.45e-09 2.17e-08 8.22e-10 7.51e-07 8.68e-07 9.37e-01 21 +2.1492e+02 +2.1492e+02 5.46e-10 6.62e-09 1.32e-10 1.19e-07 1.42e-07 8.65e-01 --------------------------------------------------------------------------------------------- Terminated with status = Solved solve time = 213.359222ms ------------------------------------------------------------------------------- Summary ------------------------------------------------------------------------------- (CVXPY) Mar 27 04:17:13 AM: Problem status: optimal (CVXPY) Mar 27 04:17:13 AM: Optimal value: 2.149e+02 (CVXPY) Mar 27 04:17:13 AM: Compilation took 6.916e-02 seconds (CVXPY) Mar 27 04:17:13 AM: Solver (including time spent in interface) took 2.270e-01 seconds
Now, what if your data is partially corrupted? Let us add some large non-Gaussian intermittent noise to your data.
z = 10*np.random.randn(n)
for i in range(n):
if np.random.rand() > 0.02:
z[i] = 0
y_cor = y + z
plt.figure(figsize=(14,9), dpi=100)
plt.plot(y_cor, alpha=0.4, label='Samsung Electronics (corrupted)')
plt.xlabel('days')
plt.ylabel('normalized price')
plt.ylim(-1.5,3.5)
plt.grid()
plt.legend()
plt.show()
We notice that the $\ell_2$ trend filter responds (very sensitively) to this type of noise as follows.
b_tilde_cor = np.hstack((y_cor, np.zeros(n-2)))
xhats_cor = sla.lsqr(As_tilde,b_tilde_cor)[0]
plt.figure(figsize=(14,9), dpi=100)
plt.plot(y_cor, alpha=0.4, label='Samsung Electronics (corrupted)')
plt.plot(xhats,linewidth=4, alpha=0.6, label=r'$\ell_2$ trend filter (on clean data)')
plt.plot(xhats_cor,linewidth=4, alpha=0.6, label=r'$\ell_2$ trend filter (on corrupted data)')
plt.xlabel('days')
plt.ylabel('normalized price')
plt.ylim(-1.5,3.5)
plt.grid()
plt.legend()
plt.show()
What about the $\ell_1$ trend filter?
x_cor = cp.Variable(n)
obj = cp.Minimize( cp.norm(y_cor-As@x_cor,1) + 100*cp.norm(Ds@x_cor,1) )
prob = cp.Problem(obj)
prob.solve(verbose=True)
plt.figure(figsize=(14,9), dpi=100)
plt.plot(y_cor, alpha=0.4, label='Samsung Electronics')
plt.plot(x.value, linewidth=4, label=r'$\ell_1$ trend filter (on clean data)')
plt.plot(x_cor.value, linewidth=3, label=r'$\ell_1$ trend filter (on corrupted data)')
plt.xlabel('days')
plt.ylabel('normalized price')
plt.ylim(-1.5,3.5)
plt.grid()
plt.legend()
plt.show()
=============================================================================== CVXPY v1.6.4 =============================================================================== (CVXPY) Mar 27 04:17:34 AM: Your problem has 2453 variables, 0 constraints, and 0 parameters. (CVXPY) Mar 27 04:17:34 AM: It is compliant with the following grammars: DCP, DQCP (CVXPY) Mar 27 04:17:34 AM: (If you need to solve this problem multiple times, but with different data, consider using parameters.) (CVXPY) Mar 27 04:17:34 AM: CVXPY will first compile your problem; then, it will invoke a numerical solver to obtain a solution. (CVXPY) Mar 27 04:17:34 AM: Your problem is compiled with the CPP canonicalization backend. ------------------------------------------------------------------------------- Compilation ------------------------------------------------------------------------------- (CVXPY) Mar 27 04:17:34 AM: Compiling problem (target solver=CLARABEL). (CVXPY) Mar 27 04:17:34 AM: Reduction chain: Dcp2Cone -> CvxAttr2Constr -> ConeMatrixStuffing -> CLARABEL (CVXPY) Mar 27 04:17:34 AM: Applying reduction Dcp2Cone (CVXPY) Mar 27 04:17:34 AM: Applying reduction CvxAttr2Constr (CVXPY) Mar 27 04:17:34 AM: Applying reduction ConeMatrixStuffing (CVXPY) Mar 27 04:17:34 AM: Applying reduction CLARABEL (CVXPY) Mar 27 04:17:34 AM: Finished problem compilation (took 3.645e-02 seconds). ------------------------------------------------------------------------------- Numerical solver ------------------------------------------------------------------------------- (CVXPY) Mar 27 04:17:34 AM: Invoking solver CLARABEL to obtain a solution. ------------------------------------------------------------- Clarabel.rs v0.10.0 - Clever Acronym (c) Paul Goulart University of Oxford, 2022 ------------------------------------------------------------- problem: variables = 7357 constraints = 9808 nnz(P) = 0 nnz(A) = 29420 cones (total) = 1 : Nonnegative = 1, numel = 9808 settings: linear algebra: direct / qdldl, precision: 64 bit max iter = 200, time limit = Inf, max step = 0.990 tol_feas = 1.0e-8, tol_gap_abs = 1.0e-8, tol_gap_rel = 1.0e-8, static reg : on, ϵ1 = 1.0e-8, ϵ2 = 4.9e-32 dynamic reg: on, ϵ = 1.0e-13, δ = 2.0e-7 iter refine: on, reltol = 1.0e-13, abstol = 1.0e-12, max iter = 10, stop ratio = 5.0 equilibrate: on, min_scale = 1.0e-4, max_scale = 1.0e4 max iter = 10 iter pcost dcost gap pres dres k/t μ step --------------------------------------------------------------------------------------------- 0 -4.0106e-13 +6.5237e-13 1.05e-12 9.50e-01 9.69e-02 1.00e+00 5.85e+02 ------ 1 +6.2890e+02 +6.5137e+02 3.57e-02 1.49e-01 1.41e-02 2.26e+01 9.99e+01 8.69e-01 2 +4.3219e+02 +4.3255e+02 8.24e-04 1.20e-02 3.82e-04 3.60e-01 2.48e+00 9.76e-01 3 +4.8303e+02 +4.8310e+02 1.37e-04 3.50e-03 5.66e-05 6.68e-02 3.58e-01 8.64e-01 4 +5.3643e+02 +5.3645e+02 3.38e-05 6.37e-04 9.83e-06 1.82e-02 6.27e-02 8.53e-01 5 +6.0974e+02 +6.0975e+02 1.70e-05 2.60e-04 3.80e-06 1.04e-02 2.55e-02 6.78e-01 6 +6.3872e+02 +6.3873e+02 8.55e-06 1.22e-04 1.71e-06 5.48e-03 1.20e-02 5.82e-01 7 +6.5186e+02 +6.5187e+02 5.36e-06 6.31e-05 8.52e-07 3.50e-03 6.19e-03 6.27e-01 8 +6.5887e+02 +6.5888e+02 2.92e-06 2.96e-05 3.90e-07 1.93e-03 2.90e-03 6.95e-01 9 +6.6168e+02 +6.6168e+02 1.64e-06 1.56e-05 2.03e-07 1.09e-03 1.53e-03 6.06e-01 10 +6.6339e+02 +6.6339e+02 7.88e-07 7.10e-06 9.18e-08 5.24e-04 6.96e-04 6.79e-01 11 +6.6397e+02 +6.6397e+02 4.80e-07 4.18e-06 5.39e-08 3.20e-04 4.10e-04 5.74e-01 12 +6.6450e+02 +6.6450e+02 1.90e-07 1.63e-06 2.09e-08 1.27e-04 1.60e-04 6.83e-01 13 +6.6465e+02 +6.6465e+02 1.05e-07 8.82e-07 1.13e-08 6.96e-05 8.66e-05 5.77e-01 14 +6.6474e+02 +6.6474e+02 5.55e-08 4.65e-07 5.95e-09 3.70e-05 4.56e-05 5.50e-01 15 +6.6480e+02 +6.6480e+02 1.65e-08 1.34e-07 1.71e-09 1.10e-05 1.31e-05 9.14e-01 16 +6.6482e+02 +6.6482e+02 5.49e-09 4.44e-08 5.69e-10 3.65e-06 4.36e-06 6.83e-01 17 +6.6483e+02 +6.6483e+02 1.77e-09 1.43e-08 1.83e-10 1.18e-06 1.40e-06 7.84e-01 18 +6.6483e+02 +6.6483e+02 2.61e-10 2.10e-09 2.68e-11 1.74e-07 2.06e-07 9.04e-01 --------------------------------------------------------------------------------------------- Terminated with status = Solved solve time = 163.759468ms ------------------------------------------------------------------------------- Summary ------------------------------------------------------------------------------- (CVXPY) Mar 27 04:17:34 AM: Problem status: optimal (CVXPY) Mar 27 04:17:34 AM: Optimal value: 6.648e+02 (CVXPY) Mar 27 04:17:34 AM: Compilation took 3.645e-02 seconds (CVXPY) Mar 27 04:17:34 AM: Solver (including time spent in interface) took 1.771e-01 seconds