Categories
SymPy

Quick Transfer Functions with SymPy

SymPy allows quick and (relatively 😜) easy manipulation of algebraic expressions. But it can do so much more! Using the sympy.physics.control toolbox, you can very inspect linear time-invariant systems in a very comfortable way.

I was looking at a paper (Janiszowski, 1993) which features three transfer functions of different properties and wanted to look at their pole-zero diagrams as well as their Bode plots in real frequency. The transfer functions where the following:

(1)    \begin{align*} G_1(s) &= \frac{ 2 + 42s }{ (1 + 2s)(1 + 40s) } \\ G_2(s) &= \frac{ 5 - 60s }{ (1 + 4s)(1 + 40s) } \\ G_3(s) &= \frac{ 4(1 + s) }{ 1 + 4s + 8s^2 } \end{align*}

Drawing the pole-zero diagrams from this representation is easy:

  • Zeros: Set numerator to zero
  • Poles: Set denominator (or its individual terms) to zero

Drawing the Bode diagram however would’ve involved some basic programming. But neither is necessary.

import sympy as sy
from sympy.physics.control.lti import TransferFunction as TF
from sympy.physics.control import bode_plot, pole_zero_plot

s = sy.symbols("s", complex=True)

G1 = (2 + 42*s)/((1 + 2*s)*(1 + 40*s))
tf = TF(*G.as_numer_denom(), s)
bode_plot(tf)
pole_zero_plot(tf)
Bode plot of the transfer function G1(s)
Pole-Zero diagram of the transfer function G1(s)

Well, that was easy. TransferFunction (abbreviated with TF in my examples) requires numerator and denominator to be passed as separate arguments. sympy_expr.as_numer_denom() is convenient, as it returns a (numerator, denominator) tuple. Using the asterisk expands this.

Now, what else can we do with this? Look at RLC resonators, for example:

A parallel resonator of resistance R, inductance L and capacitance C

The reactance of the inductor and capacitor are given by

(2)    \begin{equation*} X_C = \frac{1}{sC} \quad \text{and} \quad X_L = sL \end{equation*}

where s is basically the complex frequency (we’re looking at this with Laplace eyes). Now, off to SymPy we go:

R, C, L = sy.symbols("R, C, L", real=True, positive=True)
Xc, Xl = 1/(s*C), s*L

def par(a, b):
    # Shorthand for a parallel circuit
    return a*b/(a+b)

G = par(Xc, par(R, Xl)).ratsimp()
G = G.subs({R: 1e3, L: 1e-6, C: 1e-6})
tf = TF(*G.as_numer_denom(), s)
bode_plot(tf, 5, 7)
pole_zero_plot(tf)

For visualisation we choose R = 1 kΩ, L = 1 µH and C = 1 µF.

Bode plot of the parallel RLC circuit with R = 1 kΩ, L = 1 µH and C = 1 µF, and hence a resonance frequency of 1 MHz
And its pole-zero plot just for the lolz.

Why is this useful? Well, because it is quick and robust and once you have your transfer function typed out, you get the impulse and step responses for free:

import sympy as sy
from sympy.physics.control import impulse_response_plot
from sympy.physics.control import step_response_plot

s = sy.symbols("s", complex=True)
G1 = (2+42*s)/((1+2*s)*(1+40*s))
G2 = (5-60*s)/((1+4*s)*(1+40*s))
G3 = 4*(1+s)/(1+4*s+8*s**2)

for G in [G1, G2, G3]:
    tf = TF(*G.as_numer_denom(), s)
    bode_plot(tf)
    pole_zero_plot(tf)
    step_response_plot(tf, upper_limit=60)
    impulse_response_plot(tf, upper_limit=60)

Note: SymPy’s adaptive plotting is not particularly good at plotting oscillations, so the impulse response of the RLC circuit above will look ugly.

Categories
Linux Uncategorized

Lost Gitea Local Admin Password?

There are various possible ways you can lose access to your Gitea instance. In particular, if you are getting user accounts via LDAP, you should always have a key to get back in: a “local” admin.

In case you haven’t got one (totally not like I did definitely only once 🤫), deleted it or lost its password, here’s the procedure in to variants: running on plain Linux, and running within Docker.

Plain Linux

List the available (local) user accounts (you may need to run this with sudo):

$ gitea admin user list

ID   Username     Email             IsActive IsAdmin
1    user1        me@home.earth     true     false

You may not get a single user listed here. In order to add a local administrator, do this

$ sudo gitea admin user create --username local_admin --email admins@email.earth --admin --random-password

generated random password is 'IzgvOwv1M9EG'
New user 'local_admin' has been successfully created!

$ sudo gitea admin user list

ID   Username     Email                 IsActive IsAdmin
1    unpriv_local bowfingermail@gmx.net true     true
3    local_admin  admins@email.earth    true     true

Especially on shared machines, I’d strongly recommend not setting a password via the command line. It’ll stick in bash_history and may be visible in the process list. Hence the --random-password option here. Use the generated password upon first login in the browser.

Docker Container

In order to get to the “plain Linux” field, you simply run a bash in the Gitea Docker container.

