using matplotlib or pyqtgraph to graph real time data - plot

I have devices connected to my serial port and I need to poll them and then display that data in a plot. I currently have this working (slowly) using matplotlib. I could have up to 64 devices connected and each device could have 20 pieces of data to update. I've set it up so that a new window can be created and a piece of data can be added to be plotted. With each additional plotting window that is opened my update rate slows considerably.
I've tried using blit animation in matplotlib, but it's not real smooth and I can see anomolies in the update. I've tried PyQtGraph, but can't find any documentation on how to use this package, and now I'm trying PyQwt, but can't get it installed (mostly because my company won't let us install a package that will handle a .gz file).
Any ideas or suggestions would be greatly appreciated.
import sys
from PyQt4.QtCore import (Qt, QModelIndex, QObject, SIGNAL, SLOT, QTimer, QThread, QSize, QString, QVariant)
from PyQt4 import QtGui
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from plot_toolbar import NavigationToolbar2QT as NavigationToolbar
import matplotlib.dates as md
import psutil as p
import time
import datetime as dt
import string
import ui_plotting
import pickle
try:
_fromUtf8 = QString.fromUtf8
except AttributeError:
_fromUtf8 = lambda s: s
class Monitor(FigureCanvas):
"""Plot widget to display real time graphs"""
def __init__(self, timenum):
self.timenum=timenum
self.main_frame = QtGui.QWidget()
self.timeTemp1 = 0
self.timeTemp2 = 0
self.temp = 1
self.placeHolder = []
self.y_max = 0
self.y_min = 100
# initialization of the canvas
# self.dpi = 100
# self.fig = Figure((5.0, 4.0), dpi=self.dpi)
self.fig = Figure()
FigureCanvas.__init__(self, self.fig)
# self.canvas = FigureCanvas(self.fig)
# self.canvas.setParent(self.main_frame)
# first image setup
# self.fig = Figure()
# self.fig.subplots_adjust(bottom=0.5)
self.ax = self.fig.add_subplot(111)
self.mpl_toolbar = NavigationToolbar(self.fig.canvas, self.main_frame,False)
self.mpl_toolbar.setFixedHeight(24)
# set specific limits for X and Y axes
# now=dt.datetime.fromtimestamp(time.mktime(time.localtime()))
# self.timenum = now.strftime("%H:%M:%S.%f")
self.timeSec = 0
self.x_lim = 100
self.ax.set_xlim(0, self.x_lim)
self.ax.set_ylim(0, 100)
self.ax.get_xaxis().grid(True)
self.ax.get_yaxis().grid(True)
# and disable figure-wide autoscale
self.ax.set_autoscale_on(False)
self.ax.set_xlabel('Time in Seconds')
# generates first "empty" plots
self.timeb = []
self.user = []
self.l_user = []
self.l_user = [[] for x in xrange(50)]
for i in range(50):
self.l_user[i], = self.ax.plot(0,0)
# add legend to plot
# self.ax.legend()
def addTime(self,t1,t2):
timeStamp = t1+"000"
# print "timeStamp",timeStamp
timeStamp2 = t2+"000"
test = string.split(timeStamp,":")
test2 = string.split(test[2],".")
testa = string.split(timeStamp2,":")
testa2 = string.split(testa[2],".")
sub1 = int(testa[0])-int(test[0])
sub2 = int(testa[1])-int(test[1])
sub3 = int(testa2[0])-int(test2[0])
sub4 = int(testa2[1])-int(test2[1])
testing = dt.timedelta(hours=sub1,minutes=sub2,seconds=sub3,microseconds=sub4)
self.timeSec = testing.total_seconds()
def timerEvent(self, evt, timeStamp, val, lines):
temp_min = 0
temp_max = 0
# Add user arrays for each user_l array used, don't reuse user arrays
if self.y_max<max(map(float, val)):
self.y_max = max(map(float, val))
if self.y_min>min(map(float, val)):
self.y_min = min(map(float, val))
# print "val: ",val
if lines[len(lines)-1]+1 > len(self.user):
for k in range((lines[len(lines)-1]+1)-len(self.user)):
self.user.append([])
# append new data to the datasets
# print "timenum=",self.timenum
self.addTime(self.timenum, timeStamp)
self.timeb.append(self.timeSec)
for j in range((lines[len(lines)-1]+1)):
if j >49:
break
if j not in lines:
del self.user[j][:]
self.user[j].extend(self.placeHolder)
self.user[j].append(0)
else:
if len(self.timeb) > (len(self.user[j])+1):
self.user[j].extend(self.placeHolder)
self.user[j].append(str(val[lines.index(j)]))
for i in range(len(lines)):
if i>49:
break
self.l_user[lines[i]].set_data(self.timeb, self.user[lines[i]])
# force a redraw of the Figure
# if self.y_max < 2:
# self.y_max = 2
# if self.y_min < 2:
# self.y_min = 0
if self.y_min > -.1 and self.y_max < .1:
temp_min = -1
temp_max = 1
else:
temp_min = self.y_min-(self.y_min/10)
temp_max = self.y_max+(self.y_max/10)
self.ax.set_ylim(temp_min, temp_max)
if self.timeSec >= self.x_lim:
if str(self.x_lim)[0]=='2':
self.x_lim = self.x_lim * 2.5
else:
self.x_lim = self.x_lim * 2
self.ax.set_xlim(0, self.x_lim)
# self.fig.canvas.restore_region(self.fig.canvas)
# self.ax.draw_artist(self.l_user[lines[0]])
# self.fig.canvas.blit(self.ax.bbox)
self.fig.canvas.draw()
# self.draw()
self.placeHolder.append(None)
class List(QtGui.QListWidget):
def __init__(self, parent):
super(List, self).__init__(parent)
font = QtGui.QFont()
font.setFamily(_fromUtf8("Century Gothic"))
font.setPointSize(7)
self.setFont(font)
self.setDragDropMode(4)
self.setAcceptDrops(True)
self.row = []
self.col = []
self.disName = []
self.lines = []
self.counter = 0
self.setStyleSheet("background-color:#DDDDDD")
self.colors = ["blue", "green", "red", "deeppink", "black", "slategray", "sienna", "goldenrod", "teal", "orange", "orchid", "lightskyblue", "navy", "darkgreen", "indigo", "firebrick", "deepskyblue", "lightskyblue", "darkseagreen", "gold"]
def dragEnterEvent(self, e):
if e.mimeData().hasFormat("application/x-qabstractitemmodeldatalist"):
# print "currentRow : ", self.currentRow()
# print "self.col: ", self.col
# print "self.row: ", self.row
# print "self.col[]: ", self.col.pop(self.currentRow())
# print "self.row[]: ", self.row.pop(self.currentRow())
self.col.pop(self.currentRow())
self.row.pop(self.currentRow())
self.disName.pop(self.currentRow())
self.lines.pop(self.currentRow())
self.takeItem(self.currentRow())
if e.mimeData().hasFormat("application/pubmedrecord"):
e.accept()
else:
e.ignore()
def dropEvent(self, e):
items = 0
data = e.mimeData()
bstream = data.retrieveData("application/pubmedrecord", QVariant.ByteArray)
selected = pickle.loads(bstream.toByteArray())
e.accept()
# print selected
# if self.count() != 0:
# j = (self.lines[self.count()-1]%len(self.colors))+1
# else:
# j=0
while items < len(selected):
j=self.counter
if j >= len(self.colors)-1:
j = self.counter%len(self.colors)
m = len(self.lines)
self.lines.append(self.counter)
# if m != 0:
# n = self.lines[m-1]
# self.lines.append(n+1)
# else:
# self.lines.append(0)
self.col.append(str(selected[items]))
items = items+1
self.row.append(str(selected[items]))
items = items+1
self.disName.append(str(selected[items]))
listItem = QtGui.QListWidgetItem()
listItem.setText(str(selected[items]))
listItem.setTextColor(QtGui.QColor(self.colors[j]))
self.addItem(listItem)
items = items+1
self.counter += 1
def dragLeaveEvent(self, event):
event.accept()
class PlotDlg(QtGui.QDialog):
NextID = 0
filename = 'Plot'
def __init__(self,time, callback, parent=None):
super(PlotDlg, self).__init__(parent)
self.id = PlotDlg.NextID
PlotDlg.NextID += 1
self.callback = callback
self.setWindowFlags(Qt.Window | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint)
self.setAttribute(Qt.WA_DeleteOnClose,True)
self.value = []
print "time=",time
self.time = time
self.dc = Monitor(self.time)
# self.threadPool = []
self.listWidget = List(self)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.MinimumExpanding)
sizePolicy.setHorizontalStretch(0)
self.listWidget.setSizePolicy(sizePolicy)
self.listWidget.setMaximumSize(QSize(150, 16777215))
grid = QtGui.QGridLayout()
grid.setSpacing(0)
grid.setContentsMargins(0, 0, 0, 0)
grid.addWidget(self.dc.mpl_toolbar,0,0,1,12)
grid.addWidget(self.listWidget,1,1)
grid.addWidget(self.dc,1,0)
grid.setColumnMinimumWidth(1,110)
self.setLayout(grid)
def update(self, clear=0):
if clear == 1:
now=dt.datetime.fromtimestamp(time.mktime(time.localtime()))
self.dc.timenum = now.strftime("%H:%M:%S.%f")
self.dc.timeSec = 0
self.dc.x_lim = 100
self.dc.y_max = 0
self.dc.y_min = 100
del self.dc.timeb[:]
del self.dc.user[:]
del self.dc.placeHolder[:]
# del self.dc.l_user[:]
# self.dc.l_user = [[] for x in xrange(50)]
# for i in range(50):
# self.dc.l_user[i], = self.dc.ax.plot(0,0)
for i in range(50):
self.dc.l_user[i].set_data(0, 0)
# print self.dc.l_user
# print self.dc.user
self.dc.ax.set_xlim(0, self.dc.x_lim)
self.dc.fig.canvas.draw()
# print self.value
# print str(self.time)
# print "time:",str(self.time)
# self.threadPool.append( GenericThread(self.dc.timerEvent,None, str(self.time), self.value, self.listWidget.lines) )
# self.threadPool[len(self.threadPool)-1].start()
self.dc.timerEvent(None, str(self.time), self.value, self.listWidget.lines)
def closeEvent(self, event):
# self.update(1)
self.callback(self.id)
PlotDlg.NextID -= 1
class GenericThread(QThread):
def __init__(self, function, *args, **kwargs):
QThread.__init__(self)
self.function = function
self.args = args
self.kwargs = kwargs
def __del__(self):
self.wait()
def run(self):
self.function(*self.args,**self.kwargs)
return

