Leo
Leo

Reputation: 1168

Kill a loop with Button Jupyter Notebook?

I want to:

From How to kill a while loop with a keystroke? I have taken the example to interrupt using Keyboard Interrupt, This works, but i would like to use a button.

EXAMPLE WITH KEYBOARD INTERRUPT

weights = []
times = [] 
#open port 
ser = serial.Serial('COM3', 9600)
try:
   while True: # read infinite loop
       #DO STUFF
       line = ser.readline()   # read a byte string
       if line:
           weight_ = float(line.decode())  # convert the byte string to a unicode string
           time_ = time.time()
           weights.append(weight_)
           times.append(time_)
           print (weight_)
#STOP it by keyboard interup and continue with program 
except KeyboardInterrupt:
   pass
#Continue with plotting

However I would like to do it with a displayed button (easier for people to use). I have tried making a button (in Jupiter Notebook) that when pressed break_cicle=False, but the loop doesn't break when button pressed:

 #make a button for stopping the while loop 
button = widgets.Button(description="STOP!") #STOP WHEN THIS BUTTON IS PRESSED
output = widgets.Output()
display(button, output)
break_cicle=True


def on_button_clicked(b):
    with output:
        break_cicle = False # Change break_cicle to False
        print(break_cicle)
        
ser.close()   
button.on_click(on_button_clicked)
ser = serial.Serial('COM3', 9600)
try:
    while break_cicle:

        print (break_cicle)
        line = ser.readline()   # read a byte string
        if line:
            weight_ = float(line.decode())  # convert the byte string to a unicode string
            time_ = time.time()
            weights.append(weight_)
            times.append(time_)
            print (weight_)
except :
    pass

ser.close()    

EXAMPLE WITH GLOBAL NOT WORKING

from IPython.display import display
import ipywidgets as widgets

button = widgets.Button(description="STOP!") #STOP WHEN THIS BUTTON IS PRESSED
output = widgets.Output()
display(button, output)
break_cicle=True

def on_button_clicked():
    global break_cicle #added global
    with output:
        
        break_cicle = False # Change break_cicle to False
        print ("Button pressed inside break_cicle", break_cicle)
    
    
button.on_click(on_button_clicked)
try:
    while break_cicle:
        print ("While loop break_cicle:", break_cicle)
        time.sleep(1)
except :
    pass
print ("done")

Despite me pressing the button a few times,from the following image you can see that it never prints "Button pressed inside break_cicle".

enter image description here

Upvotes: 4

Views: 2863

Answers (1)

furas
furas

Reputation: 142641

I think problem is like in all Python scripts with long-running code - it runs all code in one thread and when it runs while True loop (long-running code) then it can't run other functions at the same time.

You may have to run your function in separated thread - and then main thread can execute on_button_clicked

This version works for me:

from IPython.display import display
import ipywidgets as widgets
import time
import threading

button = widgets.Button(description="STOP!") 
output = widgets.Output()

display(button, output)

break_cicle = True

def on_button_clicked(event):
    global break_cicle
    
    break_cicle = False

    print("Button pressed: break_cicle:", break_cicle)
    
button.on_click(on_button_clicked)

def function():
    while break_cicle:
        print("While loop: break_cicle:", break_cicle)
        time.sleep(1)
    print("Done")
    
threading.Thread(target=function).start()

Maybe Jupyter has some other method for this problem - ie. when you write functions with async then you can use asyncio.sleep() which lets Python to run other function when this function is sleeping.


EDIT:

Digging in internet (using Google) I found post on Jyputer forum

Interactive widgets while executing long-running cell - JupyterLab - Jupyter Community Forum

and there is link to module jupyter-ui-poll which shows similar example (while-loop + Button) and it uses events for this. When function pull() is executed (in every loop) then Jupyter can send events to widgets and it has time to execute on_click().

import time
from ipywidgets import Button
from jupyter_ui_poll import ui_events

# Set up simple GUI, button with on_click callback
# that sets ui_done=True and changes button text
ui_done = False
def on_click(btn):
    global ui_done
    ui_done = True
    btn.description = '👍'

btn = Button(description='Click Me')
btn.on_click(on_click)
display(btn)

# Wait for user to press the button
with ui_events() as poll:
    while ui_done is False:
        poll(10)          # React to UI events (upto 10 at a time)
        print('.', end='')
        time.sleep(0.1)
print('done')

In source code I can see it uses asyncio for this.


EDIT:

Version with multiprocessing

Processes don't share variables so it needs Queue to send information from one process to another.

Example sends message from button to function. If you would like to send message from function to button then better use second queue.

from IPython.display import display
import ipywidgets as widgets
import time
import multiprocessing

button = widgets.Button(description="STOP!") 
output = widgets.Output()

display(button, output)

queue = multiprocessing.Queue()

def on_button_clicked(event):
    queue.put('stop')
    print("Button pressed")
    
button.on_click(on_button_clicked)

def function(queue):
    
    while True:
        print("While loop")
        time.sleep(1)
        
        if not queue.empty():
            msg = queue.get()
            if msg == 'stop':
                break
            #if msg == 'other text':             
            #    ...other code...
            
    print("Done")
    
multiprocessing.Process(target=function, args=(queue,)).start()

or more similar to previous

def function(queue):

    break_cicle = True
    
    while break_cicle:
        print("While loop: break_cicle:", break_cicle)
        time.sleep(1)
        
        if (not queue.empty()) and (queue.get() == 'stop'):
            break_cicle = False
        
    print("Done")

EDIT:

Version with asyncio

Jupyter already is running asynio event loop and I add async function to this loop. And function uses await functions like asyncio.sleep so asynio event loop has time to run other functions - but if function could run only standard (not async) functions then it wouldn't work.

from IPython.display import display
import ipywidgets as widgets
import asyncio

button = widgets.Button(description="STOP!") 
output = widgets.Output()

display(button, output)

break_cicle = True

def on_button_clicked(event):
    global break_cicle
    
    break_cicle = False

    print("Button pressed: break_cicle:", break_cicle)
    
button.on_click(on_button_clicked)

async def function():   # it has to be `async`
    while break_cicle:
        print("While loop: break_cicle:", break_cicle)
        await asyncio.sleep(1)   # it needs some `await` functions
    print("Done")
    
loop = asyncio.get_event_loop()    
t = loop.create_task(function())  # assign to variable if you don't want to see `<Task ...>` in output

Upvotes: 9

Related Questions