What’s the container’s name? Get it via docker container ls -a. Let’s say it’s called gitea. To start bash in that container, do

user1@linux:$ docker exec -it gitea /bin/bash

Now, you basically do the same thing as in the section above.

docker # gitea admin user list

ID   Username     Email             IsActive IsAdmin
1    user1        me@home.earth     true     false

docker # gitea admin user create --username local_admin --email admins@email.earth --admin --random-password

generated random password is 'ikV6xzPTiH7B'
New user 'local_admin' has been successfully created!

docker # gitea admin user list

ID   Username     Email                 IsActive IsAdmin
1    unpriv_local bowfingermail@gmx.net true     true
3    local_admin  admins@email.earth    true     true

Conclusion (and Other Platforms)

The Plain Linux approach works equally well on other platforms. I hope this helps.

Categories
Uncategorized

Numbers games: Z-Transform and some sympy

Back in uni me and control systems never really clicked. So I took a dive into the topic as I had the time. Long story short for all who know equally little or even less about the maths of control systems as I do:

  • Often, you model control systems using the Laplace transform. That’s nice and good for continuous systems.
  • Once you’re going to the discrete (digital) world, you want to use the Z-transform.
  • There is a (complicated) relation between Laplace domain and Z domain.
  • There are approximations, using a complex maps to map from Laplace to Z domain (a bilinear transformation, Tustin’s method, being the one used here).

This is a post on serendipity, because what this is about is actually neither control systems, Z-, or Laplace transforms but a numbers game. Bare with me.

Many systems can be modelled using a rational function of the form

 G(s) = \frac{\sum\limits_{i=0}^m \beta_i s^i}{\sum\limits_{i=0}^n \alpha_i s^i} \qquad\text{\alpha_0 = 1}

You can easily put this into sympy and play around with it.

import sympy as sy

s, z, Ts = sy.symbols("s, z, T_s")

a0 = sy.S.One
a1, a2 = sy.symbols("alpha_1, alpha_2")
b0, b1, b2 = sy.symbols("beta_0, beta_1, beta_2")

G = (b0 * s**2 + b1 * s + b2) / (a0 * s**2 + a1 * s + a2)
G # use `print(sy.latex(G))` to get the LaTeX representation

 \frac{\beta_{0} s^{2} + \beta_{1} s + \beta_{2}}{\alpha_{1} s + \alpha_{2} + s^{2}}

Nice. Let’s generalise this a little bit:

import sympy as sy

s, z, Ts = sy.symbols("s, z, T_s")

N, M = 2, 2

a = sy.symbols(f"alpha_1:{N+1}")
a = (sy.S.One, *a)
b = sy.symbols(f"beta_0:{M+1}")

G = sy.Add(*[b[i] * s**i for i in range(M+1)]) / sy.Add(*[a[i] * s**i for i in range(N+1)])

By playing with the values for N and M we can now generate any order of rational function we wish. Again, not the thing this article is about.

Then there is the bilinear transform called Tustin’s method that approximately maps the function G from the Laplace domain into the Z domain.

 s \Rightarrow \frac{2}{T_s} \frac{z-1}{z+1}

Let’s do this in sympy:

tustin = 2/Ts * (z-1)/(z+1)

G.subs({s: tustin}).simplify()

 \frac{T_{s}^{2} \beta_{0} \left(z + 1\right)^{2} + 2 T_{s} \beta_{1} \left(z - 1\right) \left(z + 1\right) + 4 \beta_{2} \left(z - 1\right)^{2}}{T_{s}^{2} \left(z + 1\right)^{2} + 2 T_{s} \alpha_{1} \left(z - 1\right) \left(z + 1\right) + 4 \alpha_{2} \left(z - 1\right)^{2}}

Sweet. Now, I want to have the representation in two polynomials in z (numerator and denominator) to extract the coefficients (because that’s what I wanted to hack into code):

num, den = G.as_numer_denom()
num = sy.Poly(num, z)
den = sy.Poly(den, z)

num.coeffs()
den.coeffs()

This gives you a nice, codeable result:

num: [  T_s**2*beta_0 + 2*T_s*beta_1  + 4*beta_2,
      2*T_s**2*beta_0                 - 8*beta_2,
        T_s**2*beta_0 - 2*T_s*beta_1  + 4*beta_2]
den: [  T_s**2        + 2*T_s*alpha_1 + 4*alpha_2,
      2*T_s**2                        - 8*alpha_2,
        T_s**2        - 2*T_s*alpha_1 + 4*alpha_2]

… but my eye spotted a pattern here. Do you see it? It’s easier to see in matrix representation, and I’ll only look at num (i.e. the expression with βi in it, because I set É‘0 to 1):

 \text{num}^{(N=2)} = \begin{pmatrix}                                 1 & +2 & +4 \\                                 2 & 0 & -8 \\                                 1 & -2 & +4                          \end{pmatrix} \cdot \begin{pmatrix}                                                                    T_s^2 \beta_0 \\                                                                    T_s^1 \beta_1 \\                                                                    T_s^0 \beta_2                                                                \end{pmatrix}