The pyqtgraph website has a comparison of plotting libraries including matplotlib, chaco, and pyqwt. The summary is:
Matplotlib is the de-facto standard plotting library, but is not built for speed.
Chaco is built for speed but is difficult to install / deploy
PyQwt is currently abandoned
PyQtGraph is built for speed and easy to install

I've used matplotlib and PyQtGraph both extensively and for any sort of fast or 'real time' plotting I'd STRONGLY recommend PyQtGraph, (in one application I plot a data stream from an inertial sensor over a serial connection of 12 32-bit floats each coming in at 1 kHz and plot without noticeable lag.)
As previous folks have mentioned, installation of PyQtGraph is trivial, in my experience it displays and performs on both windows and linux roughly equivalently (minus window manager differences), and there's an abundance of demo code in the included examples to guide completion of almost any data plotting task.
The web documentation for PyQtGraph is admittedly less than desirable, but the source code is well commented and easy to read, couple that with well documented and diverse set of demo code and in my experience it far surpasses matplotlib in both ease of use and performance (even with the much more extensive online documentation for matplotlib).

I would suggest Chaco "... a package for building interactive and custom 2-D plots and visualizations." It can be integrated in Qt apps, though you can probably get higher frame rates from PyQwt.
I've actually used it to write an "app" (that's too big a word: it's not very fancy and it all fits in ~200 LOC) that gets data from a serial port and draws it (20 lines at over 20 fps, 50 at 15 fps, at full screen in my laptop).
Chaco documentation or online help weren't as comprehensive as matplotlib's, but I guess it will have improved and at any rate it was enough for me.
As a general advice, avoid drawing everything at every frame, ie., use the .set_data methods in both matplotlib and chaco. Also, here in stackoverflow there are some questions about making matplotlib faster.

