<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Marco Pinto-Orellana</title>
  <subtitle>Computer scientist working on brain signal analysis, signal processing and statistics.</subtitle>
  <link href="https://pintoorellana.org/atom.xml" rel="self" type="application/atom+xml"/>
  <link href="https://pintoorellana.org/" rel="alternate" type="text/html"/>
  <id>https://pintoorellana.org/atom.xml</id>
  <updated>2026-11-07T00:00:00Z</updated>
  <author><name>Marco Pinto-Orellana</name></author>
  <entry>
    <title>Optimization models in Python with PuLP</title>
    <link href="https://pintoorellana.org/blog/linear-programming/" rel="alternate" type="text/html"/>
    <id>https://pintoorellana.org/blog/linear-programming/</id>
    <published>2026-11-07T00:00:00Z</published>
    <updated>2026-11-07T00:00:00Z</updated>
    <summary>Facility location, the p-median problem, the traveling salesman and vehicle routing, written as integer linear programs and solved in Python.</summary>
    <content type="html" xml:base="https://pintoorellana.org/blog/linear-programming/">&lt;p&gt;This post is a written version of an invited lecture on Python for optimization and logistics
that I taught at the Universidad San Francisco Xavier de Chuquisaca (USFX), in Sucre,
Bolivia. The lectures follows four classic problems: placing two hospitals, the p-median
problem, the traveling salesman problem and vehicle routing.&lt;/p&gt;
&lt;h2 id=&quot;tools&quot;&gt;Tools&lt;/h2&gt;
&lt;p&gt;The models use &lt;a href=&quot;https://coin-or.github.io/pulp/&quot;&gt;PuLP&lt;/a&gt;, a Python library for writing linear programs, which calls &lt;a href=&quot;https://www.gnu.org/software/glpk/&quot;&gt;GLPK&lt;/a&gt; (an open-source mixed integer linear programming solver) behind the scenes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python -m pip install pulp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PuLP also ships with the CBC solver. If GLPK is not installed, replace
&lt;code&gt;GLPK(options=[&apos;--mipgap&apos;, &apos;0.01&apos;])&lt;/code&gt; in the code below with &lt;code&gt;PULP_CBC_CMD(gapRel=0.01)&lt;/code&gt;.
Both stop once the solution is within 1% of the optimum.&lt;/p&gt;
&lt;p&gt;A small helper file, &lt;code&gt;utils.py&lt;/code&gt;, creates the variables in bulk and runs the solver:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from pulp import *

def create_variable_matrix(i, j, name=&amp;quot;x_{i}_{j}&amp;quot;, **other_args):
    return [[LpVariable(name.format(i=a, j=b), **other_args) for b in range(j)]
            for a in range(i)]

def create_variable_array(i, name=&amp;quot;y_{i}&amp;quot;, **other_args):
    return [LpVariable(name.format(i=a), **other_args) for a in range(i)]