Here is the resulting matrix for N = M = 3 and for N = M = 5:

 \text{num}^{(N=3)} = \begin{pmatrix}                                 1    & 2    & 4    & 8    \\                                 3    & 2    & -4   & -24  \\                                 3    & -2   & -4   & 24   \\                                 1    & -2   & 4    & -8   \\                          \end{pmatrix} \cdot \begin{pmatrix}                                                                    T_s^3 \beta_0 \\                                                                    T_s^2 \beta_1 \\                                                                    T_s^1 \beta_2 \\                                                                    T_s^0 \beta_3                                                                \end{pmatrix} \\  \text{num}^{(N=5)} = \begin{pmatrix}                                 1    & 2    & 4    & 8    & 16   & 32   \\                                 5    & 6    & 4    & -8   & -48  & -160 \\                                 10   & 4    & -8   & -16  & 32   & 320  \\                                 10   & -4   & -8   & 16   & 32   & -320 \\                                 5    & -6   & 4    & 8    & -48  & 160  \\                                 1    & -2   & 4    & -8   & 16   & -32                          \end{pmatrix} \cdot \begin{pmatrix}                                                                    T_s^5 \beta_0 \\                                                                    T_s^4 \beta_1 \\                                                                    T_s^3 \beta_2 \\                                                                    T_s^2 \beta_3 \\                                                                    T_s^1 \beta_4 \\                                                                    T_s^0 \beta_5                                                                \end{pmatrix}

Here are the resulting matrices for N = M = 4:

 \text{num}^{(N=4)} = \begin{pmatrix}                                 1    & 2    & 4    & 8    & 16   \\                                 4    & 4    & 0    & -16  & -64  \\                                 6    & 0    & -8   & 0    & 96   \\                                 4    & -4   & 0    & 16   & -64  \\                                 1    & -2   & 4    & -8   & 16                          \end{pmatrix} \cdot \begin{pmatrix}                                                                    T_s^4 \beta_0 \\                                                                    T_s^3 \beta_1 \\                                                                    T_s^2 \beta_2 \\                                                                    T_s^1 \beta_3 \\                                                                    T_s^0 \beta_4                                                                \end{pmatrix}

Here are the resulting matrices for N = M = 6

 \text{num}^{(N=6)} = \begin{pmatrix}                                 1    & 2    & 4    & 8    & 16   & 32   & 64   \\                                 6    & 8    & 8    & 0    & -32  & -128 & -384 \\                                 15   & 10   & -4   & -24  & -16  & 160  & 960  \\                                 20   & 0    & -16  & 0    & 64   & 0    & -1280 \\                                 15   & -10  & -4   & 24   & -16  & -160 & 960  \\                                 6    & -8   & 8    & 0    & -32  & 128  & -384 \\                                 1    & -2   & 4    & -8   & 16   & -32  & 64                           \end{pmatrix} \cdot \begin{pmatrix}                                                                    T_s^6 \beta_0 \\                                                                    T_s^5 \beta_1 \\                                                                    T_s^4 \beta_2 \\                                                                    T_s^3 \beta_3 \\                                                                    T_s^2 \beta_4 \\                                                                    T_s^1 \beta_5 \\                                                                    T_s^0 \beta_6                                                                \end{pmatrix}

The odd values for N and M produce matrices with all non-zero values, so I’ll look at those first.

What I find stunning is the fact that there are so many patterns and yet I fail to think of a pattern that creates the whole matrix. For example, the first column clearly follows the binomial distribution:

\begin{pmatrix}N \\ k\end{pmatrix}

where k is the row number starting with 0.

The first and last rows of the matrix are power series of 2 and -2 respectively:

 2^l \quad \text{and} \quad (-2)^l

where l is the column number starting at 0.

Every column has a different sign pattern starting at all positive on column 0 and alternating, starting with + on the last column.

Ignoring the signs, the columns are related like this

N#0 ↷ #N-1#1 ↷ #N-2#2 ↷ #N-3#3 ↷ #N-4
3྾ 8྾ 2
5྾ 32྾ 8྾ 2
7྾ 128྾ 32྾ 8྾ 2
What multiples relate the lth column from the left with the lth column from the right, ignoring the sign.

Maybe this follows from the power series mentioned earlier. But looking at the coefficients in each column the binomial pattern is lost as you move to the inside.

And last but not least wild zeros appear when you look at the even values for N and M:

Apparently, in the middle row as well as the middle column, every other value is zero.

All right, I’m absolutely sure mathematicians and control systems theoreticians have looked at this back in the day and this is all solved and done. Still, I found it interesting to look at.

If you want to create those values yourself, you can use this little script, and modify N and M. But beware, at N = 10 the script runs for 10 seconds.

import sympy as sy

s, z, Ts = sy.symbols("s, z, T_s")

N, M = 3, 3  # <-- Modify here

a = sy.symbols(f"alpha_1:{N+1}")
a = (sy.S.One, *a)
b = sy.symbols(f"beta_0:{M+1}")

G = sy.Add(*[b[i] * s**i for i in range(M+1)]) / sy.Add(*[a[i] * s**i for i in range(N+1)])

tustin = 2/Ts * (z-1)/(z+1)
G = G.subs({s: tustin}).simplify()