Here is a way to do it using the animation function:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
fig, ax = plt.subplots()
data = np.zeros((32,100))
X = np.arange(data.shape[-1])
# Generate line plots
lines = []
for i in range(len(data)):
# Each plot each shifter upward
line, = ax.plot(X,i+data[i], color=".75")
lines.append(line)
# Set limits
ax.set_ylim(0,len(data))
ax.set_xlim(0,data.shape[-1]-1)
# Update function
def update(*args):
# Shift data left
data[:,:-1] = data[:,1:]
# Append new values
data[:,-1] = np.arange(len(data))+np.random.uniform(0,1,len(data))
# Update data
for i in range(len(data)):
lines[i].set_ydata(data[i])
ani = animation.FuncAnimation(fig, update,interval=10)
plt.show()

Related

Why does loading Freecad UI in new, "empty" document generate the error - : Active task dialog found

My research for this issue returned answers that confirmed that FreeCAD GUI (or Qt) does not allow multiple activeDialog instances and that while multiple dialogs could be implemented I do not need that, I only want to load one activeDialog.
As I have tried to call from a new empty document, I believe that there should be no existing control (e.i. activeDialog) conflicting with the instance I am trying to create. I am not aware of multiple calls in the code and I don't know of a method to show an existing activeDialog in a document.
In this project I am learning to use FreeCAD and pySide so am not familiar with all conventions, anomalies etc. I am using FreeCAD, pySide and python (3.10 for freeCAD) Macos 10.14. I have created UI's both with code and in Qt Designer and consistently get the same behavior. I have cut and pasted multiple examples and have gotten the same behavior. I know it's something simple, I simply haven't found it yet.
The code is being developed so is not pretty, I can refactor as I learn and can go further.
Error occurs at :
FreeCADGui.Control.showDialog(panel)
with:
<class 'RuntimeError'>:Active task dialog found
from PySide import QtGui, QtCore
import Part, PartGui
import FreeCAD as App
class OffsetCalc(QtGui.QDialog):
def __init__(self):
super(OffsetCalc, self).__init__()
self.initUI()
def __str__(self):
return "OffsetCalc([])"
def __str__(self):
return "intUI([])"
def initUI(self):
self.result = userCancelled
# setting font and size
# create our window
# define window xLoc,yLoc,xDim,yDim
self.setGeometry( 850, 550, 250, 250)
self.setWindowTitle("Ship Offset Calculator")
self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
# creating a label widget
# by default label will display at top left corner
# The beginning of the coordinate system is at the left top corner.
# The x values grow from left to right. The y values grow from top to bottom.
self.lSta = QtGui.QLabel("Station", self)
self.lSta.setFont('Ariel') # set to a non-proportional font
self.lSta.move(20, 20)
self.lSta = QtGui.QLabel("H.B/W.L.", self)
self.lSta.setFont('Ariel') # set to a non-proportional font
self.lSta.move(20, 50)
self.lSta = QtGui.QLabel("Feet", self)
self.lSta.setFont('Ariel') # set to a non-proportional font
self.lSta.move(20, 80)
self.lSta = QtGui.QLabel("Inches", self)
self.lSta.setFont('Ariel') # set to a non-proportional font
self.lSta.move(20, 110)
self.lSta = QtGui.QLabel("Eights", self)
self.lSta.setFont('Ariel') # set to a non-proportional font
self.lSta.move(20, 140)
# numeric input field
self.ista = QtGui.QLineEdit("Station", self)
self.ista.setInputMask("999")
#self.ista.setText("000")
self.ista.setFixedWidth(50)
self.ista.move(100, 20)
self.iwlht = QtGui.QLineEdit(self)
self.iwlht.setInputMask("999")
#self.iwlht.setText("000")
self.iwlht.setFixedWidth(50)
self.iwlht.move(100, 50)
self.ifeet = QtGui.QLineEdit(self)
self.ifeet.setInputMask("999")
#self.ifeet.setText("000")
self.ifeet.setFixedWidth(50)
self.ifeet.move(100, 80)
self.iinch = QtGui.QLineEdit(self)
self.iinch.setInputMask("999")
#self.iinch.setText("000")
self.iinch.setFixedWidth(50)
self.iinch.move(100, 110)
self.ieight = QtGui.QLineEdit(self)
self.ieight.setInputMask("999")
#self.ieight.setText("000")
self.ieight.setFixedWidth(50)
self.ieight.move(100, 140)
self.bok = QtGui.QPushButton("OK", self)
self.bok.clicked.connect(self.onbok)
self.bok.move(20, 200)
self.hbht = QtGui.QRadioButton("Calc H.B", self)
self.hbht.move(150, 205)
self.show()
def onbok(self):
sta = float(self.ista.text())
wlht = float(self.iwlht.text())
feet = float(self.ifeet.text())
inches = float(self.iinch.text())
eights = float(self.ieight.text())
inches8 = inches*8 # number of 1/8's in Inches column
dec_ft = (inches8 + eights)/96
total_ft = feet + dec_ft
# doc = App.activeDocument()
#p = Part.Point
p = App.ActiveDocument.addObject("Part::Vertex", "p1")
p.Y = sta # Station is always Y
if self.hbht.isChecked: #True =hb / False =ht
# use calced X
print("Using calc X")
p.X = total_ft
p.Z = wlht
else:
# use calced Z
print("Using calced Z")
p.X = wlht
p.Z = total_ft
App.ActiveDocument.recompute()
# print("Station = ", sta, "Height = ", wlht, "Feet = ", feet, "Inches = ", inches, "Eights + ", eights)
# print("Eights of Inches = ", inches8, "Dec Ft. = ", dec_ft, "Total = ", total_ft)
self.result = userOK
self.close()
doc=App.activeDocument()
p = Part.Point()
p.Y = sta # Station is always Y
if self.hbht.isChecked: #True =hb / False =ht
# use calced X
print("Using calc X")
p.X = total_ft
p.Z = wlht
else:
# use calced Z
print("Using calced Z")
p.X = wlht
p.Z = total_ft
doc.recompute()
def add_X_Point(self):
doc=App.activeDocument()
p = Part.Point
p.Y = Sta
p.X = hb
p.Z = total_ft
doc.recompute()
def onCancel(self):
self.result = userCancelled
self.close()
def onOk(self):
self.result = userOK
self.close()
def mousePressEvent(self, event):
# print mouse position, X & Y
print("X = ", event.pos().x())
print("Y = ", event.pos().y())
#
userCancelled = "Cancelled"
userOK = "OK"
form = OffsetCalc()
form.exec_()

