The Python Equivalent of the LabVIEW "He ...

The Python Equivalent of the LabVIEW "Hello World" State Machine

May 15, 2024

Introduction
Have you asked yourself "is LabVIEW easier than Python?" or "is LabVIEW similar to Python?", you are not alone, thus I decided to play with both in a reasonably simple test case.

As a seasoned LabVIEW programmer, the quest for a "minimalist" State Machine implementation in Python that rivals the simplicity and clarity of LabVIEW's approach has been a bit of a challenge. That is, until the discovery of Python's nonlocal statement, a game-changer that enabled the recreation of LabVIEW's shift register functionality in a functional programming style. Why opt for a functional approach rather than an object-oriented one? The answer lies in the quest for simplicity. In a functional paradigm, the code's purpose and behaviour are transparent, making it intuitively understandable, even to those new to it. In contrast, object-oriented designs, while powerful, can become cumbersome, obscuring the core logic with layers of abstraction. The goal was to devise something truly minimalist, a State Machine so straightforward that its operation is immediately apparent, needing little to no explanation. This article explores this journey, detailing how a functional Python State Machine can be close to the LabVIEW counterpart in effectiveness and readability, without the overhead of classes and objects, and focusing on clarity and maintainability.

Next, we'll delve into the specifics of this minimalist design, comparing with the exact equivalent in LabVIEW. We'll examine the core concepts, code snippets, and practical examples.

Nonlocal variables and shift-registers

 

def encolosing_func():

    myvar = "any string"

    def enclosed_func():

        nonlocal myvar

        print(myvar) 

In LabVIEW, the shift register is a powerful feature that provides a straightforward way to maintain and update state information across multiple iterations within a loop. It acts as a memory element, holding onto data between iterations, which is crucial for implementing State Machines. This concept finds its counterpart in Python through the nonlocal statement, it can be used in nested functions to maintain data across executions. The nonlocal keyword allows a nested function to access and modify variables from its enclosing scope, effectively preserving state across function calls. While Python's approach might initially seem less visual compared to LabVIEW's graphical representation, it offers a similarly robust mechanism for state and context data retention. The nonlocal statement, therefore, serves as a textual bridge to the conceptual model provided by LabVIEW's shift registers, offering a minimalist yet potent tool for state management in Python.

The Python while-dictionary equivalent of while–case in LabVIEW

 

In LabVIEW, the case structure within a while loop is a common pattern for creating State Machines, where the case selected depends on the current state, allowing the program to execute different code blocks based on the state's value. Similarly, in Python, a dictionary can map states to corresponding functions, and a while loop can repeatedly invoke these functions based on the current state. This dictionary acts as a dynamic dispatcher, simulating a switch-case behaviour. It must be noted up until Python 3.10 switch-case and match-case were not element of the language and the best approximation of switch-case was a series of if – elif.

 def simple_sm():

   

“… state codes is here… “

 

    statedict={"state_1":state_1,

               "state_2":state_2,

               "stop":stop}

   

    while nextstate!="stop" :

        statedict[nextstate]()

   

    #LabVIEW behaviour is DO-WHILE the last call here ensure the last state
    # “stop” is called like in LabVIEW otherwise the last state, triggers the

    #exit condition of the while statement and is never executed

   

    statedict[nextstate]()

By updating the state variable within each function, the loop iteratively calls different functions, akin to transitioning between cases in LabVIEW.

This approach in Python, while distinct in syntax, captures the essence of LabVIEW's state-based decision-making within a loop. It offers a clear, concise way to manage state transitions, embodying the simplicity and directness of our minimalist approach.

The basic State Machine skeleton

Below the most basic possible implementation of the State Machine pattern in Python equivalent to the LabVIEW one.

def simple_sm():

   

    counter=0

    nextstate="state_1"

   

    def state_1():

        nonlocal counter,nextstate

        counter=counter+1

        nextstate="state_2"

    

    def state_2():

        nonlocal counter,nextstate

        counter=counter+2

        nextstate="stop"

   

    def stop():

        nonlocal counter,nextstate

        print(counter)

        nextstate="stop"

       

    statedict={"state_1":state_1,

               "state_2":state_2,

               "stop":stop}

   

    while nextstate!="stop" :

        statedict[nextstate]()

   

    statedict[nextstate]()

   