num, den = G.as_numer_denom()
num = sy.Poly(num, z)
den = sy.Poly(den, z)

for l in num.coeffs():
    p = sy.Poly(l, Ts)
    for i, B in enumerate(p.all_coeffs()):
        try:
            coeff, _ = B.as_two_terms()
        except:
            coeff = 0 if B is sy.S.Zero else 1
        print(f"{repr(coeff):6s} & ", end="")
    print()

Oh, and of course you can change N and M independently from each other, but I could not be bothered to look at this as well.

Categories
Uncategorized

Rogue docker processes on Ubuntu: Snap!

I’ve come across a situation where out of three docker containers that should be running, only one was shown by docker container ls -a. Weird, hu?

The missing two were clearly running their processes because the web service they provided was active and reachable.

After a lot of forth and back, it turned out that Ubuntu 20.04 with snap came with a docker snap – and had a recent Docker CE installed via the apt package manager. Don’t do that.

Once the snap docker was removed, it worked and showed all the containers again.

Categories
Hardware Linux

ath/ath9k/cfg80211 being weird (regdomain and wireless)

Upon upgrading my Kernel on my Ubuntu 20.04 LTS to the HWE stack, I ran into the issue that Wifi stopped working.

Turns out, it only refused to listen/transmit on channels 12 and higher. My router was using channel 13, so no luck.

After some research, I found that you can set the Wifi stack to obey your country’s regulatory requirements. In Germany, channels 12 and 13 are fine to use.

Others had the same problem back in the day, apparently. But the proposed solution did not work or did not work anymore.

In my case, with a Qualcomm Atheros AR93xx using ath9k, editing /etc/default/crda setting REGDOMAIN=DE or trying to use iw reg set DE did not help.

What helped after all was setting ieee80211_regdom=DE when loading the cfg80211 module. So, I somehow doubt that the issue was the ath9k module itself.

Try this:

Create /etc/modprobe.d/cfg80211.conf with content:

options cfg80211 ieee80211_regdom=DE

Obviously, you set it to the 2-digit ISO code of the place you live at.

For Germany, this results in the expected 2.4 GHz band channel list (iw list | grep -A 15 Frequencies:):

* 2412 MHz [1] (20.0 dBm)
* 2417 MHz [2] (20.0 dBm)
* 2422 MHz [3] (20.0 dBm)
* 2427 MHz [4] (20.0 dBm)
* 2432 MHz [5] (20.0 dBm)
* 2437 MHz [6] (20.0 dBm)
* 2442 MHz [7] (20.0 dBm)
* 2447 MHz [8] (20.0 dBm)
* 2452 MHz [9] (20.0 dBm)
* 2457 MHz [10] (20.0 dBm)
* 2462 MHz [11] (20.0 dBm)
* 2467 MHz [12] (20.0 dBm)
* 2472 MHz [13] (20.0 dBm)
* 2484 MHz [14] (disabled)

Categories
Uncategorized

Missing important details: Don’t update to DSM 7 yet*

*) if you rely on the SynoCommunity package rdiff-backup to do backups.

That’s really it: The package is not supported (yet?) and backups should continue.

Don’t repeat my mistakes.

Categories
Hardware Linux

SATA/Soft-RAID Cards that work

A while ago, I wanted to add more space to my MD+LVM2 array on a Linux machine (Ubuntu 20.04 at the moment).
The setup used MD to make a RAID1 of two HDDs and knowing that I would eventually like to add more storage later, I put LVM2 on top.

A Card That Doesn’t Work

The moment came but I realised my mainboard did not have enough SATA ports. So, after some research, I bought a HighPoint Rocket 640L PICe SATA host adapter which comes with a Marvell 88SE92xx series chip (88SE9230 in my case).

Tl;dr: Don’t.

It took me half a day of research to figure out that (as of September 2021 and Linux kernel 5.4, but I tried 5.14 too) it would not show disks at all if IOMMU was activated. Also, it did not like stand-by/hibernation: HDD 4 would spin up fine on boot, but would not show up after a stand-by rendering the RAID incomplete, and the LVM volume in read-only mode. Which is a nice safeguard, thanks LVM-developers! But still annoying as such of course.

A Card That Does Work

So, I did more research and found the Delock 5x SATA PCIe x4 Card featuring a JMicron Technology chip (with an ID [197b:0585]). And guess what:

  • All disks come back after stand-by/hibernation
  • IOMMU and virtualisation can be switched on, no problem
  • It even has one more port than the HighPoint card

Disclaimer: All this may change in the future. I have no idea if this is a driver problem or a problem with the chip. But I though it might save some of you time.

Conclusion

Don’t use Marvell 88SE92xx series, at least in a Linux system. Full stop.

Categories
Uncategorized

Repairing Electronics Rant

So, I have this Samsung NP900X3A laptop that I used for about 4 years. It started to have random glitches when I was about to start some very important work, so I decided to get a new laptop. I never sold or scrapped it, and recently I took it out of its neoprene bag, cleared the disk, installed a clean Ubuntu 20.04 and it worked like a charm…