Error message when running the codes in Jupyter notebook

I am trying to test out the accuracy of the images without using image augmentation. When I run both of the codes, I got an error shown below:
TypeError: 'NoneType' object is not callable
I found that the error occurs in the second code. I would like to know the reason on the cause of this error message, and how to resolve it. Attached below are my codes, which have to be run simultaneously. I am using Jupyter notebook for that. Thanks!
Code 1:
import torch
from torch import nn
from torch.autograd import Variable
import torch.nn.functional as F
import math
class CrossEntropyLabelSmooth(nn.Module):
"""Cross entropy loss with label smoothing regularizer.
Reference:
Szegedy et al. Rethinking the Inception Architecture for Computer Vision. CVPR 2016.
Equation: y = (1 - epsilon) * y + epsilon / K.
Args:
num_classes (int): number of classes.
epsilon (float): weight.
"""
def __init__(self, num_classes, epsilon=0.1, device='cpu'):
super(CrossEntropyLabelSmooth, self).__init__()
self.num_classes = num_classes
self.epsilon = epsilon
self.device = device
self.logsoftmax = nn.LogSoftmax(dim=1)
def forward(self, inputs, targets):
"""
Args:
inputs: prediction matrix (before softmax) with shape (batch_size, num_classes)
targets: ground truth labels with shape (num_classes)
"""
log_probs = self.logsoftmax(inputs)
# targets = torch.zeros(log_probs.size()).scatter_(1, targets.unsqueeze(1).data, 1)# for mldg da
targets = torch.zeros(log_probs.size()).scatter_(1, targets.unsqueeze(1).data.cpu(), 1)#for zzd
targets = targets.to(self.device)
targets = (1 - self.epsilon) * targets + self.epsilon / self.num_classes
loss = (-Variable(targets) * log_probs).mean(0).sum()
return loss
class TripletLoss(nn.Module):
"""Triplet loss with hard positive/negative mining.
Reference:
Hermans et al. In Defense of the Triplet Loss for Person Re-Identification. arXiv:1703.07737.
Code imported from https://github.com/Cysu/open-reid/blob/master/reid/loss/triplet.py.
Args:
margin (float): margin for triplet.
"""
def __init__(self, margin=0.3):
super(TripletLoss, self).__init__()
self.margin = margin
self.ranking_loss = nn.MarginRankingLoss(margin=margin)
def forward(self, inputs, targets):
"""
Args:
inputs: feature matrix with shape (batch_size, feat_dim)
targets: ground truth labels with shape (num_classes)
"""
n = inputs.size(0)
# Compute pairwise distance, replace by the official when merged
dist = torch.pow(inputs, 2).sum(dim=1, keepdim=True).expand(n, n)
dist = dist + dist.t()
dist.addmm_(1, -2, inputs, inputs.t())
dist = dist.clamp(min=1e-12).sqrt() # for numerical stability
# For each anchor, find the hardest positive and negative
mask = targets.expand(n, n).eq(targets.expand(n, n).t())
dist_ap, dist_an = [], []
for i in range(n):
dist_ap.append(dist[i][mask[i]].max().unsqueeze(0))
dist_an.append(dist[i][mask[i] == 0].min().unsqueeze(0))
dist_ap = torch.cat(dist_ap)
dist_an = torch.cat(dist_an)
# Compute ranking hinge loss
y = torch.ones_like(dist_an)
loss = self.ranking_loss(dist_an, dist_ap, y)
return loss
class CenterLoss(nn.Module):
"""Center loss.
Reference:
Wen et al. A Discriminative Feature Learning Approach for Deep Face Recognition. ECCV 2016.
Args:
num_classes (int): number of classes.
feat_dim (int): feature dimension.
"""
def __init__(self, num_classes=10, feat_dim=2048, device='cpu'):
super(CenterLoss, self).__init__()
self.num_classes = num_classes
self.feat_dim = feat_dim
self.device = device
self.centers = nn.Parameter(torch.randn(self.num_classes, self.feat_dim)).to(self.device)
def forward(self, x, labels):
"""
Args:
x: feature matrix with shape (batch_size, feat_dim).
labels: ground truth labels with shape (num_classes).
"""
batch_size = x.size(0)
distmat = torch.pow(x, 2).sum(dim=1, keepdim=True).expand(batch_size, self.num_classes) + \
torch.pow(self.centers, 2).sum(dim=1, keepdim=True).expand(self.num_classes, batch_size).t()
distmat.addmm_(1, -2, x, self.centers.t())
classes = torch.arange(self.num_classes).long()
classes = classes.to(self.device)
labels = labels.unsqueeze(1).expand(batch_size, self.num_classes)
mask = labels.data.eq(classes.expand(batch_size, self.num_classes))
dist = []
for i in range(batch_size):
value = distmat[i][mask[i]]
value = value.clamp(min=1e-12, max=1e+12) # for numerical stability
dist.append(value)
dist = torch.cat(dist)
loss = dist.mean()
return loss
Code 2:
# Code without data augmentation
import torch
import torch.nn as nn
from torchvision.datasets import ImageFolder
from torchvision import transforms
import torchvision.models as models
from torch.utils.data import Dataset, DataLoader
import os
import numpy as np
from tqdm import tqdm
from PIL import Image
class FoodDataset(Dataset):
def __init__(self, file, transform=None, mode='train'):
self.transforms = transform
self.mode = mode
with open(file, 'r') as f:
self.image_list = f.readlines()
def __len__(self):
return len(self.image_list)
def __getitem__(self, index):
label = None
if self.mode == 'train':
image, label = self.image_list[index].split('\n')[0].split('\t')
label = int(label)
else:
image = self.image_list[index].split('\n')[0]
image = Image.open(image).convert('RGB')
image = self.transforms(image)
if self.mode == 'train':
return image, label
else:
return image
#transforms_train = transforms.Compose([
# transforms.Resize((224, 224)),
# transforms.RandomHorizontalFlip(p=0.5),
# transforms.RandomVerticalFlip(p=0.5),
# transforms.Pad(10, 10),
# transforms.RandomRotation(45),
# transforms.RandomCrop((224, 224)),
# transforms.ColorJitter(brightness=0.5, contrast=0.5, saturation=0.5),
# transforms.ToTensor(),
# transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
# ])
#transforms_test = transforms.Compose([
# transforms.Resize((224, 224)),
# transforms.ToTensor(),
# transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
# ])
def evaluate(prediction, ground_truth):
num_correct = (np.array(prediction) == np.array(ground_truth)).sum()
return num_correct / len(prediction)
train_ds = FoodDataset('data/train.txt')
val_ds = FoodDataset('data/val.txt')
test_ds = FoodDataset('data/test.txt')
train_dl = DataLoader(train_ds, batch_size=32, shuffle=True)
val_dl = DataLoader(val_ds, batch_size=32, shuffle=True)
test_dl = DataLoader(test_ds, batch_size=32, shuffle=True)
num_classes = 5
train_model = models.resnet50(pretrained=True)
train_model.fc = nn.Linear(2048, num_classes)
output_dir = 'checkpoint'
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
ce_loss = CrossEntropyLabelSmooth(num_classes = num_classes, device = device)
optimizer = torch.optim.Adam(train_model.parameters(), lr=0.001)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.1)
for param in train_model.parameters():
param.requires_grad = False
for param in train_model.fc.parameters():
param.requires_grad = True
for i in range(5):
train_model.train()
train_model.to(device)
for img, label in tqdm(train_dl):
img = img.to(device)
label = label.to(device)
optimizer.zero_grad()
output= train_model(img)
loss = ce_loss(output, label)
loss.backward()
optimizer.step()
for param in train_model.parameters():
param.requires_grad = True
epoch = 100
highest_acc = {'epoch': 0, 'accuracy': 0}
for ep in range(epoch):
train_model.train()
train_model.to(device)
count = 0
running_loss = 0.0
validation_loss = 0.0
output_list = []
ground_truth_list = []
for img, label in tqdm(train_dl):
img = img.to(device)
label = label.to(device)
optimizer.zero_grad()
output= train_model(img)
loss = ce_loss(output, label)
count += 1
prediction = torch.argmax(output, dim=1)
output_list.extend(prediction.detach().cpu())
ground_truth_list.extend(label.cpu())
running_loss += loss.item()
loss.backward()
optimizer.step()
scheduler.step()
if ep % 10 == 0:
torch.save(train_model.state_dict(), output_dir + '/resnet50_' + str(ep) + '.pth')
accuracy = evaluate(output_list, ground_truth_list)
print(f'Epoch[{ep}] training accuracy: {accuracy} '
f'training loss: {running_loss / count:.3e} Base Lr: {optimizer.param_groups[0]["lr"]:.5e}')
if ep % 10 == 0:
train_model.eval()
count = 0
output_list = []
ground_truth_list = []
for img, label in tqdm(val_dl):
with torch.no_grad():
img = img.to(device)
lbl = label.to(device)
output= train_model(img)
val_loss = ce_loss(output, lbl)
validation_loss += val_loss.item()
count += 1
prediction = torch.argmax(output, dim=1)
output_list.extend(prediction.detach().cpu())
ground_truth_list.extend(label)
accuracy = evaluate(output_list, ground_truth_list)
if accuracy > highest_acc['accuracy']:
highest_acc['accuracy'] = accuracy
highest_acc['epoch'] = ep
print(f'Accuracy: {accuracy} Epoch:{ep}')
torch.save(train_model.state_dict(), output_dir + '/resnet50_' + 'final' + '.pth')
print('highest_acc: {} epoch: {}'.format(highest_acc['accuracy'], highest_acc['epoch']))