#%%

 

simple_sm()

 

The provided Python script demonstrates a basic yet effective State Machine using a functional approach. At its core, the simple_sm function encapsulates the entire State Machine, with counter and nextstate variables tracking the state and associated data. The nested functions state_1, state_2, and stop represent individual states, each manipulating the shared counter and transitioning to the next state by updating nextstate. The statedict dictionary maps state names to their corresponding functions, enabling dynamic function calls based on the current state. The while loop continues to invoke the appropriate state functions until reaching the "stop" state, at which point the final state function is executed to conclude the machine's run. This skeleton highlights the elegance and simplicity of managing state transitions in Python using closures and dictionaries, offering a clear, maintainable structure for implementing state-driven logic.

 

Our target “Hello World” example in LabVIEW

In LabVIEW, the equivalent of "Hello World" State Machine involves a simple yet illustrative example: a random data plotter.

This State Machine typically cycles through states such as initialization, data generation, plotting, and checking for a stop condition and provides a useful pattern for data generation and visualization. Each state is visually represented as a case within a while loop, with the loop iterating based on the current state. Random data generation in the example can be easily substituted with other data sources as needed by the user. The user can stop the process through a UI control, transitioning the machine to a final state that ends the loop. This example in LabVIEW showcases the intuitive, graphical approach to State Machines, setting a clear and visual standard that we aim to parallel in Python with a functional and concise code structure.

First no-gui Python implementation


In our first Python implementation, we translate the LabVIEW "Hello World" State Machine into a console-based version without a UI, focusing purely on the functional logic. This Python script emulates the LabVIEW example by cycling through initialization, data generation, data "display" (via print statements), and a termination condition after 100 iterations. By capturing the essence of state transitions without graphical elements, we lay the foundational structure, demonstrating the core functionality of generating and handling data sequentially in a State Machine manner, akin to our LabVIEW counterpart.

import random

import time

 

def simple_sm():

    counter = 0

    next_state = "init"

   

    def init():

        nonlocal next_state

        print("Initialization")

        next_state = "wait_100"

   

    def wait_100():

        nonlocal next_state

        time.sleep(0.005)  # Wait for 5 milliseconds

        next_state = "generate_data"

   

    def generate_data():

        nonlocal next_state, counter

        if counter >= 100:

            next_state = "stop"

            return

        data = random.random()

        print(f"Data generated: {data}")

        counter += 1

        next_state = "wait_100"

   

    def stop():

        nonlocal next_state

        print("Stopping after 100 iterations.")

        next_state = "stop"

   

    state_dict = {

        "init": init,

        "wait_100": wait_100,

        "generate_data": generate_data,

        "stop": stop,

    }

   

    while next_state != "stop":

        state_dict[next_state]()

   

    state_dict[next_state]()

 

simple_sm()

Building the GUI Implementation: The Need for a Circular Buffer.

Tkinter, the basic GUI library for Python, does not have an equivalent chart like the LabVIEW one. We can plot traces using matplotlib but the inherent circular functionality must be built explicitly. In this case I opted for a very simple class. This class provide the buffer functionality and is essentially the Python equivalent of a functional global variable in LabVIEW that would do the same.

# Circular buffer to store data

class CircularBuffer:

    def init(self, size):

        self.buffer = [0] * size

        self.size = size

        self.index = 0

   

    def append(self, value):

        self.buffer[self.index] = value

        self.index = (self.index + 1) % self.size

   

    def get(self):

        return self.buffer[self.index:] + self.buffer[:self.index]

 

Understanding the Need for the GUI Thread in GUI Implementation.

 

In LabVIEW, the graphical user interface (GUI) is inherently integrated into the development environment. Programmers can effortlessly create and interact with GUI components without explicitly considering event handling or the GUI's existence at the code level. This seamless integration contrasts with Python, where GUI development requires explicit consideration of the graphical elements and their event handling.