…ish. Well, obviously it’s old, the battery had seen better days, but for work when it’s on power supply, it is a very decent device. Until recently:

I plugged it in, opened the lid, pressed power aaaand: it won’t boot.

Okay, my first though: The SSD died. Nothing catastrophic, a 120 GB mSATA costs about 45 €. So I booted into a Linux live system, quickly installed smartmontools (smartctl), opened my favourite smartctl reminder wiki page (seriously, man pages are still the best, but they’re so long!), and found that all tests passed. Well, the drive was old, yes, but still intact. fsck did not complain either.

But it still wouldn’t boot.

So, I opened the BIOS menu. First suspect: time and date were off by years. Aha! The BIOS battery, or actually, the battery for the real time clock (RTC), had died. And that set everything to defaults. Including: UEFI [DISABLED]. Well, you could’ve printed something, you nob…

Anyways: Enabled it, it boots again.

Here’s the culprit: Empty, unmarked

So, what about the battery? It’s obviously a coin cell on two wires with a plug. Nice idea, very accessible. Really. But at least some documentation would have been nice. No label, no print, nothing.

Measuring reveals: diameter 16 mm, thickness maybe a little below 2 mm. Hard to tell exactly while still in the rubber protection. So, could it just be a CR1616 or CR1620 cell with some spot welded soldering pins enclosed in some heat-shrink tubing?

No way to get the original part anymore. Samsung stopped selling laptops for quite a while in Germany (or even the whole EU, though it may have recently restarted again). Ebay has some offers. Anything from 5 to 150 € (seriously, it’s a coin cell, who charges more than 10 € for that?).

I decided to get one full package (cell, heat-shrink, wires and connector) for 5 € and a bare cell with spot welded solder pads for 2 €.

Got the wrong cell for 5 € (for a Samsung P20). Would’ve fit if it wasn’t for the connector.
CR1616 for 1,59 € to the rescue!

So, where’s the rant? Here:

COMPLEX ELECTRONIC DEVICES “BREAK” BECAUSE OF < 5 € COMPONENTS AND YOU NEED A F****ING PhD IN BATTERIES AND EBAY TO FIND REPLACEMENT PARTS BECAUSE NOONE THOUGHT OF PRINTING “CHECK UEFI SETTINGS” OR OF PRINTED LABELS ON A BATTERY OR A SIMPLE REMARK IN THE USER MANUAL (“YEAH, AND BTW, THE BATTERY IS A CR1616 FOR 84 CENTS WITH A MOLEX CONNECTOR, YOU’RE WELCOME.”)???

By now, this laptop would’ve been thrown out twice by every single average capable consumer in the world. And honestly, I cannot blame them. Right to repair will have a loooooong way to go.


So yeah: Turns out the coin cell actually was a CR1616.

Cut off the wires, placed heat-shrink, soldered to replacement cell (not to the cell directly but to the solder tails), pack all in heat-shrink and voilà: a 1,65 â‚¬ (+some solder and heat-shrink) replacement for a 10 year old laptop.

Categories
Uncategorized

A Toy Traffic Signal with an Arduino Nano

My kid likes traffic lights. So I decided to quickly build one. I started off on a breadboard with a bunch of components:

  • 3 × LEDs (red, amber, green)
  • 3 × 470 Î© Resistors (only had two, so the third are actually 2 1 kΩ in parallel)
  • 1 × 10 kΩ Resistor
  • 1 × Pushbutton Switch
  • 1 × Arduino Nano (actually a clone)

The circuitry is relatively simple. All LEDs’ cathodes point to ground (GND) through a 470 Î© resistor each to limit current and not blow anything. The anodes are connected individually to the digital port D3 through D5.

The button works such that, if open, it connects digital port D2 via a 10 kΩ resistor to the 3.3 V output of the Nano. When it closes, it’s supposed to pull D2 to GND, so it’s directly connected to it. The resistor serves to limit the current and – again – not blow anything.

That’s it. Now, for example, pulling D3 high (i.e. applying 3.3 V) will switch on the red LED. Likewise for D4 on the yellow and D5 on the green LED. Let’s introduce some human readable names instead of D2 through D5:

#include <Arduino.h>

#define BUTTON      D2
#define LED_RED     D3
#define LED_YELLOW  D4
#define LED_GREEN   D5

In the setup() function, I set the button pin as an input and the LED pins as outputs:

void setup() {
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_YELLOW, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
  pinMode(BUTTON, INPUT);
}

To control the traffic light logic, I scribbled a Moore machine on a piece of paper, starting with a pedestrian traffic light (only red and green).

Moore machine with two states (red and green), initialised in the red state. Looped arrows indicate idle non-transition.

The machine starts in the red-light state. It remains there, unless the button is pressed. Then, it instantly switches to green (a pedestrian’s dream) where it remains until the button is pressed again.

This behaviour can be implemented by putting a simple switch case statement in the main loop() and implementing a state variable:

// States
enum states{ ST_R,
             ST_G
           }

void setState(bool red, bool green) {
  digitalWrite(LED_RED, red ? HIGH : LOW);
  digitalWrite(LED_GREEN, green ? HIGH : LOW);
}