bokeh selected.on_change not working for my current setup

Basically, this is an interactive heatmap but the twist is that the source is updated by reading values from a file that gets updated regularly.
dont bother about the class "generator", it is just for keeping data and it runs regularly threaded
make sure a file named "Server_dump.txt" exists in the same directory of the script with a single number greater than 0 inside before u execute the bokeh script.
what basically happens is i change a number inside the file named "Server_dump.txt" by using echo 4 > Server_dump.txt on bash,
u can put any number other than 4 and the script automatically checks the file and plots the new point.
if u don't use bash, u could use a text editor , replace the number and save, and all will be the same.
the run function inside the generator class is the one which checks if this file was modified , reads the number, transforms it into x& y coords and increments the number of taps associated with these coords and gives the source x,y,taps values based on that number.
well that function works fine and each time i echo a number , the correct rectangle is plotted but,
now I want to add the functionality of that clicking on a certain rectangle triggers a callback to plot a second graph based on the coords of the clicked rectangle but i can't even get it to trigger even though i have tried other examples with selected.on_change in them and they worked fine.
*if i increase self.taps for a certain rect by writing the number to the file multiple times, color gets updated but if i hover over the rect it shows me the past values and not the latest value only .
my bokeh version is 1.0.4
from functools import partial
from random import random,randint
import threading
import time
from tornado import gen
from os.path import getmtime
from math import pi
import pandas as pd
from random import randint, random
from bokeh.io import show
from bokeh.models import LinearColorMapper, BasicTicker, widgets, PrintfTickFormatter, ColorBar, ColumnDataSource, FactorRange
from bokeh.plotting import figure, curdoc
from bokeh.layouts import row, column, gridplot
source = ColumnDataSource(data=dict(x=[], y=[], taps=[]))
doc = curdoc()
#sloppy data receiving function to change data to a plottable shape
class generator(threading.Thread):
def __init__(self):
super(generator, self).__init__()
self.chart_coords = {'x':[],'y':[],'taps':[]}
self.Pi_coords = {}
self.coord = 0
self.pos = 0
self.col = 0
self.row = 0
self.s = 0
self.t = 0
def chart_dict_gen(self,row, col):
self.col = col
self.row = row+1
self.chart_coords['x'] = [i for i in range(1,cla.row)]
self.chart_coords['y'] = [i for i in range(cla.col, 0, -1)] #reversed list because chart requires that
self.chart_coords['taps']= [0]*(row * col)
self.taps = [[0 for y in range(col)] for x in range(row)]
def Pi_dict_gen(self,row,col):
key = 1
for x in range(1,row):
for y in range(1,col):
self.Pi_coords[key] = (x,y)
key = key + 1
def Pi_to_chart(self,N):
x,y = self.Pi_coords[N][0], self.Pi_coords[N][1]
return x,y
def run(self):
while True:
if(self.t == 0):
self.t=1
continue
time.sleep(0.1)
h = getmtime("Server_dump.txt")
if self.s != h:
self.s = h
with open('Server_dump.txt') as f:
m = next(f)
y,x = self.Pi_to_chart(int(m))
self.taps[x][y] += 1
# but update the document from callback
doc.add_next_tick_callback(partial(update, x=x, y=y, taps=self.taps[x][y]))
cla = generator()
cla.chart_dict_gen(15,15)
cla.Pi_dict_gen(15, 15)
x = cla.chart_coords['x']
y = cla.chart_coords['y']
taps = cla.chart_coords['taps']
#gen.coroutine
def update(x, y, taps):
taps += taps
print(x,y,taps)
source.stream(dict(x=[x], y=[y], taps=[taps]))
colors = ["#CCEBFF","#B2E0FF","#99D6FF","#80CCFF","#66c2FF","#4DB8FF","#33ADFF","#19A3FF", "#0099FF", "#008AE6", "#007ACC","#006BB2", "#005C99", "#004C80", "#003D66", "#002E4C", "#001F33", "#000F1A", "#000000"]
mapper = LinearColorMapper(palette=colors, low= 0, high= 15) #low = min(cla.chart_coords['taps']) high = max(cla.chart_coords['taps'])
TOOLS = "hover,save,pan,box_zoom,reset,wheel_zoom"
p = figure(title="Tou",
x_range=list(map(str,x)),
y_range=list(map(str,reversed(y))),
x_axis_location="above",
plot_width=900, plot_height=400,
tools=TOOLS, toolbar_location='below',
tooltips=[('coords', '#y #x'), ('taps', '#taps%')])
p.grid.grid_line_color = "#ffffff"
p.axis.axis_line_color = "#ef4723"
p.axis.major_tick_line_color = "#af0a36"
p.axis.major_label_text_font_size = "7pt"
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = None
p.rect(x="x", y="y",
width=0.9, height=0.9,
source=source,
fill_color={'field': 'taps', 'transform': mapper},
line_color = "#ffffff",
)
color_bar = ColorBar(color_mapper=mapper,
major_label_text_font_size="7pt",
ticker=BasicTicker(desired_num_ticks=len(colors)),
formatter=PrintfTickFormatter(format="%d%%"),
label_standoff=6, border_line_color=None, location=(0, 0))
curdoc().theme = 'dark_minimal'
def ck(attr, old, new):
print('here') #doesn't even print hi in the terminal if i click anywhere
source.selected.on_change('indices', ck)
p.add_layout(color_bar, 'right')
doc.add_root(p)
thread = cla
thread.start()
i wanted even to get a printed hi in the terminal but nothing
You have not actually added any selection tool at all to your plot, so no selection is ever made. You have specified:
TOOLS = "hover,save,pan,box_zoom,reset,wheel_zoom"
Those are the only tools that will be added, and none of them make selections, there for nothing will cause source.selection.indices to ever be updated. If you are looking for selections based on tap, you must add a TapTool, e.g. with
TOOLS = "hover,save,pan,box_zoom,reset,wheel_zoom,tap"
Note that there will not be repeated callbacks if you tap the same rect multiple times. The callback only fires when the selection changes and clicking the same glyph twice in a row results in an identical selection.