def euclidian_distance(point1, point2):
    return ((point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2) ** 0.5

def create_distance_matrix(points):
    return [[euclidian_distance(a, b) for b in points] for a in points]

def resolve_problem(problem, show_variables=True):
    GLPK(options=[&apos;--mipgap&apos;, &apos;0.01&apos;]).solve(problem)
    if show_variables:
        for v in problem.variables():
            if v.varValue != 0:
                print(v.name, &amp;quot;=&amp;quot;, v.varValue)
    return value(problem.objective)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that &lt;code&gt;LpVariable&lt;/code&gt; takes three main params: &lt;code&gt;cat&lt;/code&gt;, the kind of variable (&lt;code&gt;&apos;Continuous&apos;&lt;/code&gt; or &lt;code&gt;&apos;Integer&apos;&lt;/code&gt;); &lt;code&gt;lowBound&lt;/code&gt;, its minimum (&lt;span class=&quot;math&quot;&gt;\(-\infty\)&lt;/span&gt; when omitted); and &lt;code&gt;upBound&lt;/code&gt;, its maximum (&lt;span class=&quot;math&quot;&gt;\(+\infty\)&lt;/span&gt; when omitted).&lt;/p&gt;
&lt;p&gt;An integer variable between 0 and 1 is a yes-or-no decision. A &lt;span class=&quot;math&quot;&gt;\(5 \times 10\)&lt;/span&gt; matrix of them, and a list of 17 non-negative integers can be written as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;f = create_variable_matrix(5, 10, name=&amp;quot;F_{i}_{j}&amp;quot;, cat=&apos;Integer&apos;, lowBound=0, upBound=1)
l = create_variable_array(17, name=&amp;quot;L_{i}&amp;quot;, cat=&apos;Integer&apos;, lowBound=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A model has the class &lt;code&gt;LpProblem&lt;/code&gt;. It has overloaded the operator &lt;code&gt;+=&lt;/code&gt; to allow an interesting syntax for adding constraint: &lt;code&gt;model += constraint&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;problem = LpProblem(&amp;quot;my_model&amp;quot;, LpMinimize)   # or LpMaximize
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;model-a-two-hospitals&quot;&gt;Model A: two hospitals&lt;/h2&gt;
&lt;p&gt;Seven neighborhoods need two new hospitals, each built in one of the neighborhoods. Where should they go?&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/linear-programming/two-hospitals.webp&quot; alt=&quot;Three panels: a table of the coordinates of the seven neighborhoods, the neighborhoods as points in the plane joined by every possible connection, and a heat map of the distances between them.&quot; width=&quot;1400&quot; height=&quot;700&quot; loading=&quot;lazy&quot;&gt;  &lt;figcaption&gt;The seven neighborhoods, every possible connection between them, and their distance matrix.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;A good solution should keep every neighborhood close to the hospital that serves it, so the
objective is the sum of the distances from each neighborhood to its hospital. That
needs two kinds of decisions to be modeled:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class=&quot;math&quot;&gt;\(x_{ij} = 1\)&lt;/span&gt; if neighborhood &lt;span class=&quot;math&quot;&gt;\(i\)&lt;/span&gt; is served by a hospital in neighborhood &lt;span class=&quot;math&quot;&gt;\(j\)&lt;/span&gt;, and
&lt;span class=&quot;math&quot;&gt;\(0\)&lt;/span&gt; otherwise, for &lt;span class=&quot;math&quot;&gt;\(0 \le i, j \le 6\)&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;math&quot;&gt;\(y_j = 1\)&lt;/span&gt; if a hospital is built in neighborhood &lt;span class=&quot;math&quot;&gt;\(j\)&lt;/span&gt;, and &lt;span class=&quot;math&quot;&gt;\(0\)&lt;/span&gt; otherwise.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;problem = LpProblem(&amp;quot;two_hospital_problem&amp;quot;, LpMinimize)
x = create_variable_matrix(7, 7, cat=&apos;Integer&apos;, lowBound=0, upBound=1)
y = create_variable_array(7, name=&amp;quot;y_{i}&amp;quot;, cat=&apos;Integer&apos;, lowBound=0, upBound=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;span class=&quot;math&quot;&gt;\(d_{ij}\)&lt;/span&gt; is the distance between neighborhoods &lt;span class=&quot;math&quot;&gt;\(i\)&lt;/span&gt; and &lt;span class=&quot;math&quot;&gt;\(j\)&lt;/span&gt;, the objective is&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[\min \sum_{i=0}^{6} \sum_{j=0}^{6} d_{ij}\, x_{ij} = 0.0\,x_{0,0} + 20.0\,x_{0,1} + 20.0\,x_{0,2} + 28.3\,x_{0,3} + \dots + 14.1\,x_{6,5} + 0.0\,x_{6,6}.\]&lt;/div&gt;
&lt;p&gt;This means that the objective function needs to link all possible &lt;span class=&quot;math&quot;&gt;\(7 * 7\)&lt;/span&gt; combinations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;objective = (0.0 * x[0][0] + 20.0 * x[0][1] + 20.0 * x[0][2] + 28.3 * x[0][3]
             + 14.1 * x[0][4] + 22.4 * x[0][5] + 22.4 * x[0][6]
             + 20.0 * x[1][0] + ...)                 # 49 terms in all
problem += objective
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a trivial solution: &lt;span class=&quot;math&quot;&gt;\(x=0\)&lt;/span&gt;, i.e., the solution that minimizes the distance between hospitals and neighborhoods is &lt;strong&gt;to not build any hospital at all&lt;/strong&gt;.
So, we need additional constraints to make a solution valid:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Every neighborhood is served by exactly one hospital:
&lt;span class=&quot;math&quot;&gt;\(\sum_{j} x_{ij} = 1\)&lt;/span&gt; for every &lt;span class=&quot;math&quot;&gt;\(i\)&lt;/span&gt;. For neighborhood 0 that is
&lt;span class=&quot;math&quot;&gt;\(x_{0,0} + x_{0,1} + \dots + x_{0,6} = 1\)&lt;/span&gt;, and the same holds for the other six.&lt;/li&gt;
&lt;li&gt;There are exactly two hospitals: &lt;span class=&quot;math&quot;&gt;\(\sum_{j} y_j = 2\)&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;A neighborhood can only be served by a hospital that exists: if &lt;span class=&quot;math&quot;&gt;\(x_{ij} = 1\)&lt;/span&gt;, then
&lt;span class=&quot;math&quot;&gt;\(y_j\)&lt;/span&gt; must be 1. We can write this as a linear constraint, &lt;span class=&quot;math&quot;&gt;\(x_{ij} \le y_j\)&lt;/span&gt; for every &lt;span class=&quot;math&quot;&gt;\(i\)&lt;/span&gt; and &lt;span class=&quot;math&quot;&gt;\(j\)&lt;/span&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for i in range(7):                              # 1. one hospital per neighborhood
    problem += lpSum(x[i][j] for j in range(7)) == 1
problem += lpSum(y) == 2                        # 2. two hospitals
for i in range(7):                              # 3. only hospitals that exist
    for j in range(7):
        problem += x[i][j] &amp;lt;= y[j]

resolve_problem(problem, show_variables=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we run the code, GLPK prints its progress, then the variables that are not zero:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;x_0_4 = 1
x_1_5 = 1
x_2_4 = 1
x_3_5 = 1
x_4_4 = 1
x_5_5 = 1
x_6_4 = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can interpret this as the hospitals should be built in neighborhoods 4 and 5.
Neighbourhoods 0, 2, 4 and 6 use the hospital in 4; neighborhoods 1, 3 and 5 use the one in 5. The total distance is 58.2.&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/linear-programming/two-hospitals-solution.webp&quot; alt=&quot;Solution drawn in black: two star-shaped groups of connections, centered on neighborhoods 4 and 5&quot; width=&quot;1400&quot; height=&quot;700&quot; loading=&quot;lazy&quot;&gt;  &lt;figcaption&gt;Solution drawn in black: two star-shaped groups of connections, centered on neighborhoods 4 and 5&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;model-b-p-median-problem&quot;&gt;Model B: p-median problem&lt;/h2&gt;
&lt;p&gt;The two-hospital model is a special case of a &lt;em&gt;location problem&lt;/em&gt;. There are &lt;span class=&quot;math&quot;&gt;\(n\)&lt;/span&gt;
customers to serve and &lt;span class=&quot;math&quot;&gt;\(m\)&lt;/span&gt; places where a service point could open. The p-median problem
opens &lt;span class=&quot;math&quot;&gt;\(p\)&lt;/span&gt; of them and assigns every customer to one open service point, so that the sum
of the distances from the customers to their service points is as small as possible.&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/linear-programming/p-median.webp&quot; alt=&quot;20 random points joined by every possible connection&quot; width=&quot;1400&quot; height=&quot;700&quot; loading=&quot;lazy&quot;&gt;  &lt;figcaption&gt;20 random points joined by every possible connection&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The variables are similar to those in model A: &lt;span class=&quot;math&quot;&gt;\(y_j = 1\)&lt;/span&gt; if a service point opens at &lt;span class=&quot;math&quot;&gt;\(j\)&lt;/span&gt;, and
&lt;span class=&quot;math&quot;&gt;\(x_{ij} = 1\)&lt;/span&gt; if customer &lt;span class=&quot;math&quot;&gt;\(i\)&lt;/span&gt; is served from &lt;span class=&quot;math&quot;&gt;\(j\)&lt;/span&gt;. So is the model, with &lt;span class=&quot;math&quot;&gt;\(p\)&lt;/span&gt; in place of 2:&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[\begin{aligned}
\min\ &amp;amp; \sum_{i=1}^{n} \sum_{j=1}^{m} d_{ij}\, x_{ij} \\
\text{subject to}\ &amp;amp; \sum_{j=1}^{m} x_{ij} = 1, &amp;amp;&amp;amp; i = 1, \dots, n, \\
&amp;amp; \sum_{j=1}^{m} y_j = p, \\
&amp;amp; x_{ij} \le y_j, &amp;amp;&amp;amp; i = 1, \dots, n,\ j = 1, \dots, m, \\
&amp;amp; x_{ij}, y_j \in \{0, 1\}
\end{aligned}\]&lt;/div&gt;
&lt;p&gt;This time we can write this objective function can be written as loop for any number of points and any &lt;span class=&quot;math&quot;&gt;\(p\)&lt;/span&gt;.
To keep the example simple, every customer is also a candidate place (&lt;span class=&quot;math&quot;&gt;\(m = n\)&lt;/span&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def p_median_problem(distance_matrix, facilities):
    n = len(distance_matrix)
    problem = LpProblem(&amp;quot;p_median_problem&amp;quot;, LpMinimize)
    x = create_variable_matrix(n, n, cat=&apos;Integer&apos;, lowBound=0, upBound=1)
    y = create_variable_array(n, name=&amp;quot;y_{i}&amp;quot;, cat=&apos;Integer&apos;, lowBound=0, upBound=1)

    problem += lpSum(distance_matrix[i][j] * x[i][j] for i in range(n) for j in range(n))
    for i in range(n):                          # each customer served once
        problem += lpSum(x[i][j] for j in range(n)) == 1
    problem += lpSum(y) == facilities           # exactly p service points
    for i in range(n):                          # only from open service points
        for j in range(n):
            problem += x[i][j] &amp;lt;= y[j]

    return resolve_problem(problem)
&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/linear-programming/p-median-solution.webp&quot; alt=&quot;The best three service points for the 20 customers&quot; width=&quot;1400&quot; height=&quot;700&quot; loading=&quot;lazy&quot;&gt;  &lt;figcaption&gt;The best three service points for the 20 customers&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;model-c-the-traveling-salesman&quot;&gt;Model C: the traveling salesman&lt;/h2&gt;
&lt;p&gt;This is one of the most classical examples in linear programming or optimization.
Assume we know &lt;span class=&quot;math&quot;&gt;\(n\)&lt;/span&gt; cities and the distances between them, the traveling salesman problem looks for
the shortest route that starts in one city, visits each of the others exactly once and
comes back.&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/linear-programming/tsp.webp&quot; alt=&quot;20 cities to visit&quot; width=&quot;1400&quot; height=&quot;700&quot; loading=&quot;lazy&quot;&gt;  &lt;figcaption&gt;20 cities to visit&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Let &lt;span class=&quot;math&quot;&gt;\(x_{ij} = 1\)&lt;/span&gt; if the route goes straight from city &lt;span class=&quot;math&quot;&gt;\(i\)&lt;/span&gt; to city &lt;span class=&quot;math&quot;&gt;\(j\)&lt;/span&gt;, and &lt;span class=&quot;math&quot;&gt;\(0\)&lt;/span&gt; otherwise.
Thus, the objective is the length of the route:&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[\min \sum_{i=1}^{n} \sum_{j=1}^{n} d_{ij}\, x_{ij}.\]&lt;/div&gt;
&lt;p&gt;At first sight two constraints are enough and intutive enough: one road arrives at each city, and one road leaves it.&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[\sum_{i=1}^{n} x_{ik} = 1 \quad\text{and}\quad \sum_{j=1}^{n} x_{kj} = 1, \qquad k = 1, \dots, n.\]&lt;/div&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for k in range(n):
    problem += lpSum(x[i][k] for i in range(n)) == 1   # one road in
    problem += lpSum(x[k][j] for j in range(n)) == 1   # one road out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Are those two enough? &lt;strong&gt;No!&lt;/strong&gt;. This is a valid solution only using those two constraints:&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/linear-programming/tsp-subtours.webp&quot; alt=&quot;A solution using only 2 constraints may involve several small loops&quot; width=&quot;1400&quot; height=&quot;700&quot; loading=&quot;lazy&quot;&gt;  &lt;figcaption&gt;A solution using only 2 constraints may involve several small loops&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The answer is correct for the model: every city has exactly one road in and one road out.
Nothing in the model informs that the roads must form a single loop, and several short loops
are cheaper than one long one. We often called these loops as &lt;em&gt;subtours&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;There are several ways to rule them out. We use the &lt;em&gt;flow formulation&lt;/em&gt; (Gavish&amp;amp;Graves, 1978).
They solve the subtours issue in a clever way: the idea was to add a new variable: &lt;span class=&quot;math&quot;&gt;\(f_{ij} \ge 0\)&lt;/span&gt;, an amount of flow
sent along the road from &lt;span class=&quot;math&quot;&gt;\(i\)&lt;/span&gt; to &lt;span class=&quot;math&quot;&gt;\(j\)&lt;/span&gt;.
This amount is not measured and does not appear in the objective function, but it works as a variable &lt;em&gt;that appears while the model is running&lt;/em&gt; (like a temporary variable in a programming language!).
Using it, now we can model that city 1 need to send out &lt;span class=&quot;math&quot;&gt;\(n - 1\)&lt;/span&gt; units, and every other city
keeps exactly one:&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[\sum_{i=1}^{n} f_{ik} - \sum_{j=1}^{n} f_{kj} = 1, \qquad k = 2, \dots, n.\]&lt;/div&gt;
&lt;p&gt;Flow can only travel along roads that the route uses, and since no road ever needs to carry
more than &lt;span class=&quot;math&quot;&gt;\(n - 1\)&lt;/span&gt; units,&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[f_{ij} \le (n - 1)\, x_{ij}, \qquad i, j = 1, \dots, n,\ i \ne j.\]&lt;/div&gt;
&lt;p&gt;A subtour that does not pass through city 1 has no road bringing flow in from outside,
and each of its cities must keep one unit. That is impossible, so every city has to be on
the same loop as city 1. For the same reason, a city cannot be its own loop
(&lt;span class=&quot;math&quot;&gt;\(x_{kk} = 1\)&lt;/span&gt;), which is why the code does not have to exclude those variables.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def travelling_salesman_problem(distance_matrix):
    n = len(distance_matrix)
    problem = LpProblem(&amp;quot;travelling_salesman_problem&amp;quot;, LpMinimize)
    x = create_variable_matrix(n, n, cat=&apos;Integer&apos;, lowBound=0, upBound=1)
    f = create_variable_matrix(n, n, name=&amp;quot;f_{i}_{j}&amp;quot;, cat=&apos;Integer&apos;, lowBound=0)

    problem += lpSum(distance_matrix[i][j] * x[i][j] for i in range(n) for j in range(n))
    for k in range(n):
        problem += lpSum(x[i][k] for i in range(n)) == 1          # one road in
        problem += lpSum(x[k][j] for j in range(n)) == 1          # one road out
    for k in range(1, n):                                          # each city keeps one unit
        problem += lpSum(f[i][k] for i in range(n)) - lpSum(f[k][j] for j in range(n)) == 1
    for i in range(n):                                             # flow only on used roads
        for j in range(n):
            if i != j:
                problem += f[i][j] &amp;lt;= (n - 1) * x[i][j]

    GLPK(options=[&apos;--mipgap&apos;, &apos;0.01&apos;, &apos;--tmlim&apos;, &apos;30&apos;]).solve(problem)
    return value(problem.objective)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;--tmlim 30&lt;/code&gt; option stops GLPK after 30 seconds, so on larger instances the result is
the best route found in that time, not necessarily the shortest one.&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/linear-programming/tsp-solution.webp&quot; alt=&quot;With the flow constraints, the solution is a single route through every city&quot; width=&quot;1400&quot; height=&quot;700&quot; loading=&quot;lazy&quot;&gt;  &lt;figcaption&gt;With the flow constraints, the solution is a single route through every city&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;model-d-vehicle-routing&quot;&gt;Model D: vehicle routing&lt;/h2&gt;
&lt;p&gt;The vehicle routing problem could be seen as a expansion of the TSP, where we have more than one salesman (or trucks in this case) and the traveler can only bring a limited amount of supply or goods.
A depot, point 0, serves &lt;span class=&quot;math&quot;&gt;\(n\)&lt;/span&gt; customers, numbered 1 to &lt;span class=&quot;math&quot;&gt;\(n\)&lt;/span&gt;.
Customer &lt;span class=&quot;math&quot;&gt;\(k\)&lt;/span&gt; needs &lt;span class=&quot;math&quot;&gt;\(q_k\)&lt;/span&gt; units of goods, and the depot needs none (&lt;span class=&quot;math&quot;&gt;\(q_0 = 0\)&lt;/span&gt;).
There are as many vehicles as needed, each carrying at most &lt;span class=&quot;math&quot;&gt;\(Q\)&lt;/span&gt; units. The goal is the cheapest
set of routes such that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;every route starts and ends at the depot,&lt;/li&gt;
&lt;li&gt;every customer’s demand is delivered, and&lt;/li&gt;
&lt;li&gt;no route carries more than &lt;span class=&quot;math&quot;&gt;\(Q\)&lt;/span&gt; units in total.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/linear-programming/vrp.webp&quot; alt=&quot;A depot and 9 customers&quot; width=&quot;1400&quot; height=&quot;700&quot; loading=&quot;lazy&quot;&gt;  &lt;figcaption&gt;A depot and 9 customers&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Defining &lt;span class=&quot;math&quot;&gt;\(c_{ij}\)&lt;/span&gt; as the cost of traveling from &lt;span class=&quot;math&quot;&gt;\(i\)&lt;/span&gt; to &lt;span class=&quot;math&quot;&gt;\(j\)&lt;/span&gt;, the variables &lt;span class=&quot;math&quot;&gt;\(x_{ij} = 1\)&lt;/span&gt; if a
vehicle goes from &lt;span class=&quot;math&quot;&gt;\(i\)&lt;/span&gt; to &lt;span class=&quot;math&quot;&gt;\(j\)&lt;/span&gt;, and &lt;span class=&quot;math&quot;&gt;\(f_{ij} \ge 0\)&lt;/span&gt;, the amount of goods carried from &lt;span class=&quot;math&quot;&gt;\(i\)&lt;/span&gt; to
&lt;span class=&quot;math&quot;&gt;\(j\)&lt;/span&gt;, the model is then:&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[\begin{aligned}
\min\ &amp;amp; \sum_{i=0}^{n} \sum_{j=0}^{n} c_{ij}\, x_{ij} \\
\text{subject to}\ &amp;amp; \sum_{j=0}^{n} x_{kj} = 1, \quad \sum_{i=0}^{n} x_{ik} = 1, &amp;amp;&amp;amp; k = 1, \dots, n, \\
&amp;amp; \sum_{j=1}^{n} x_{0j} = \sum_{i=1}^{n} x_{i0}, \\
&amp;amp; \sum_{i=0}^{n} f_{ik} - \sum_{j=0}^{n} f_{kj} = q_k, &amp;amp;&amp;amp; k = 1, \dots, n, \\
&amp;amp; f_{ij} \le Q\, x_{ij}, &amp;amp;&amp;amp; i, j = 0, \dots, n,\ i \ne j.
\end{aligned}\]&lt;/div&gt;
&lt;p&gt;Line by line the model can be interpreted as we did before:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One vehicle leaves and one arrives at each customer&lt;/li&gt;
&lt;li&gt;As many vehicles come back to the depot as leave it&lt;/li&gt;
&lt;li&gt;Each customer keeps exactly its demand from the goods that pass through it&lt;/li&gt;
&lt;li&gt;And, no road carries more than a vehicle holds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The flow constraints play the same role as in the TSP model:  goods can only start at the depot, so
every route must pass through it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def vehicle_routing_problem(distance_matrix, demand, vehicle_capacity):
    n = len(distance_matrix)
    problem = LpProblem(&amp;quot;vehicle_routing_problem&amp;quot;, LpMinimize)
    x = create_variable_matrix(n, n, cat=&apos;Integer&apos;, lowBound=0, upBound=1)
    f = create_variable_matrix(n, n, name=&amp;quot;f_{i}_{j}&amp;quot;, cat=&apos;Integer&apos;, lowBound=0)

    problem += lpSum(distance_matrix[i][j] * x[i][j] for i in range(n) for j in range(n))
    for k in range(1, n):
        problem += lpSum(x[k][j] for j in range(n)) == 1          # one vehicle leaves
        problem += lpSum(x[i][k] for i in range(n)) == 1          # one vehicle arrives
    problem += (lpSum(x[0][j] for j in range(1, n))                # depot: as many out as in
                == lpSum(x[i][0] for i in range(1, n)))
    for k in range(1, n):                                          # each customer keeps q_k
        problem += lpSum(f[i][k] for i in range(n)) - lpSum(f[k][j] for j in range(n)) == demand[k]
    for i in range(n):                                             # vehicle capacity
        for j in range(n):
            if i != j:
                problem += f[i][j] &amp;lt;= vehicle_capacity * x[i][j]

    return resolve_problem(problem)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the example every customer needs one unit and a vehicle carries five, so the nine
customers need at least two vehicles. The best plan uses exactly two, which could make
their deliveries at the same time.&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/linear-programming/vrp-solution.webp&quot; alt=&quot;Optimal route: 2 vehicles with their own loop from the depot&quot; width=&quot;1400&quot; height=&quot;700&quot; loading=&quot;lazy&quot;&gt;  &lt;figcaption&gt;Optimal route: 2 vehicles with their own loop from the depot&lt;/figcaption&gt;
&lt;/figure&gt;
</content>
  </entry>
  <entry>
    <title>Numerical Hell: When Cross-Language Code Breaks (Part 1)</title>
    <link href="https://pintoorellana.org/blog/numerical-hell-1/" rel="alternate" type="text/html"/>
    <id>https://pintoorellana.org/blog/numerical-hell-1/</id>
    <published>2025-02-09T00:00:00Z</published>
    <updated>2025-02-09T00:00:00Z</updated>
    <summary>The same FIR filter gives different coefficients, and different detections, in MATLAB, Octave, Python and R.</summary>
    <content type="html" xml:base="https://pintoorellana.org/blog/numerical-hell-1/">&lt;p&gt;I believe engineers have an unspoken rule: we trust our tools. When you design an &lt;a href=&quot;https://en.wikipedia.org/wiki/Infinite_impulse_response&quot;&gt;IIR filter&lt;/a&gt; or an &lt;a href=&quot;https://en.wikipedia.org/wiki/Finite_impulse_response&quot;&gt;FIR filter&lt;/a&gt;, you expect identical results regardless of your programming language. While I suspected this assumption wouldn’t always hold, I never gave it much thought until recently.&lt;/p&gt;
&lt;p&gt;Working with electrical brain signals, my lab analyzes bursts that could help identify seizure-generating tissue. Some of our legacy code runs in MATLAB, but I had my statistical and machine learning pipelines in Python. After having horrible experiences in the past with bugs while connecting MATLAB-Python, I had two options: either use an &lt;a href=&quot;https://blink1073.github.io/oct2py/&quot;&gt;Python-Octave bridge (oct2py)&lt;/a&gt; or rewrite the algorithm in Python. Due to memory efficiency, I decided to port the algorithm entirely.&lt;/p&gt;
&lt;p&gt;The plan was simple: implement everything identically and use a unit test battery that can confirm the results are the same. The code looked perfect until I ran the tests. To my surprise, around 20% failed. Well, it is known that some operations can raise sligtly different results on both platforms, as &lt;a href=&quot;https://github.com/scikit-image/scikit-image/issues/3742&quot;&gt;the inverse random transformation&lt;/a&gt;. Sometimes, due to the way the &lt;a href=&quot;https://github.com/zzd1992/Numpy-Pytorch-Bicubic/issues/1&quot;&gt;algorithm is implemented between MATLAB versions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After two frustrating days of debugging, I isolated several issues. Let’s start with the simplest and most frustrating one: the FIR filter design functions (&lt;code&gt;fir1&lt;/code&gt; in MATLAB/Octave, &lt;code&gt;firwin&lt;/code&gt; in scipy).&lt;/p&gt;
&lt;h2 id=&quot;minimal-example&quot;&gt;Minimal example&lt;/h2&gt;
&lt;p&gt;Here’s what we obtained as a FIR bandpass filter between 0.2&lt;span class=&quot;math&quot;&gt;\(f_s\)&lt;/span&gt; and 0.4&lt;span class=&quot;math&quot;&gt;\(f_s\)&lt;/span&gt; with a Hamming window (&lt;span class=&quot;math&quot;&gt;\(f_s\)&lt;/span&gt; is the sampling frequency) in Octave, python and R:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-matlab&quot;&gt;% Octave 9.3.0 / signal 1.4.6
pkg load signal
fir1(2, [0.1, 0.2], &amp;quot;bandpass&amp;quot;)
% ans =
%    0.06310305169   0.88754953849   0.06310305169

% Matlab 2024b
format longg
fir1(2, [0.1, 0.2], &amp;quot;bandpass&amp;quot;)
%ans =
%   0.063016142388235   0.887704412005799   0.063016142388235
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Python 3.9.16 / NumPy 1.25.1 / SciPy 1.11.1
import scipy.signal
scipy.signal.firwin(2+1, [0.1, 0.2], pass_zero=&amp;quot;bandpass&amp;quot;, window=&amp;quot;hamming&amp;quot;, scale=1)
# array([0.06301614, 0.88770441, 0.06301614])
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-r&quot;&gt;# R 4.4.2 / signal 1.8-1
library(signal)
b = signal::fir1(2, c(0.1, 0.2), &amp;quot;pass&amp;quot;, window=&amp;quot;hamming&amp;quot;)
# [1] 0.06310305 0.88754954 0.06310305
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You might think “it’s just the Hamming window implementation.” Let’s try with a boxcar window:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-matlab&quot;&gt;% Octave
fir1(2, [0.1, 0.2], &amp;quot;bandpass&amp;quot;, &amp;quot;boxcar&amp;quot;)
% ans =
%    0.343971272   0.387038706   0.343971272
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Python
scipy.signal.firwin(2+1, [0.1, 0.2], pass_zero=&amp;quot;bandpass&amp;quot;, window=&amp;quot;boxcar&amp;quot;, scale=1)
# array([0.34376453, 0.38740713, 0.34376453])
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-r&quot;&gt;# R
signal::fir1(2, c(0.1, 0.2), &amp;quot;pass&amp;quot;, window=&amp;quot;boxcar&amp;quot;)
# [1] 0.3439713 0.3870387 0.3439713
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These differences might seem negligible. But when I applied this filter to detect bursts in a signal, the results were surprisingly different. Assume that we record the following signal:&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/numerical-hell-1/sample/example-pulses-numerical-diff.png&quot; alt=&quot;Sample signal&quot; width=&quot;966&quot; height=&quot;554&quot; loading=&quot;lazy&quot;&gt;&lt;/figure&gt;
&lt;p&gt;To be precise, these are the recorded values:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x = [ 1.76405235, 0.40015721, 0.97873798, 2.2408932 , 1.86755799, -0.97727788, 0.95008842, -0.15135721, -0.10321885, 0.4105985 , 0.14404357, 1.45427351, 0.76103773, 0.12167502, 0.44386323, 0.33367433, 1.49407907, -0.20515826, 0.3130677 , -0.85409574, -2.55298982, 0.6536186 , 0.8644362 , -0.74216502, 2.26975462, -1.45436567, 0.04575852, -0.18718385, 1.53277921, 1.46935877, 0.15494743, 0.37816252, -0.88778575, -1.98079647, -0.34791215, 0.15634897, 1.23029068, 1.20237985, -0.38732682, -0.30230275, -1.04855297, -1.42001794, -1.70627019, 1.9507754 , -0.50965218, -0.4380743 , -1.25279536, 0.77749036, -1.61389785, -0.21274028, -0.89546656, 0.3869025 , -0.51080514, -1.18063218, -0.02818223, 0.42833187, 0.06651722, 0.3024719 , -0.63432209, -0.36274117, -0.67246045, -0.35955316, -0.81314628, -1.7262826 , 0.17742614, -0.40178094, -1.63019835, 0.46278226, -0.90729836, 0.0519454 , 0.72909056, 0.12898291, 1.13940068, -1.23482582, 0.40234164, -0.68481009, -0.87079715, -0.57884966, -0.31155253, 0.05616534, -1.16514984, 0.90082649, 0.46566244, -1.53624369, 1.48825219, 1.89588918, 1.17877957, -0.17992484, -1.07075262, 1.05445173, -0.40317695, 1.22244507, 0.20827498, 0.97663904, 0.3563664 , 0.70657317, 0.01050002, 1.78587049, 0.12691209, 0.40198936]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we need to know &lt;strong&gt;how many pulses are inferred&lt;/strong&gt; if we use a detection threshold of 1 mean + 1 standard deviation. We got THREE different results based on the implementation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#python-example&quot;&gt;Python implementation&lt;/a&gt;: 9 bursts&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#r-example&quot;&gt;R implementation&lt;/a&gt;: 11 bursts&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#octave-example&quot;&gt;Octave implementation&lt;/a&gt;: 12 bursts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The implications are concerning. If such a basic operation produces different results, what other disparities might exist in more complex algorithms? Are other discrepancies due to language and operating systems (I tested the method on the same laptop)? Is our assumption of cross-platform consistency fundamentally flawed?&lt;/p&gt;
&lt;p&gt;In the next post, I’ll explore why these differences occur and discuss potential solutions. For now, remember: when porting algorithms between languages, verify every numerical operation, no matter how basic it seems.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id=&quot;octave-example&quot;&gt;Octave code&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-matlab&quot;&gt;output_precision (10)
b = fir1(2, [0.1, 0.2], &amp;quot;bandpass&amp;quot;)
% b =
%    0.06310305169   0.88754953849   0.06310305169
x_filtered = filtfilt(b, 1, x);
y = x_filtered &amp;gt; mean(x_filtered) + std(x_filtered);
y = [0 y 0];
intervals = [find(diff(y)&amp;gt;0)&apos;, find(diff(y)&amp;lt;0)&apos;];
size(intervals)
% ans =
%    12    2
intervals
% intervals =
%     1    2
%     4    6
%    12   13
%    17   18
%    25   26
%    29   31
%    37   39
%    44   45
%    73   74
%    85   88
%    92   93
%    98   99

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;python-example&quot;&gt;Python code&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;b = scipy.signal.firwin(2+1, [0.1, 0.2], pass_zero=&amp;quot;bandpass&amp;quot;, window=&amp;quot;hamming&amp;quot;, scale=1)
b
# array([0.06301614, 0.88770441, 0.06301614])
x_filtered = scipy.signal.filtfilt(b, [1], x)
y = np.array(x_filtered) &amp;gt; (np.mean(x_filtered) + 1*np.std(x_filtered))
y = np.concatenate([[0], y, [0]])
interval_start = np.nonzero((np.diff(y)&amp;gt;0))[0]
interval_stop = np.nonzero((np.diff(y)&amp;lt;0))[0]
intervals = np.vstack([interval_start, interval_stop]).T
intervals.shape
# (9, 2)
intervals
# array([[ 5, 7],
#        [16, 18],
#        [31, 33],
#        [46, 47],
#        [49, 50],
#        [52, 53],
#        [57, 58],
#        [74, 75],
#        [88, 89]])

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;r-example&quot;&gt;R code&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-r&quot;&gt;library(signal)
b = signal::fir1(2, c(0.1, 0.2), &amp;quot;pass&amp;quot;, window=&amp;quot;hamming&amp;quot;)
b
#[1] 0.06310305 0.88754954 0.06310305
x_filtered = signal::filtfilt(b, x)
y = x_filtered &amp;gt; (mean(x_filtered) + sd(x_filtered))
y = c(0, y, 0)
intervals = cbind(which(diff(y) &amp;gt; 0), which(diff(y) &amp;lt; 0))
dim(intervals)
# [1] 11  2
intervals
#       [,1] [,2]
#  [1,]    1    2
#  [2,]    3    6
#  [3,]   12   13
#  [4,]   17   18
#  [5,]   25   26
#  [6,]   29   31
#  [7,]   37   39
#  [8,]   44   45
#  [9,]   85   88
# [10,]   92   93
# [11,]   98   99

&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>EDF files&apos; specification</title>
    <link href="https://pintoorellana.org/blog/edf-specification/" rel="alternate" type="text/html"/>
    <id>https://pintoorellana.org/blog/edf-specification/</id>
    <published>2024-11-11T00:00:00Z</published>
    <updated>2024-11-11T00:00:00Z</updated>
    <summary>A byte-by-byte tour of the EDF and EDF+ file formats for biomedical signals.</summary>
    <content type="html" xml:base="https://pintoorellana.org/blog/edf-specification/">&lt;p&gt;It’s 2024 and ChatGPT have been trained on all the Internet’s available information. So, it should contain inforabout a widely used file format from the 90s, right? In particular, I needed help on a data-file format used in biomedical datasets since 1992: EDF+C (European Data Format for contiguous stored signals). And, several LLMs hallucinated the results, regurgitating that EDF+C was a gzip-compressed format. Because &lt;code&gt;+C&lt;/code&gt;, the LLMs inferred that it meant “compression” and not “contiguous”. Given the age of the standard and its popularity among open-access datasets, it surprised me a lot.&lt;/p&gt;
&lt;p&gt;But, I needed to known about the EDF+C binary format, and I digged into the &lt;a href=&quot;https://www.edfplus.info/&quot;&gt; specification&lt;/a&gt; to explore how this format is implemented. This is the tutorial for dummies like me.&lt;/p&gt;
&lt;p&gt;Let’s recall that when signals are recording, we expected to have the following multidimensional pattern:&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/edf-specification/packets/multidim_signals.mmjs.svg&quot; alt=&quot;Diagram of a multichannel recording as channels by time samples&quot; width=&quot;579&quot; height=&quot;258&quot; loading=&quot;lazy&quot;&gt;&lt;/figure&gt;
&lt;p&gt;We should rememeber that this is an abstract representation of the data we will use for our algorithms, and not how the data will be stored in disk. Also, because there are additional information that could be valuable for our analysis such as when this data was recorded, a subject identifies, channel names, and so on, that should be stored somewhere.
A proper standard should standardize the way to store such information, while also allowing a fast data-access, and that’s precisely what the EDF format does. In particular, it uses the following structure by adding metadata and partioning the entire dataset into &lt;code&gt;records&lt;/code&gt; (in DL nomenclature, this would be equivalent to &lt;strong&gt;batches&lt;/strong&gt;):&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/edf-specification/packets/format.mmjs.svg&quot; alt=&quot;Diagram of an EDF file: general header, channel headers, then data records&quot; width=&quot;1330&quot; height=&quot;210&quot; loading=&quot;lazy&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Now, a file &lt;code&gt;dataset1.edf&lt;/code&gt; can be stored in either, the the &lt;a href=&quot;https://pubmed.ncbi.nlm.nih.gov/1374708/&quot;&gt;EDF1990&lt;/a&gt;
or the &lt;a href=&quot;https://pubmed.ncbi.nlm.nih.gov/12948806&quot;&gt;EDF+2003&lt;/a&gt; standard.
The latter, EDF+2003, is almost compatible with &lt;a href=&quot;https://pubmed.ncbi.nlm.nih.gov/1374708/&quot;&gt;EDF1990&lt;/a&gt; with a couple of differences.
The discrepancy that is most important to me is that EDF+2003 allows that contiguous records do not need to represent consecutive time intervals. This random recording behaviour is denoted as &lt;code&gt;EDF+D&lt;/code&gt; (discontiguous EDF), while the classical contiguous structure is categorized as &lt;code&gt;EDF+C&lt;/code&gt;. While, I think that EDF+D involves a huge penalty in computationally efficiency, that’s the standard.&lt;/p&gt;
&lt;h2 id=&quot;general-metadata&quot;&gt;General metadata&lt;/h2&gt;
&lt;p&gt;The first 256 of the file describes the main metadata: patient, date, time, storage mode, number of channels, records and the duration of each record. Note that classically, EDF names &lt;code&gt;channels&lt;/code&gt; as &lt;code&gt;signals&lt;/code&gt;, I’ll use channels here, becaue it’s less confusing that “signals”.&lt;/p&gt;
&lt;!-- 

[8,80,80,8,8,8,44,8,8,4]

--&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/edf-specification/packets/general_header.mmjs.svg&quot; alt=&quot;Byte layout of the 256-byte EDF general header&quot; width=&quot;738&quot; height=&quot;391&quot; loading=&quot;lazy&quot;&gt;&lt;/figure&gt;
&lt;p&gt;There are a few things that needs to be emphasized in this header:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The last field (bits &lt;code&gt;252-253&lt;/code&gt;) contains the total number of channels (or signals) in the file. So, the maximum number of simultaneous channels to be recorded is &lt;code&gt;9999&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;This header uses only strings, so the fields marked with &lt;code&gt;(N)&lt;/code&gt; are integers or floats saved as strings, and they will need to be parsed.&lt;/li&gt;
&lt;li&gt;The segment &lt;code&gt;Reserved(StorageMode)&lt;/code&gt; was supposed to be ignored in the &lt;a href=&quot;https://pubmed.ncbi.nlm.nih.gov/1374708/&quot;&gt;EDF1990&lt;/a&gt; standard. But in &lt;a href=&quot;https://pubmed.ncbi.nlm.nih.gov/12948806&quot;&gt;EDF+2003&lt;/a&gt; will contain the string “EDF+C” or “EDF+D” to identify if the recordings are contiguous or not.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;channel-level-metadata&quot;&gt;Channel-level metadata&lt;/h2&gt;
&lt;p&gt;Then, per each channel another 256 bytes are used to store channel-specific information.
We need to emphasize that here, &lt;strong&gt;all values are ascii strings&lt;/strong&gt;, even though that they represent numbers. I assume that the designers were inspired by the HDLC or TCP packet formats.&lt;/p&gt;
&lt;!-- 

[16,80,8,8,8,8,8,80,8,32]
bits_duration = np.repeat([16,80,8,8,8,8,8,80,8,32], 3)
bits_position = np.concatenate(([0], np.cumsum(bits_duration)))
print(&quot;\n&quot;.join([f&quot;{a}-{b}: \&quot;llll\&quot;&quot; for a,b in np.vstack((bits_position[:-1], -1+bits_position[1:])).T]))

--&gt;
&lt;p&gt;If the file only contains a single channel, this would be the header:&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/edf-specification/packets/channel_header_n1.mmjs.svg&quot; alt=&quot;Byte layout of the channel header for a single channel&quot; width=&quot;738&quot; height=&quot;391&quot; loading=&quot;lazy&quot;&gt;&lt;/figure&gt;
&lt;p&gt;However, if there are several channels in the EDF file, each field is repeated for all channels before moving to the next field:&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/edf-specification/packets/channel_header_several.mmjs.svg&quot; alt=&quot;Byte layout of the channel headers for several channels, field by field&quot; width=&quot;999&quot; height=&quot;194&quot; loading=&quot;lazy&quot;&gt;&lt;/figure&gt;
&lt;p&gt;For instance, if we have three channels, the 768-byte metadata will have the following layout:&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/edf-specification/packets/channel_header_n3.mmjs.svg&quot; alt=&quot;Byte layout of the 768-byte channel headers for three channels&quot; width=&quot;1106&quot; height=&quot;767&quot; loading=&quot;lazy&quot;&gt;&lt;/figure&gt;
&lt;p&gt;We need to note that each channel could have channels with different number of samples per record.&lt;/p&gt;
&lt;h3 id=&quot;sampling-frequency&quot;&gt;Sampling frequency?&lt;/h3&gt;
&lt;p&gt;If you check carefully, the EDF header does not specify the sampling frequency. Thus, how we get it?
For example, for a channel &lt;span class=&quot;math&quot;&gt;\(q\)&lt;/span&gt;, we check the number of samples per record &lt;span class=&quot;math&quot;&gt;\(M_p\)&lt;/span&gt; (10th field in the channel metadata) and the duration of each record per second &lt;span class=&quot;math&quot;&gt;\(T_r\)&lt;/span&gt; (bytes &lt;code&gt;244-251&lt;/code&gt; in the general metadata). Then, the sampling frequency, in Hertz, for the channel &lt;span class=&quot;math&quot;&gt;\(p\)&lt;/span&gt; will be:&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[f^{(p)}_s = \frac{1}{M_p T_r}\]&lt;/div&gt;
&lt;h2 id=&quot;records&quot;&gt;Records&lt;/h2&gt;
&lt;p&gt;Records, or &lt;strong&gt;batches&lt;/strong&gt; as I prefer to think, came after the channel-level metadata. And, they are also structured as the channel-level metadata: the data from one channel after the data from another channel, and so on.&lt;/p&gt;
&lt;p&gt;EDF files organized the data in records, or &lt;strong&gt;batches&lt;/strong&gt; (as I prefer to think about them). The records are saved consecutively:&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/edf-specification/packets/records.mmjs.svg&quot; alt=&quot;Diagram of consecutive data records in an EDF file&quot; width=&quot;566&quot; height=&quot;50&quot; loading=&quot;lazy&quot;&gt;&lt;/figure&gt;
&lt;p&gt;That means that if we have &lt;span class=&quot;math&quot;&gt;\(N_{ch}\)&lt;/span&gt; channels, the first record (record 0) will start at byte &lt;span class=&quot;math&quot;&gt;\(256\cdot(1+N_{ch})\)&lt;/span&gt;, just after the general metadata (first 256 bytes) and the channels’ metadata (next &lt;span class=&quot;math&quot;&gt;\(256\cdot N_{ch}\)&lt;/span&gt; bytes).&lt;/p&gt;
&lt;p&gt;Each record &lt;strong&gt;has the same size&lt;/strong&gt;, and it is organized in the following way, the data from one channel followed by the channel of the next one:&lt;/p&gt;
&lt;figure class=&quot;figure&quot;&gt;
  &lt;img src=&quot;/blog/edf-specification/packets/records_single.mmjs.svg&quot; alt=&quot;Layout of one data record: the samples of each channel in turn&quot; width=&quot;530&quot; height=&quot;146&quot; loading=&quot;lazy&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Note that each channel can have a different number of bytes assigned. This number (&lt;span class=&quot;math&quot;&gt;\(M_p\)&lt;/span&gt; for a channel &lt;span class=&quot;math&quot;&gt;\(p\)&lt;/span&gt;) is defined in the “samples per record” field in the &lt;a href=&quot;#channel-level-metadata&quot;&gt;channels’ metadata&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Thus, as we expected, to get the size of the record, we only need to sum all the sizes for each channel: &lt;span class=&quot;math&quot;&gt;\(N_{rec} = \sum_{p=0}^{N_{ch}-1} M_p\)&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;We need to emphasize that all records have the same size, and due to that, it’s possible to move to any record &lt;span class=&quot;math&quot;&gt;\(q\)&lt;/span&gt;, because it will be start at the byte &lt;span class=&quot;math&quot;&gt;\(B_{(q)}\)&lt;/span&gt; and it will end exactly at byte &lt;span class=&quot;math&quot;&gt;\(B_{(q+1)}-1\)&lt;/span&gt;:&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[B_{(q)} = 256\cdot(1+N_{ch}) + q\cdot N_{rec}\]&lt;/div&gt;
&lt;p&gt;Why I stressing the position locations? Because, if you only need a portion of the data, &lt;code&gt;fseek&lt;/code&gt; will be your friend (in C-based languages, of course).&lt;/p&gt;
&lt;h2 id=&quot;annotations&quot;&gt;Annotations&lt;/h2&gt;
&lt;p&gt;If you are familiar with biomedical data formats, you have seen that EDF files often contain annotations. But, until now, we only saw records and channels, and nothing else so far.
The &lt;a href=&quot;https://pubmed.ncbi.nlm.nih.gov/12948806&quot;&gt;EDF+2003&lt;/a&gt; standard define that the &lt;strong&gt;annotations should be stored in the last channel&lt;/strong&gt; which should have the case sensitive label &lt;code&gt;EDF Annotations&lt;/code&gt;. This implies that the values for this channel in each record are ascii strings and not numbers. So, a parser should be aware of that.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Innocent P-hacking</title>
    <link href="https://pintoorellana.org/blog/innocent-p-hacking/" rel="alternate" type="text/html"/>
    <id>https://pintoorellana.org/blog/innocent-p-hacking/</id>
    <published>2023-02-11T00:00:00Z</published>
    <updated>2023-02-11T00:00:00Z</updated>
    <summary>Why p-values estimated with too few simulations can lead to contradictory conclusions.</summary>
    <content type="html" xml:base="https://pintoorellana.org/blog/innocent-p-hacking/">&lt;p&gt;As a researcher,  reading and interpreting academic papers has become unavoidable in my daily routine. Lately, perhaps due to a &lt;a href=&quot;https://en.wikipedia.org/wiki/Frequency_illusion&quot;&gt;Baader-Meinhof phenomenon&lt;/a&gt;, I have come across several papers exhibiting what I’d call non-malicious p-hacking, often stemming from an insufficient number of simulations.&lt;/p&gt;
&lt;h2 id=&quot;about-p-values&quot;&gt;About p-values&lt;/h2&gt;
&lt;p&gt;If you are not familiar with uncertainty estimation or p-values, allow me to provide a brief, albeit not entirely accurate, intuition: a p-value represents the probability that a measured (or observed) value is not merely a result of error or chance. Consider the following scenario: you flip a recently forged coin 20 times and get 14 heads (a 70% rate). How do you determine whether this coin is fair or not?&lt;/p&gt;
&lt;p&gt;Let’s use a proof by contradiction. Imagine we have a theoretical-ideal fair coin and observe the values using it as a “reference” or null model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;[1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1]
[1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1]
(repeating this process a million times)
[1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this simulated world where a fair coin is flipped, how many times do we observe a rate equal to or higher than 70%? 57839 times, 5.87%, or 0.0587 (the exact value may vary a bit if you run the simulation yourself). &lt;a href=&quot;https://en.wikipedia.org/wiki/Statistical_significance#History&quot;&gt;By tradition, we use the values of 0.05&lt;/a&gt; (or 0.01 sometimes) as a cutoff to determine whether something is “significant” or not.&lt;/p&gt;
&lt;p&gt;In summary, in a simulated world where a fair coin is flipped, we still see 5.78% with 14 or more heads. Therefore, we cannot claim that what we observed is exceptionally unusual, and we cannot prove the coin is unfair (or, using a more statistical phrasing, we do not have enough statistical evidence to reject the possibility that the coin is fair).&lt;/p&gt;
&lt;p&gt;Note that in this peculiar scenario, we know the experiment can be modeled using a binomial distribution. Therefore, we could be tempted to think there is a method to calculate the p-value analytically or in a closed form. However, while the variance of our &lt;a href=&quot;https://stats.stackexchange.com/a/29644&quot;&gt;estimate can be reasonably calculated&lt;/a&gt;, the p-value and the confidence intervals are &lt;a href=&quot;https://en.wikipedia.org/wiki/Binomial_distribution#Confidence_intervals&quot;&gt;particularly challenging to obtain&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-p-hacking-issue&quot;&gt;The P-hacking issue&lt;/h2&gt;
&lt;p&gt;P-hacking is a pejorative term to highlight experiments that manipulate methods to achieve p-values below 0.05, passing them off as “statistically significant.” In our example, 14 out of 20 heads are not highly uncommon for a fair coin; it’s just a fortunate situation. However, the probability of seeing 14 or more heads is dangerously close to 0.05.&lt;/p&gt;
&lt;p&gt;The interesting aspect here is that the “p-value” measured through simulations depends on the number of simulations conducted. We may get inconsistent results with fewer simulations, leading to incorrect reporting.&lt;/p&gt;
&lt;p&gt;Thus, let’s make a meta-simulation of the simulations with different simulation sizes (assuming that creating a large number of simulations is computationally challenging):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;With 10 simulations per scenario, 4506 out of 10000 (45.06%) agree that the coin could be fair. 54.94% will suggest that the coin was rigged!&lt;/li&gt;
&lt;li&gt;With 100 simulations per scenario, 6914 out of 10000 (69.14%) agree with our simulations. 30.86% will suggest that the coin was unfair! Almost one-third of the cases!&lt;/li&gt;
&lt;li&gt;With 1000 simulations per scenario, 8731 out of 10000 (87.31%) agree that the coin is fair.&lt;/li&gt;
&lt;li&gt;With 10000 simulations per scenario, 9998 out of 10000 (99.98%) agree that the coin is fair.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This illustrates a common issue with simulations. Even in simple experiments like coin flipping, estimating p-values through simulations should be cautiously approached, primarily when used as a fixed threshold, because it could lead to diverse conclusions.&lt;/p&gt;
&lt;p&gt;For real-world examples of this mistake in academic papers, I recommend Googling these:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EEG p-value &amp;quot;100 surrogate&amp;quot; &amp;quot;significance&amp;quot; site:https://www.ncbi.nlm.nih.gov
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;EEG p-value &amp;quot;500 surrogate&amp;quot; &amp;quot;significance&amp;quot; site:https://www.ncbi.nlm.nih.gov
&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>x86-assembling, cross-compilation and C</title>
    <link href="https://pintoorellana.org/blog/cross-compilation/" rel="alternate" type="text/html"/>
    <id>https://pintoorellana.org/blog/cross-compilation/</id>
    <published>2020-11-23T00:00:00Z</published>
    <updated>2020-11-23T00:00:00Z</updated>
    <summary>Building tiny cross-platform binaries for numerical C code with SmallerC and YASM.</summary>
    <content type="html" xml:base="https://pintoorellana.org/blog/cross-compilation/">&lt;p&gt;Working in research often means moving code between my laptop and various workstations or servers. This constant migration comes with a familiar headache: managing dependencies and permissions. While I could use Docker containers or write code in Go, I started wondering - could I achieve this using plain C with minimal OS interaction?&lt;/p&gt;
&lt;p&gt;I’m specifically interested in raw numerical simulations like Metropolis-Hastings samplers - algorithms that essentially just work with vectors of numbers. These are perfect candidates for stripped-down implementations that should, theoretically, run anywhere.
The ideal scenario in my mind is straightforward: compile my C code, upload the binary to a server, and run it with a simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chmod +x simulation
./simulation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While cross-compilation from Linux to Windows is possible with GCC, macOS binaries remain a challenge. This got me thinking about the minimal requirements I actually need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build dependencies under 10MB - no VMs or huge toolchains like MinGW&lt;/li&gt;
&lt;li&gt;Cross-platform binaries that might sacrifice some optimization for portability&lt;/li&gt;
&lt;li&gt;Simple compilation process for Windows/Linux deployment&lt;/li&gt;
&lt;li&gt;Basic debugging capabilities (GCC/GDB would suffice)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-smallerc--yasm-approach&quot;&gt;The SmallerC + YASM approach&lt;/h2&gt;
&lt;p&gt;SmallerC is a stripped-down C compiler that generates 32-bit instructions. It’s not a complete C90 implementation, but it covers enough ground for numerical computing. What makes it particularly useful is its ability to work with various assemblers - this is where YASM comes in, providing the cross-platform compilation capability we need.&lt;/p&gt;
&lt;p&gt;After some exploration, I found an interesting solution: using a cross-compiler assembler. Specifically, the combination of SmallerC and YASM caught my attention.&lt;/p&gt;
&lt;p&gt;Here’s how to set it up &lt;code&gt;SmallerC&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Clone the GitHub repository SmallerC, or download the &lt;a href=&quot;https://github.com/alexfru/SmallerC/archive/refs/heads/master.zip&quot;&gt;ZIP source code&lt;/a&gt;. I’ll do it &lt;code&gt;~/.smlrcc&lt;/code&gt;, but you can use your favorite directory.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;In order to create binary files in Windows, the files must be combined. Some of the compiler’s low-level instructions can cause anti-malware software to report false positives.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;If you don’t trust the code, you can always read, check, and compile it yourself (just type &lt;code&gt;./configure &amp;amp;&amp;amp; make&lt;/code&gt; in the root directory and that’s all).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Set the environment variable &lt;code&gt;SMLRC&lt;/code&gt;. In Linux/macOS, you can do it by adding this at the end of &lt;code&gt;~/.profile&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export SMLRC=~/.smlrcc/v0100
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;There are three folders with the binaries &lt;code&gt;~/.smlrcc/v0100/binw&lt;/code&gt;, &lt;code&gt;~/.smlrcc/v0100/binl&lt;/code&gt; or &lt;code&gt;~/.smlrcc/v0100/binm&lt;/code&gt; in your system &lt;code&gt;PATH&lt;/code&gt;. For instance, in macOS you can do it by adding this in &lt;code&gt;~/.profile&lt;/code&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export PATH=&amp;quot;$PATH:~/.smlrcc/v0100/bin&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, we can install &lt;code&gt;YASM&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows users can &lt;a href=&quot;http://www.tortall.net/projects/yasm/releases/yasm-1.3.0-win64.exe&quot;&gt;download it&lt;/a&gt; directly or use MSYS2 (my preferred approach)&lt;/li&gt;
&lt;li&gt;On Debian/Ubuntu: &lt;code&gt;sudo apt install yasm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;On macOS with &lt;a href=&quot;https://www.macports.org/install.php&quot;&gt;MacPorts&lt;/a&gt;: &lt;code&gt;sudo port install yasm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;our-first-cross-hello-world&quot;&gt;Our first Cross-Hello World!&lt;/h2&gt;
&lt;p&gt;That’s all. Let’s test it with a simple Hello World &lt;code&gt;sample.c&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;

int main(int argc, char** argv){
  printf(&amp;quot;Hello world!&amp;quot;);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can generate binaries for each platform:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Windows binary
smlrcc sample.c -win -o sample-win.exe

# Linux binary
smlrcc sample.c -Linux -o sample-lin

# macOS binary
smlrcc sample.c -macos -o sample-mac
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The size differences compared to GCC (&lt;code&gt;gcc -O2 -o GCC-sample-default sample.c&lt;/code&gt;) build are interesting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ ls -Alh | awk &apos;{print $5, $9}&apos;
357K GCC-sample-default
20K sample-lin
17K sample-mac
26K sample-win.exe
99 sample.c
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;additional-thoughts&quot;&gt;Additional thoughts&lt;/h2&gt;
&lt;p&gt;After working with this setup, I’ve found both advantages and limitations worth considering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The good parts:
&lt;ul&gt;
&lt;li&gt;The project has 6 years of development behind it, showing reasonable maturity&lt;/li&gt;
&lt;li&gt;Cross-platform compilation feels surprisingly straightforward&lt;/li&gt;
&lt;li&gt;Binaries are compact with minimal dependencies&lt;/li&gt;
&lt;li&gt;The codebase is well-written and comprehensible, making it fixable if needed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The challenges:
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;It only supports a subset of C90. &lt;a href=&quot;https://github.com/alexfru/SmallerC/blob/master/v0100/doc/smlrc.md#what-features-are-not-supported-by-smaller-c&quot;&gt;More details here&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;No integrated debugger (though GCC/GDB can fill this gap)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Not quite production-ready for commercial applications&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;May require code adjustments based on specific use cases &lt;a href=&quot;https://bestofcpp.com/repo/alexfru-SmallerC-cpp-compiler#issues&quot;&gt;as every user can have different experiences&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For my research simulations, this approach has proven valuable. It’s not a universal solution, but for numerical computations that need to run across different platforms with minimal setup, it’s a practical alternative to heavier toolchains.&lt;/p&gt;
&lt;p&gt;If you want to explore this setup yourself, here are the key resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/alexfru/SmallerC&quot;&gt;SmallerC Compiler&lt;/a&gt;:
&lt;ul&gt;
&lt;li&gt;Binaries: &lt;a href=&quot;https://github.com/alexfru/SmallerC/tree/master/v0100/binw&quot;&gt;Windows &lt;/a&gt;, &lt;a href=&quot;https://github.com/alexfru/SmallerC/tree/master/v0100/binl&quot;&gt;Linux&lt;/a&gt;, &lt;a href=&quot;https://github.com/alexfru/SmallerC/tree/master/v0100/binm&quot;&gt;MacOS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/alexfru/SmallerC/blob/master/v0100/doc/smlrcc.md&quot;&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yasm.tortall.net/&quot;&gt;YASM: Yasm Modular Assembler&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Smoothing, exponential averages and digital filters</title>
    <link href="https://pintoorellana.org/blog/smoothing-and-filtering/" rel="alternate" type="text/html"/>
    <id>https://pintoorellana.org/blog/smoothing-and-filtering/</id>
    <published>2020-11-23T00:00:00Z</published>
    <updated>2020-11-23T00:00:00Z</updated>
    <summary>Exponential smoothing is a low-pass filter in disguise.</summary>
    <content type="html" xml:base="https://pintoorellana.org/blog/smoothing-and-filtering/">&lt;p&gt;I always found the wide application of digital filters fascinating. However, I think that their versatility is not always intuitive.&lt;/p&gt;
&lt;p&gt;Here, I want to discuss the connection between filters and a commonly used smoothing algorithm.&lt;/p&gt;
&lt;p&gt;Let’s consider a stream of data denoted as &lt;span class=&quot;math&quot;&gt;\(x\)&lt;/span&gt;, which is extremely noisy. We seek a method to obtain a smoothed version with fewer spikes. A common technique for this purpose is the exponential average. In this method, we assume that the current value &lt;span class=&quot;math&quot;&gt;\(x(n)\)&lt;/span&gt; is too noisy, and we intend to use the streaming history to adjust it:&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[y(n)=\alpha x(n)+(1-\alpha)y(n-1)\]&lt;/div&gt;
&lt;p&gt;Think of it this way: if we observe a value of 10, it doesn’t make much sense if all previous values were between 0 and 1. Perhaps a new event occurred, but it is more logical to assume that the increment wasn’t too large and it’s an error in the stream. The forgetting factor &lt;span class=&quot;math&quot;&gt;\(\alpha\)&lt;/span&gt; controls this change; the closer the value to 1, the less we rely on previous values of the system.&lt;/p&gt;
&lt;p&gt;Now that we understand the logic behind this technique, let’s delve into a discussion. What does this smoothing algorithm have in common with a frequency filter? At least here, our goal is to clean the data, not select a frequency band.&lt;/p&gt;
&lt;p&gt;Interestingly, our minds tend to assume that a clean signal implies a signal that is not changing drastically too often. I want to emphasize the idea of “not changing faster”; this means we associate low-band signals with “smooth signals.”&lt;/p&gt;
&lt;p&gt;If the linguistic connection wasn’t clear enough, we can derive the spectrum of &lt;span class=&quot;math&quot;&gt;\(y\)&lt;/span&gt;, by obtaining the discrete Fourier transform of &lt;span class=&quot;math&quot;&gt;\(y\)&lt;/span&gt;:&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[Y(\omega)=\alpha X(z)+(1-\alpha)e^{j\omega}Y\left(\omega\right)\]&lt;/div&gt;
&lt;p&gt;Rearranging the items,&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[Y(\omega)=\frac{\alpha}{1-\alpha e^{j\omega}}X(\omega)=G(\omega)X(\omega)\]&lt;/div&gt;
&lt;p&gt;where &lt;span class=&quot;math&quot;&gt;\(G\)&lt;/span&gt; is the filter gain&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[\left\Vert G(e^{j\omega})\right\Vert =\left\Vert \frac{\alpha}{1-\alpha e^{j\omega}}\right\Vert\]&lt;/div&gt;
&lt;p&gt;To easily check what kind of filter is G, let’s examine its behavior as &lt;span class=&quot;math&quot;&gt;\(z\)&lt;/span&gt; approaches different values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Closer to 0Hz:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;math&quot;&gt;\[\left\Vert G(\omega\rightarrow0)\right\Vert =\left\Vert G(z\rightarrow0)\right\Vert =\frac{\alpha}{1-\alpha}\]&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;And closer to the Nyquist frequency (the maximum frequency that we can represent in the stream):&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;math&quot;&gt;\[\left\Vert G(\omega\rightarrow\pi)\right\Vert =\frac{\alpha}{1+\alpha}\]&lt;/div&gt;
&lt;p&gt;Now, let’s unravel the implications when &lt;span class=&quot;math&quot;&gt;\(\alpha\)&lt;/span&gt; takes specific values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Minimal smoothing: &lt;span class=&quot;math&quot;&gt;\(\alpha=0.9\)&lt;/span&gt; (&lt;span class=&quot;math&quot;&gt;\(x\)&lt;/span&gt; is almost copying &lt;span class=&quot;math&quot;&gt;\(y\)&lt;/span&gt;). In this case, the filter gain goes from 9 (0 Hz) to 0.81 (Nyquist’s frequency), that is, a 9.09% decrease. A minimal decrease for a minimal smoothing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Strong smoothing: &lt;span class=&quot;math&quot;&gt;\(\alpha=0.1\)&lt;/span&gt;. In this case, we trust less in the current stream values &lt;span class=&quot;math&quot;&gt;\(x\)&lt;/span&gt; so, probably, we will tend to over-smooth it. In the spectrum, that means that gain changes from 0.111 at 0Hz plumbetting to 0.091 at the max. frequency, in other words, a significant 81.81% decrease.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, we can confirm that there an interplay between concepts: smoothing implicates low-pass filtering, even if not consciously perceived in that way.&lt;/p&gt;
&lt;p&gt;But, as a consequence, an intriguing question emerges: while “low-pass” is inherently associated with “smoothness” and “high-pass” with “noise”, does a subtle conceptual association extend to “band-pass” processes? At which point, we stop thinking that something is “noisy” or “smooth”?&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Spectrum and eigendecomposition</title>
    <link href="https://pintoorellana.org/blog/spectrum-eigenvalues-covariance/" rel="alternate" type="text/html"/>
    <id>https://pintoorellana.org/blog/spectrum-eigenvalues-covariance/</id>
    <published>2020-11-23T00:00:00Z</published>
    <updated>2020-11-23T00:00:00Z</updated>
    <summary>How the spectrum of a time series relates to the eigenvalues of its covariance matrix.</summary>
    <content type="html" xml:base="https://pintoorellana.org/blog/spectrum-eigenvalues-covariance/">&lt;p&gt;In the vault of things that I get amused every time I remember is the connection between the time series’ spectrum and matrices’ spectrum.&lt;/p&gt;
&lt;p&gt;Having been trained as an engineer during my undergraduate years, I instinctively associate a signal’s spectrum with its oscillations. It’s the spectrum that unveils the predominant oscillatory components within the signal under analysis. But as a graduate student learning the dark arts of &lt;del&gt;magic&lt;/del&gt; statistics, I’ve often encountered colleagues whose notion of spectrum was completely different from mine: for them, spectrum represents the sum of eigenvalues of the signal’s covariance matrix (as a consequence of the Wiener–Khinchin–Einstein theorem.)&lt;/p&gt;
&lt;p&gt;The discussion I show here might seem too detailed for a book yet too basic for a academic paper. Nonetheless, I find it to be a stimulating mental exercise in linking seemingly distant concepts.&lt;/p&gt;
&lt;p&gt;Let us start with spectral definitions in linear algebra: the spectrum of a square matrix is the set of its eigenvalues. (I think a different post is needed to discuss the proper natural interpretation of eigenvectors and eigenvalues.)&lt;/p&gt;
&lt;p&gt;In the classical definition of a time series spectrum (or power spectrum density, PSD), the spectrum is the Fourier transform of the covariance function:&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[X\left(\omega\right)
=\left|\mathcal{F}
\left\{
\sigma_{xx}\left(\tau\right)
\right\} \left(\omega\right)
\right|^{2}\]&lt;/div&gt;
&lt;p&gt;However, consider a scenario where we only have &lt;span class=&quot;math&quot;&gt;\(T\)&lt;/span&gt; points sampled from a weakly stationary time series. Then, the spectrum could be defined as&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[X\left(\omega\right)=\sum_{t=0}^{T-1}\sigma_{xx}\left(\tau\right)e^{-j\omega t}\]&lt;/div&gt;
&lt;p&gt;Then, we can construct a covariance matrix &lt;span class=&quot;math&quot;&gt;\(\Sigma_{xx}\)&lt;/span&gt; as&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[\Sigma_{xx}=\left(\begin{array}{cccc}
\sigma_{xx}\left(0\right) &amp;amp; \sigma_{xx}\left(1\right) &amp;amp; \cdots &amp;amp; \sigma_{xx}\left(T-1\right)\\
\sigma_{xx}\left(-1\right) &amp;amp; \sigma_{xx}\left(0\right) &amp;amp; \cdots &amp;amp; \sigma_{xx}\left(T-2\right)\\
\vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots\\
\sigma_{xx}\left(-T+1\right) &amp;amp; \sigma_{xx}\left(-T+2\right) &amp;amp; \cdots &amp;amp; \sigma_{xx}\left(0\right)
\end{array}\right)\]&lt;/div&gt;
&lt;p&gt;The nice trick here is to remember that due to weak stationarity, the correlation at &lt;span class=&quot;math&quot;&gt;\(-\tau\)&lt;/span&gt;, &lt;span class=&quot;math&quot;&gt;\(\sigma_{xx}\left(-\tau\right)\)&lt;/span&gt;, will be the same at &lt;span class=&quot;math&quot;&gt;\(\tau\)&lt;/span&gt;, &lt;span class=&quot;math&quot;&gt;\(\sigma_{xx}\left(\tau\right)\)&lt;/span&gt;. This symmetric property makes &lt;span class=&quot;math&quot;&gt;\(\Sigma_{xx}\)&lt;/span&gt; to be a circulant matrix.&lt;/p&gt;
&lt;p&gt;Then, let’s proceed with an eigendecomposition of &lt;span class=&quot;math&quot;&gt;\(\Sigma_{xx}\)&lt;/span&gt;, where &lt;span class=&quot;math&quot;&gt;\(Y\)&lt;/span&gt; will be the eigenvectors and &lt;span class=&quot;math&quot;&gt;\(\gamma\)&lt;/span&gt; the eigenvalues:&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[\Sigma_{xx}Y=\gamma Y\]&lt;/div&gt;
&lt;p&gt;While this expression may seem complex, focusing on the &lt;span class=&quot;math&quot;&gt;\(k\)&lt;/span&gt;-th row of &lt;span class=&quot;math&quot;&gt;\(\Sigma_{xx}\)&lt;/span&gt; simplifies matters:&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[\gamma_{k}Y_{k}=\sum_{\tau=0}^{T-1}\sigma_{xx}\left(\tau-k\right)Y_{\tau}=\sum_{\tau=-k}^{T-1-\tau}\sigma_{xx}\left(\tau\right)Y_{\tau+k}\]&lt;/div&gt;
&lt;p&gt;Recall that &lt;span class=&quot;math&quot;&gt;\(k\)&lt;/span&gt; represents the lag index and it can go from &lt;span class=&quot;math&quot;&gt;\(0\)&lt;/span&gt; to &lt;span class=&quot;math&quot;&gt;\(T-1\)&lt;/span&gt;: &lt;span class=&quot;math&quot;&gt;\(\left(0\le k\le T-1\right)\)&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Now, a last trick: assume that &lt;span class=&quot;math&quot;&gt;\(Y_{k}\)&lt;/span&gt; has some periodicity. That means that the values &lt;span class=&quot;math&quot;&gt;\(Y_{k}\)&lt;/span&gt; would start repeating after some period. Even if we do not think that condition is true, the period can be made as large as possible so that, in practice, no periodic values would be observed.&lt;/p&gt;
&lt;p&gt;Considering the periodicity assumption for &lt;span class=&quot;math&quot;&gt;\(Y_{k}\)&lt;/span&gt;, let’s say &lt;span class=&quot;math&quot;&gt;\(Y_{k}=e^{-j2\pi\frac{k}{T}\tau}\)&lt;/span&gt; for some &lt;span class=&quot;math&quot;&gt;\(\tau\)&lt;/span&gt;. This allows us to define the &lt;span class=&quot;math&quot;&gt;\(k\)&lt;/span&gt;-th eigenvalue as&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[\gamma_{k}e^{-j2\pi\frac{k^{2}}{T}}=\sum_{\tau=0}^{T-1}\sigma_{xx}\left(\tau\right)e^{-j2\pi\frac{k}{T}\left(\tau+k\right)}\]&lt;/div&gt;
&lt;p&gt;Simplifying further,&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[\gamma_{k}=\sum_{\tau=0}^{T-1}\sigma_{xx}\left(\tau\right)e^{-j2\pi\frac{k}{T}\tau}\]&lt;/div&gt;
&lt;p&gt;Now, replacing &lt;span class=&quot;math&quot;&gt;\(\omega=2\pi\frac{k}{T}\)&lt;/span&gt;, we notice striking similarities with the PSD equation:&lt;/p&gt;
&lt;div class=&quot;math&quot;&gt;\[X\left(\omega\right)=\sum_{t=0}^{T-1}\sigma_{xx}\left(\tau\right)e^{-j\omega t}\]&lt;/div&gt;
&lt;p&gt;This expression establishes a deeper connection- the PSD is also the set of eigenvalues of the Fourier transform of the covariance matrix.&lt;/p&gt;
&lt;p&gt;In essence, the matrix’s spectrum and a time series’ spectrum share a special link that was unclear to me for a long time. And I would try to explain why it is still amusing to me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;In my mind, I picture a matrix as a fixed object, as something that holds information in rows and columns. This object has nice descriptive elements: the eigenvalues/eigenvectors. We can calculate trace and determinants with them. And we can check if systems will be stable using them too. Yet, a matrix, per se, remains unchanged unless it is decided to be time-varying.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;On the other hand, we have a time series, a perpetually fluctuating entity, ever-changing from one moment to the next. And a crucial information of this time-living object is the spectrum.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In this time series, a matrix emerges when you calculate the average correlation between each point in the signal with any other past value and organize this information into rows and columns. Again, this matrix is a fixed, non-time-varying entity. But, the spectrum of this newly-constructed matrix mirrors the spectrum of the original time series.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
</feed>