void setRed() { setState(true, false); }
void setGreen() { setState(false, true); }

void loop() {
  switch(machineState) {
    case ST_R:
      // Serial.write("State: Red");
      setRed();
      break;
    case ST_G:
      // Serial.write("State: Green");
      setGreen();
      break;
}

Why use this enum thingy, you say? You could just use an int and call your states 0, 1, 2, etc., you say? Yes, you could also sign up for the biannual global brainfuck contest.

But, hey, wait, there are no transitions!

Exactly! For that purpose I’ll use an interrupt.

Just add one more function:

void ISR_ButtonToggle() {
  Serial.write("BUUUUTTON!\n");
  if(machineState == ST_R) {
    machineState = ST_G;
  } else if (machineState == ST_G) {
    machineState = ST_R;
  }
}

And don’t forget to actually bind the interrupt function in setup():

attachInterrupt(digitalPinToInterrupt(BUTTON), ISR_ButtonToggle, FALLING);

Now that’s a simple and pretty useless traffic light (well, I still like that I can switch it to green at will, this really should be the default pedestrian crossing light!).

But it is easy to extend. You want a yellow state? Add a yellow state ST_Y to your state machine (the switch case statement). Or you want it to show yellow before switching from green to red? Add a yellow-before-red state ST_Y_R. Or you really like to let people wait? Add a wait state. Oh, and of course you can call your states whatever you want.

Here is my full implementation of a non-pedestrian red-yellow-green traffic light as it is stated in the German traffic rules:

  • From red to green, with wait periods, go through
    • Red
    • Red+Yellow (yes, red+yellow, yellow meaning “get ready” and red meaning “not too fast, it’s still red, Freundchen!”)
    • Green
  • From green to red, with wait periods, go through
    • Green
    • Yellow (yes, only yellow. The meaning is disputed. Reasonable people say it means “Prepare to stop, if you can’t make it in time, it’s okay.” while idiots say it means “STEP ON THE GAS!”)
    • Red (This is also sometimes disputed, I won’t go into this.)
#include <Arduino.h>

#define BAUDRATE      9600
#define BUTTON        DD2
#define LED_RED       DD3
#define LED_YELLOW    DD4
#define LED_GREEN     DD5

// Transition delays
#define BLINK_WAIT        7
#define DELAY_R_RY        250
#define DELAY_RY_G        250
#define DELAY_G_Y         250
#define DELAY_Y_R         250
#define DELAY_R_G         250
#define DELAY_G_R         250

// States
enum states{ ST_R,
             ST_RY,
             ST_G,
             ST_Y,
             WAIT_R_RY,
             WAIT_RY_G,
             WAIT_G_Y,
             WAIT_Y_R,
             WAIT_G_R,
             WAIT_R_G };

states machineState = ST_R;
bool builtin_led = false;

void setState(bool red, bool yellow, bool green) {
  digitalWrite(LED_RED, red ? HIGH : LOW);
  digitalWrite(LED_YELLOW, yellow ? HIGH : LOW);
  digitalWrite(LED_GREEN, green ? HIGH : LOW);
}

void setRed() { setState(true, false, false); }
void setRedYellow() { setState(true, true, false); }
void setYellow() { setState(false, true, false); }
void setGreen() { setState(false, false, true); }

void blinkDelay(int delay_ms) {
  for(int i=0; i<BLINK_WAIT; i++){
    delay(delay_ms);
    digitalWrite(LED_BUILTIN, builtin_led);
    Serial.write(".");
    builtin_led = !builtin_led;
  }
  Serial.write("\n");
}

void ISR_ButtonToggle() {
  Serial.write("BUUUUTTON!\n");
  if(machineState == ST_R) {
    machineState = WAIT_R_RY;
  } else if (machineState == ST_G) {
    machineState = WAIT_G_Y;
  }
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_YELLOW, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
  digitalWrite(LED_RED, LOW);
  digitalWrite(LED_YELLOW, LOW);
  digitalWrite(LED_GREEN, LOW);
  pinMode(BUTTON, INPUT);

  attachInterrupt(digitalPinToInterrupt(BUTTON), ISR_ButtonToggle, FALLING);

  Serial.begin(BAUDRATE);
  Serial.write("Ready.\n");
}

void loop() {
  switch(machineState) {
    case ST_R:
      // Serial.write("State: Red");
      setRed();
      break;
    case ST_G:
      // Serial.write("State: Green");
      setGreen();
      break;
    case WAIT_R_RY:
      Serial.write("Wait: Red->RedYellow\n");
      blinkDelay(DELAY_R_RY);
      machineState = ST_RY;
      break;
    case ST_RY:
      setRedYellow();
      machineState = WAIT_RY_G;
      break;
    case WAIT_RY_G:
      Serial.write("Wait: RedYellow->Green\n");
      blinkDelay(DELAY_RY_G);
      machineState = ST_G;
      break;
    case WAIT_G_Y:
      Serial.write("Wait: Green->Yellow\n");
      blinkDelay(DELAY_G_Y);
      machineState = ST_Y;
      break;
    case ST_Y:
      setYellow();
      machineState = WAIT_Y_R;
      break;
    case WAIT_Y_R:
      Serial.write("Wait: Yellow->Red\n");
      blinkDelay(DELAY_Y_R);
      machineState = ST_R;
      break;
    case WAIT_R_G:
      Serial.write("Wait: Red->Green\n");
      blinkDelay(DELAY_R_G);
      machineState = ST_G;
      break;
    case WAIT_G_R:
      Serial.write("Wait: Green->Red");
      blinkDelay(DELAY_G_R);
      machineState = ST_R;
      break;
    default:
      Serial.write("Woops...\n");
      machineState = ST_R;
  }
}

Is this the best way to write this? Hell, no! Is it even a good tutorial? You tell me. I think it’s fairly simple and shows some evenly common concepts:

  • Using IO pins
  • Wiring LEDs without burning them or the output pin
  • Wiring a button*
  • Using #define statements to make code more readable and lives more easy
  • Using some abstraction (setState(), setRed(), etc.)
  • Using a state machine as an extensible concept
  • Using an interrupt (Why use an interrupt? Because it interrupts your code exactly when the button is pressed. It makes your little circuit very responsive and – at least in this example – it is even easier to write.)

*) You may encounter a problem that goes by the name “bouncing” (or in the beautiful German language “prellen”). That’s when your button press toggles multiple interrupts in a row. Try putting a capacitor in parallel with the resistor, or as a bad software hack, add a 5 ms delay (delay(5);) to the end of your interrupt function.