Patch glyph not updated when using multiple ColumnDataSources in bokeh app

I am trying to use the bokeh server to plot a time series together with a shaded percentile band around, and this, since bokeh does not support the fill_between function from matplotlib, requires the construction of a patch object of double dimension. Hence, I need two ColumnDataSources to hold the data. However, only the first curve is rendered correctly when the data changes. Although the data_source of the GlyphRenderer is updated, the figure does not change. I use bokeh 0.12.3, and have tried with several servers and browsers. A complete, and reasonably minimal example:
import numpy as np
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
from bokeh.layouts import column
from bokeh.io import curdoc
from bokeh.models.widgets import Select
class AppData:
def __init__(self, n):
self.p_source = None
self.c_source = None
self.x = np.linspace(0, 10, 20)
self.n = n
self.ys = [np.sin(self.x) - i for i in range(self.n)]
self.line = None
self.patch = None
def update_module(self, a, b):
assert b - a == 5
p_data = dict() if self.p_source is None else self.p_source.data
c_data = dict() if self.c_source is None else self.c_source.data
ys = [self.ys[j] for j in range(a, b)]
if "x" not in c_data:
c_data["x"] = self.x
p_data["x"] = c_data["x"].tolist() + c_data["x"][::-1].tolist()
n_r = len(ys[0])
n_p = 2*n_r
if "ys" not in p_data:
p_data["ys"] = np.empty((n_p))
p_data["ys"][:n_r] = ys[0]
p_data["ys"][n_r:] = np.flipud(ys[-1])
c_data["y"] = ys[2]
if self.p_source is None:
self.p_source = ColumnDataSource(data=p_data)
else:
self.p_source.data.update(p_data)
if self.c_source is None:
self.c_source = ColumnDataSource(data=c_data)
else:
self.c_source.data.update(c_data)
if self.line is not None:
print(max(self.line.data_source.data["y"]))
print(max(self.patch.data_source.data["ys"])) # The value changes, but the figure does not!
# initialize
app_data = AppData(10)
app_data.update_module(4, 4 + 5)
s1 = figure(width=500, plot_height=125, title=None, toolbar_location="above")
app_data.line = s1.line("x", "y", source=app_data.c_source)
app_data.patch = s1.patch("x", "ys", source=app_data.p_source, alpha=0.3, line_width=0)
select = Select(title="Case", options=[str(i) for i in range(5)], value="4")
def select_case(attrname, old, new):
a = int(select.value)
app_data.update_module(a, a + 5)
select.on_change('value', select_case)
layout = column(select, s1)
curdoc().add_root(layout)
curdoc().title = "Example of patches not being updated"
I am certainly not very experienced in using bokeh, so I could very well be using the system wrong. However, any help on this matter would be of great help!