In text-based programming languages like Python, the GUI is not a given but is created and managed through an application layer. This layer oversees the business logic and responds to user events. Python offers several libraries for GUI development, with Tkinter being the most commonly included in standard distributions. Other popular libraries include PyQt , Kivy and wxPython each providing distinct widgets and styles.

Unlike LabVIEW's drag-and-drop interface, in Python, GUI elements must be explicitly declared and configured in the code. This approach grants programmers a high degree of freedom, allowing them to choose from various libraries and customize their application's look and functionality. However, it also requires a more hands-on approach to GUI creation and event management.

Here's a basic example of a Tkinter application structure in Python:

import tkinter as tk

 

root = tk.Tk()

root.title("My Tkinter App")

 

# Create and configure widgets

label = tk.Label(root, text="Hello, Tkinter!")

label.pack()

 

# Start the application's event loop

root.mainloop()


Integrating Everything Together: The Tkinter GUI Equivalent of LabVIEW's Hello World

 

 

In this section, we present the final Python implementation of our target example, mirroring the LabVIEW "Hello World" State Machine using Tkinter for the graphical elements and incorporating our circular buffer for data management.

Key differences from the initial console-based implementation include transitioning from a while-loop to a scheduled event mechanism. This shift is essential for integrating with Tkinter's event-driven model.

    def runstatemachine():

        state_dict[next_state]()

        if next_state != 'stop':

            root.after(0, runstatemachine)

Using root.after(0, runstatemachine) replaces a while loop with a non-blocking call, ensuring the Tkinter event loop remains responsive to user interactions. A simple time.sleep would pause the entire GUI, making it unresponsive, which is why the event scheduling method is crucial.

Additionally, we link function calls to GUI events. For example, the stop button is tied to the request_stop function, allowing the button press event to access and modify the stop_requested variable's scope:

 stop_button = tk.Button(root, text="Stop", command=request_stop)

This code snippet establishes the core GUI components, sets up the matplotlib figure within the Tkinter window, and initializes the State Machine alongside the GUI's stop button. It demonstrates a harmonious blend of State Machine logic with GUI interactivity, providing a comprehensive example of how to adapt a LabVIEW-style State Machine to a Pythonic, functional approach with Tkinter.

if name == "main":

    root = tk.Tk()

    root.title("Random Data Plotter")

    fig, ax = plt.subplots()

    line, = ax.plot(range(100), [0] * 100)

    canvas = FigureCanvasTkAgg(fig, master=root)

    canvas.gettkwidget().pack()

    runstatemachine, request_stop = createstatemachine(root, line, ax, canvas)

    stop_button = tk.Button(root, text="Stop", command=request_stop)

    stop_button.pack()

    root.after(0, runstatemachine)

    root.mainloop()

 

By elucidating these modifications, we illustrate the seamless integration of a functional Python State Machine with Tkinter's graphical interface, reaching the closest approximation possible to the LabVIEW random data plotter I’m aware of.

Final Code

import tkinter as tk

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

from matplotlib.figure import Figure

import matplotlib.pyplot as plt

import random

 

# Circular buffer to store data

class CircularBuffer:

    def init(self, size):

        self.buffer = [0] * size

        self.size = size

        self.index = 0

   

    def append(self, value):

        self.buffer[self.index] = value

        self.index = (self.index + 1) % self.size

   

    def get(self):

        return self.buffer[self.index:] + self.buffer[:self.index]

 