Anyways: My girl loves it. Ha! You thought she’d be a boy because it was a traffic light. Shame on you!

Categories
Finite Elements

Breaking Changes 2: Pygmsh, Fenics and Meshio>=4.0.0

Package versions this was tested with (2020-12-22):
gmsh 4.7.1
fenics 2019.1.0:latest
meshio 4.3.7
pygmsh 7.1.5

So, I thought, let’s do the article on Meshio>=4.0.0 and Fenics and show how interchangeable the gmsh itself, gmsh Python API, and pygmsh are. Well, I seem to not get it, but at least I did not manage to show that.

Using pygmsh with physical labels and Fenics is a bit unclear to me. I got it to work eventually, so here are the code blocks.

Generate the mesh using pygmsh

import pygmsh
# OpenCascade in pygmsh seems not to support extraction of lines from a rectangle (... to use with physical labels).
# So, let's use the geo kernel:
with pygmsh.geo.Geometry() as geom:
    r1 = geom.add_rectangle(0., 5e-3, 0., 2.5e-3, z=0.)
    geom.add_physical(r1.lines, label="1")
    geom.add_physical(r1.surface, label="2")

mesh = geom.generate_mesh(dim=2)
# We'll use gmsh format version 2.2 here, as there's a problem
# with writing nodes in the format version 4.1 here, that I cannot figure out
mesh.write("test.msh", file_format="gmsh22")

So, here’s the first oddity I would not get my head around: There seems to be no easy way to access the boundaries of the rectangle generated with the OpenCASCADE kernel. In gmsh’s API they were there as the first four one-dimensional items (although without the tutorial file there would have been no way I could have guessed that).

The second problem was writing the generated mesh to a gmsh format version 4.1, which resulted in an error message I could not quite track back:

>>> mesh.write("test.msh", file_format="gmsh") # That's gmsh41

---------------------------------------------------------------------------
WriteError                                Traceback (most recent call last)
  in 
      13 # with writing nodes in the format version 4.1 here, that I cannot
      14 # figure out
 ---> 15 mesh.write("test.msh", file_format="gmsh")
 /usr/local/lib/python3.6/dist-packages/meshio/_mesh.py in write(self, path_or_buf, file_format, **kwargs)
     158         from ._helpers import write
     159 
 --> 160         write(path_or_buf, self, file_format, **kwargs)
     161 
     162     def get_cells_type(self, cell_type):
 /usr/local/lib/python3.6/dist-packages/meshio/_helpers.py in write(filename, mesh, file_format, **kwargs)
     144 
     145     # Write
 --> 146     return writer(filename, mesh, **kwargs)
 /usr/local/lib/python3.6/dist-packages/meshio/gmsh/main.py in (f, m, **kwargs)
     109     {
     110         "gmsh22": lambda f, m, **kwargs: write(f, m, "2.2", **kwargs),
 --> 111         "gmsh": lambda f, m, **kwargs: write(f, m, "4.1", **kwargs),
     112     },
     113 )
 /usr/local/lib/python3.6/dist-packages/meshio/gmsh/main.py in write(filename, mesh, fmt_version, binary, float_fmt)
     100             )
     101 
 --> 102     writer.write(filename, mesh, binary=binary, float_fmt=float_fmt)
     103 
     104 
 /usr/local/lib/python3.6/dist-packages/meshio/gmsh/_gmsh41.py in write(filename, mesh, float_fmt, binary)
     356 
     357         _write_entities(fh, cells, tag_data, mesh.cell_sets, mesh.point_data, binary)
 --> 358         _write_nodes(fh, mesh.points, mesh.cells, mesh.point_data, float_fmt, binary)
     359         _write_elements(fh, cells, tag_data, binary)
     360         if mesh.gmsh_periodic is not None:
 /usr/local/lib/python3.6/dist-packages/meshio/gmsh/_gmsh41.py in _write_nodes(fh, points, cells, point_data, float_fmt, binary)
     609         if len(cells) != 1:
     610             raise WriteError(
 --> 611                 "Specify entity information to deal with more than one cell type"
     612             )
     613 
 WriteError: Specify entity information to deal with more than one cell type