PySide/PyQt truncate text in QLabel based on minimumSize

I am wondering how to best truncate text in a QLabel based on it's maximum width/height.
The incoming text could be any length, but in order to keep a tidy layout I'd like to truncate long strings to fill a maximum amount of space (widget's maximum width/height).
E.g.:
'A very long string where there should only be a short one, but I can't control input to the widget as it's a user given value'
would become:
'A very long string where there should only be a short one, but ...'
based on the required space the current font needs.
How can I achieve this best?
Here is a simple example of what I'm after, though this is based on word count, not available space:
import sys
from PySide.QtGui import *
from PySide.QtCore import *
def truncateText(text):
maxWords = 10
words = text.split(' ')
return ' '.join(words[:maxWords]) + ' ...'
app = QApplication(sys.argv)
mainWindow = QWidget()
layout = QHBoxLayout()
mainWindow.setLayout(layout)
text = 'this is a very long string, '*10
label = QLabel(truncateText(text))
label.setWordWrap(True)
label.setFixedWidth(200)
layout.addWidget(label)
mainWindow.show()
sys.exit(app.exec_())
Even easier - use the QFontMetrics.elidedText method and overload the paintEvent, here's an example:
from PyQt4.QtCore import Qt
from PyQt4.QtGui import QApplication,\
QLabel,\
QFontMetrics,\
QPainter
class MyLabel(QLabel):
def paintEvent( self, event ):
painter = QPainter(self)
metrics = QFontMetrics(self.font())
elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width())
painter.drawText(self.rect(), self.alignment(), elided)
if ( __name__ == '__main__' ):
app = None
if ( not QApplication.instance() ):
app = QApplication([])
label = MyLabel()
label.setText('This is a really, long and poorly formatted runon sentence used to illustrate a point')
label.setWindowFlags(Qt.Dialog)
label.show()
if ( app ):
app.exec_()
I found that #Eric Hulser's answer, while great, didn't work when the label was put into another widget.
I came up with this by hacking together Eric's response with the Qt Elided Label Example. It should behave just like a regular label, yet elide horizontally when the text width exceeds the widget width. It has an extra argument for different elide modes. I also wrote some tests for fun :)
If you want to use PyQt5...
Change "PySide2" to "PyQt5"
Change "Signal" to "pyqtSignal"
Enjoy!
Eliding Label
# eliding_label.py
from PySide2 import QtCore, QtWidgets, QtGui
class ElidingLabel(QtWidgets.QLabel):
"""Label with text elision.
QLabel which will elide text too long to fit the widget. Based on:
https://doc-snapshots.qt.io/qtforpython-5.15/overviews/qtwidgets-widgets-elidedlabel-example.html
Parameters
----------
text : str
Label text.
mode : QtCore.Qt.TextElideMode
Specify where ellipsis should appear when displaying texts that
don’t fit.
Default is QtCore.Qt.ElideMiddle.
Possible modes:
QtCore.Qt.ElideLeft
QtCore.Qt.ElideMiddle
QtCore.Qt.ElideRight
parent : QWidget
Parent widget. Default is None.
f : Qt.WindowFlags()
https://doc-snapshots.qt.io/qtforpython-5.15/PySide2/QtCore/Qt.html#PySide2.QtCore.PySide2.QtCore.Qt.WindowType
"""
elision_changed = QtCore.Signal(bool)
def __init__(self, text='', mode=QtCore.Qt.ElideMiddle, **kwargs):
super().__init__(**kwargs)
self._mode = mode
self.is_elided = False
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
self.setText(text)
def setText(self, text):
self._contents = text
# This line set for testing. Its value is the return value of
# QFontMetrics.elidedText, set in paintEvent. The variable
# must be initialized for testing. The value should always be
# the same as contents when not elided.
self._elided_line = text
self.update()
def text(self):
return self._contents
def paintEvent(self, event):
super().paintEvent(event)
did_elide = False
painter = QtGui.QPainter(self)
font_metrics = painter.fontMetrics()
text_width = font_metrics.horizontalAdvance(self.text())
# layout phase
text_layout = QtGui.QTextLayout(self._contents, painter.font())
text_layout.beginLayout()
while True:
line = text_layout.createLine()
if not line.isValid():
break
line.setLineWidth(self.width())
if text_width >= self.width():
self._elided_line = font_metrics.elidedText(self._contents, self._mode, self.width())
painter.drawText(QtCore.QPoint(0, font_metrics.ascent()), self._elided_line)
did_elide = line.isValid()
break
else:
line.draw(painter, QtCore.QPoint(0, 0))
text_layout.endLayout()
if did_elide != self.is_elided:
self.is_elided = did_elide
self.elision_changed.emit(did_elide)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
long_text = "this is some long text, wouldn't you say?"
elabel = ElidingLabel(long_text)
elabel.show()
app.exec_()
Test Eliding Label
# test_eliding_label.py.py
#
# Run tests with
#
# python3 -m unittest test_eliding_label.py --failfast --quiet
import unittest
import unittest.mock
from PySide2 import QtCore, QtWidgets, QtGui, QtTest
import eliding_label
if not QtWidgets.QApplication.instance():
APP = QtWidgets.QApplication([]) # pragma: no cover
class TestElidingLabelArguments(unittest.TestCase):
def test_optional_text_argument(self):
elabel = eliding_label.ElidingLabel()
self.assertEqual(elabel.text(), "")
def test_text_argument_sets_label_text(self):
elabel = eliding_label.ElidingLabel(text="Test text")
self.assertEqual(elabel.text(), "Test text")
def test_optional_elision_mode_argument(self):
elabel = eliding_label.ElidingLabel()
self.assertEqual(elabel._mode, QtCore.Qt.ElideMiddle)
class TestElidingLabel(unittest.TestCase):
def setUp(self):
self.elabel = eliding_label.ElidingLabel()
def test_elabel_is_a_label(self):
self.assertIsInstance(self.elabel, QtWidgets.QLabel)
def test_has_elision_predicate(self):
self.assertEqual(self.elabel.is_elided, False)
def test_elision_predicate_changes_when_text_width_exceeds_widget_width(self):
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
self.elabel.setFixedWidth(25)
self.assertEqual(self.elabel.width(), 25)
long_text = "This is line is definely longer than 25 pixels."
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
long_text_width = font_metrics.horizontalAdvance(long_text)
self.assertGreater(long_text_width, 25)
self.elabel.setText(long_text)
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
self.assertEqual(self.elabel.is_elided, True)
def test_text_is_elided_when_text_width_exceeds_widget_width(self):
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
self.elabel.setFixedWidth(25)
self.assertEqual(self.elabel.width(), 25)
long_text = "This is line is definely longer than 25 pixels."
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
long_text_width = font_metrics.horizontalAdvance(long_text)
self.assertGreater(long_text_width, 25)
self.elabel.setText(long_text)
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
# PySide2.QtGui.QFontMetrics.elidedText states, "If the string
# text is wider than width , returns an elided version of the
# string (i.e., a string with '…' in it). Otherwise, returns
# the original string."
self.assertEqual(self.elabel._elided_line, '…')
def test_text_is_not_elided_when_text_width_is_less_than_widget_width(self):
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
self.elabel.setFixedWidth(500)
self.assertEqual(self.elabel.width(), 500)
short_text = "Less than 500"
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
short_text_width = font_metrics.horizontalAdvance(short_text)
self.assertLess(short_text_width, 500)
self.elabel.setText(short_text)
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
# PySide2.QtGui.QFontMetrics.elidedText states, "If the string
# text is wider than width , returns an elided version of the
# string (i.e., a string with '…' in it). Otherwise, returns
# the original string."
self.assertEqual(self.elabel._elided_line, short_text)
def test_stores_full_text_even_when_elided(self):
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
self.elabel.setFixedWidth(25)
self.assertEqual(self.elabel.width(), 25)
long_text = "This is line is definely longer than 25 pixels."
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
long_text_width = font_metrics.horizontalAdvance(long_text)
self.assertGreater(long_text_width, 25)
self.elabel.setText(long_text)
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
# PySide2.QtGui.QFontMetrics.elidedText states, "If the string
# text is wider than width , returns an elided version of the
# string (i.e., a string with '…' in it). Otherwise, returns
# the original string."
self.assertEqual(self.elabel._elided_line, '…')
self.assertEqual(self.elabel.text(), long_text)
def test_has_elision_changed_signal(self):
self.assertIsInstance(self.elabel.elision_changed, QtCore.Signal)
def test_elision_changed_signal_emits_on_change_to_is_elided_predicate(self):
mock = unittest.mock.Mock()
self.elabel.elision_changed.connect(mock.method)
# NOTE: This is a bit of a stretch, inducing a paint event
# when the event loop isn't running. Throws a bunch of C++
# sourced text which can't be (easily) caught.
# Induce elision
self.elabel.setFixedWidth(150)
self.assertEqual(self.elabel.width(), 150)
long_text = "This line is definitely going to be more than 150 pixels"
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
long_text_width = font_metrics.horizontalAdvance(long_text)
self.assertGreater(long_text_width, 150)
self.elabel.setText(long_text)
self.assertEqual(self.elabel.is_elided, False) # no elide until painting
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
self.assertEqual(self.elabel.is_elided, True)
mock.method.assert_called_once()
# Remove elision
short_text = "Less than 150"
painter = QtGui.QPainter()
font_metrics = painter.fontMetrics()
short_text_width = font_metrics.horizontalAdvance(short_text)
self.assertLess(short_text_width, 150)
self.elabel.setText(short_text)
self.assertEqual(self.elabel.is_elided, True) # still elided until painting
x = self.elabel.x()
y = self.elabel.y()
w = self.elabel.width()
h = self.elabel.height()
paint_event = QtGui.QPaintEvent(QtGui.QRegion(x, y, w, h))
self.elabel.paintEvent(paint_event)
self.assertEqual(self.elabel.is_elided, False)
self.assertEqual(mock.method.call_count, 2)
You can achieves this through determining the width with QFontMetrics, see this answer.
You would probably want to use or create some algorithm which finds the place to cut quickly, unless doing it in a simple for loop would be sufficient.
simpler solution if you want show QLabel in center in provided area
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.minimumSizeHint = lambda self=label: QSize(0, QLabel.minimumSizeHint(self).height() )

Resources