def create_state_machine(root, plot, ax, canvas):

    next_state = 'init'

    data_buffer = CircularBuffer(100)

    stop_requested = False

 

    def init_state():

        nonlocal next_state

        print("Initialization")

        next_state = 'wait_100'

 

    def wait_100_state():

        nonlocal next_state

        next_state = 'generate_random'

 

    def generate_random_state():

        nonlocal next_state

        if not stop_requested:

            random_data = random.random()

            data_buffer.append(random_data)

        next_state = 'plot_data'

 

    def plot_data_state():

        nonlocal next_state

        plot.set_ydata(data_buffer.get())

        ax.relim()

        ax.autoscale_view()

        canvas.draw()

        next_state = 'check_gui_input'

 

    def check_gui_input_state():

        nonlocal next_state

        if stop_requested:

            next_state = 'stop'

        else:

            next_state = 'wait_100'

 

    def stop_state():

        nonlocal next_state

        print("Stopping")

        root.destroy()

        root.quit()

 

    def request_stop():

        nonlocal stop_requested

        stop_requested = True

 

    state_dict = {

        'init': init_state,

        'wait_100': wait_100_state,

        'generate_random': generaterandomstate,

        'plot_data': plotdatastate,

        'check_gui_input': check_gui_input_state,

        'stop': stop_state,

    }

 

    def run_state_machine():

        state_dict[next_state]()

        if next_state != 'stop':

            root.after(0, runstatemachine)

 

    return runstatemachine, request_stop

 

 

if name == "main":

    root = tk.Tk()  # Initialize the main Tkinter window

    root.title("Random Data Plotter")

 

    # Create a matplotlib figure and embed it in the Tkinter window

    fig, ax = plt.subplots()

    line, = ax.plot(range(100), [0] * 100)

    canvas = FigureCanvasTkAgg(fig, master=root)

    canvas.gettkwidget().pack()  # Pack the canvas for display

 

    # Initialize the State Machine and bind the stop button

    run_state_machine, requeststop = create_state_machine(root, line, ax, canvas)

 

    stop_button = tk.Button(root, text="Stop", command=request_stop)

    stop_button.pack()  # Place the stop button in the window

 

    # Schedule the first call to runstatemachine; this does not create a new thread

    # but places the function in the main GUI event loop queue

    root.after(0, runstatemachine)

 

    # Start the Tkinter main loop; this is where the GUI becomes responsive,

    # handling events and executing scheduled functions in the event queue

    root.mainloop()  # This is essentially the "main thread" of the Tkinter application

 

Conclusion

This article has delved into the development of a Python-based State Machine, illustrating its parallels with the LabVIEW environment. The aim was to assist LabVIEW users in transitioning to Python, enabling them to apply familiar programming patterns in a new context.Through a functional programming approach, we've demonstrated how Python's nonlocal keyword and dictionary-based state management can effectively mimic the behaviour of LabVIEW's shift registers and case structures, providing a minimalist yet powerful method for State Machine implementation.

The transition from a console-based to a Tkinter-based GUI illustrates Python's version of handling complex state-driven logic alongside responsive graphical interfaces. This example serves not only as a guide for LabVIEW programmers venturing into Python but also underscores the broader theme of adaptability and clarity in programming—principles that hold value regardless of the language or environment.

Future Directions

I'm curious to hear your thoughts on this journey from LabVIEW's graphical IDE to Python's script-based approach. What aspects of Python's State Machine implementation did you find most intriguing or challenging? Are there specific topics or examples you'd like to delve into next? Like a parallel producer-consumer architecture ? or integrating popular Python Libraries like Random Forest Classifier into LabVIEW ? Your feedback is invaluable and will help shape future content to better suit your interests and needs. Whether you're integrating Python into your workflow, tackling complex projects, or simply exploring new programming horizons, your insights can drive our next exploration. Let's continue this journey together.

Support and Downloads

If you found this tutorial helpful and are feeling generous, consider supporting my work with a spontaneous offer of 1 coffee (or any amount that suits you). Your support not only motivates me but also helps in creating more insightful content like this. Remember, the primary aim is to share valuable knowledge, so feel free to enjoy and share this tutorial regardless of contribution. Every bit of support is greatly appreciated!

You can download all the code associated with this small article, both LabVIEW and Python from here.

Save for later

if you are reading this content on a mobile device you can send this article to your email using this link

MAIL TO ME

Enjoy this post?

Buy Filippo Persia a coffee

More from Filippo Persia