Preparing mesh and boundary files for Fenics

Falling back to gmsh format version 2.2, I could generate the mesh and boundary files like in the original post:

outfile_mesh = f"{prefix:s}_mesh.xdmf"
outfile_boundary = f"{prefix:s}_boundaries.xdmf"

# read input from infile
inmsh = meshio.read(f"{prefix:s}.msh")
# delete third (obj=2) column (axis=1), this strips the z-component
outpoints = np.delete(arr=inmsh.points, obj=2, axis=1)

# create (two dimensional!) triangle mesh file
outmsh = meshio.Mesh(points=outpoints,
                      cells=[('triangle', inmsh.get_cells_type("triangle"))],
                      cell_data={'Subdomain': [inmsh.cell_data_dict['gmsh:physical']['triangle']]},
                      field_data=inmsh.field_data)

# write mesh to file
meshio.write(outfile_mesh, outmsh)
# create (two dimensional!) boundary data file
outboundary = meshio.Mesh(points=outpoints,
                           cells=[('line', inmsh.get_cells_type('line') )],
                           cell_data={'Boundary': [inmsh.cell_data_dict['gmsh:physical']['line']]},
                           field_data=inmsh.field_data)
# write boundary data to file
meshio.write(filename=outfile_boundary, mesh=outboundary)

Just to figure out that while pygmsh allows you to assign a string label to physical groups it numbers them automatically (apparently starting at 0). This is fine, it just seems to be written nowhere to be found.

Modifying the original code at the definition of the Dirichlet Boundary Condition did the trick (full listing at the end):

# ...

bcs = [
     do.DirichletBC(FS, do.Constant((0.0, 0.0)), outerwall, 0), # Choose physical group "index" zero here.
     ]

# ...

Conclusion

While this arguably still works and does the job it took me a few tries to figure out how. So, I hope this helps others at some point.

Full listing of the Fenics part

import dolfin as do
import numpy as np
import matplotlib.pyplot as plt

# Import Mesh
mesh = do.Mesh()
with do.XDMFFile(outfile_mesh) as meshfile, \
        do.XDMFFile(outfile_boundary) as boundaryfile:
    meshfile.read(mesh)
    mvc = do.MeshValueCollection("size_t", mesh, 2)
    boundaryfile.read(mvc, "Boundary")
    outerwall = do.MeshFunction("size_t", mesh, mvc)

do.plot(mesh); plt.show()

# Generate Function Space
FE = do.FiniteElement("RTE", mesh.ufl_cell(), 3)
FS = do.FunctionSpace(mesh, FE)

# Use markers for boundary conditions (watch for the change! ;-) )
bcs = [
    do.DirichletBC(FS, do.Constant((0.0, 0.0)), outerwall, 0), # Choose physical group "index" zero here.
    ]

# Trial and Test functions
E = do.TrialFunction(FS)
EE = do.TestFunction(FS)

# Helmholtz EVP (Ae = -k_co^2B*e)
a = do.inner(do.curl(E), do.curl(EE))*do.dx
b = do.inner(E, EE)*do.dx

# For EVP make use of PETSc and SLEPc
dummy = E[0]*do.dx
A = do.PETScMatrix()
B = do.PETScMatrix()

# Assemble System
do.assemble_system(a, dummy, bcs, A_tensor=A)
do.assemble_system(b, dummy, bcs, A_tensor=B)

# Apply Boundaries
[bc.apply(B) for bc in bcs]
[bc.apply(A) for bc in bcs]

# Let SLEPc solve that generalised EVP
solver = do.SLEPcEigenSolver(A, B)
solver.parameters["solver"] = "krylov-schur"
solver.parameters["tolerance"] = 1e-16
solver.parameters["problem_type"] = "gen_hermitian"
solver.parameters["spectrum"] = "target magnitude"
solver.parameters["spectral_transform"] = "shift-and-invert"
solver.parameters["spectral_shift"] = -(2.np.pi/10e-3)*2

neigs = 20
solver.solve(neigs)
print(f"Found {solver.get_number_converged():d} solutions.")

# Return the computed eigenvalues in a sorted array
computed_eigenvalues = []

for i in range(min(neigs, solver.get_number_converged())):
    r, _, fieldRe, fieldIm = solver.get_eigenpair(i) # ignore the imaginary part
    f = do.Function(FS)
    f.vector()[:] = fieldRe
    if np.abs(r) > 1.1:
        # With r = -gamma^2, find gamma = sqrt(-r)
        gamma = np.sqrt(r)
        do.plot(f); plt.title(f"{gamma:f}"); plt.show()
    computed_eigenvalues.append(r)

print(np.sort(np.array(computed_eigenvalues)))