How do I make it possible to change the scale and recalculate the temperature? - properties

So, I want to create a class that describes temperature and allows users to set and get temperatures on the Celsius and Fahrenheit scales, and data can be placed on one scale and retrieved on the other.
class Temperature:
def __init__(self, location, temperature, scale):
self.__location = location
self.__temperature = temperature
self.__scale = scale
#classmethod
def verify_location(cls, location):
if type(location) != str:
raise TypeError('Location name written incorrectly')
#classmethod
def verify_temperature(cls, temperature):
if type(temperature) != int and type(temperature) != float:
raise TypeError('Temperature parameter must be int or float')
#classmethod
def verify_scale(cls, scale):
if scale != 'C' and scale != 'F':
raise TypeError('System must be "C"(Celsium) or "F"(Fahrenheit)')
elif scale == 'C':
return scale
elif scale == 'F':
temperature == float((temperature * 9 / 5) + 32)
#property
def location(self):
return self.__location
#location.setter
def location(self, location):
self.__location = location
#location.deleter
def location(self):
del self.__location
#property
def temperature(self):
return self.__temperature
#temperature.setter
def temperature(self, temperature):
self.__temperature = temperature
#temperature.deleter
def temperature(self):
del self.__temperature
#property
def scale(self):
return self.__scale
#scale.setter
def scale(self, scale):
self.__scale = scale
#scale.deleter
def scale(self):
del self.__scale
t_kyiv = Temperature('Kyiv', 4, 'C')
print(t_kyiv.temperature)
t_kyiv.scale = 'F'
print(t_kyiv.temperature)
the problem is that formula i was trying to put
#classmethod
def verify_scale(cls, scale):
if scale != 'C' and scale != 'F':
raise TypeError('System must be "C"(Celsium) or "F"(Fahrenheit)')
elif scale == 'C':
return scale
elif scale == 'F':
# temperature == float((temperature * 9 / 5) + 32)

Related

Why does QThread freezes Gui Thread while working?

I need to keep updating the table to have up-to-date information. To do this, I created a Thread and implemented an infinite loop in it, which iterates through all the values and compares them, but for some reason the work of this thread affects GUI and as a result, the thread with GUI freezes.
import random
import sys
import pandas as pd
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import QObject, pyqtSignal, Qt, QThread, QModelIndex
from PyQt5.QtWidgets import QApplication, QMainWindow
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, header: list):
super(TableModel, self).__init__()
self._data = pd.DataFrame(columns=header)
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
if role == Qt.DisplayRole:
return self._data.values[index.row()][index.column()]
return None
def rowCount(self, parent: QModelIndex = ...) -> int:
return len(self._data.values)
def columnCount(self, index):
return self._data.columns.size
def headerData(self, section, orientation, role):
# section is the index of the column/row.
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._data.columns[section])
if orientation == Qt.Vertical:
return str(self._data.index[section])
def setData(self, index, value, role):
if not index.isValid():
return False
row = index.row()
if row < 0 or row >= len(self._data.values):
return False
column = index.column()
if column < 0 or column >= self._data.columns.size:
return False
self._data.iloc[row, column] = value
self.dataChanged.emit(index, index)
return True
def removeRows(self, position, rows, parent=QModelIndex()):
start, end = position, position + rows - 1
if 0 <= start <= end < self.rowCount(parent):
self.beginRemoveRows(parent, start, end)
for index in range(start, end + 1):
self._data.drop(index, inplace=True)
self._data.reset_index(drop=True, inplace=True)
self.endRemoveRows()
return True
return False
def insertRows(self, position, rows, parent=QModelIndex()):
start, end = position, position + rows - 1
if 0 <= start <= end:
self.beginInsertRows(parent, start, end)
for index in range(start, end + 1):
default_row = [[None] for _ in range(self._data.shape[1])]
new_df = pd.DataFrame(dict(zip(list(self._data.columns), default_row)))
self._data = pd.concat([self._data, new_df])
self._data = self._data.reset_index(drop=True)
self.endInsertRows()
return True
return False
def addRow(self, item):
self.insertRows(self.rowCount(), 1)
row = self.rowCount() - 1
self.setData(self.index(row, 0), item[0], Qt.DisplayRole)
self.setData(self.index(row, 1), item[1], Qt.DisplayRole)
self.setData(self.index(row, 2), item[2], Qt.DisplayRole)
self.setData(self.index(row, 3), item[3], Qt.DisplayRole)
self.setData(self.index(row, 4), item[4], Qt.DisplayRole)
def getTable(self):
return self._data
class Updater(QObject):
addRow = pyqtSignal(object)
def __init__(self, table: TableModel):
super().__init__()
self.items = []
for i in range(50):
item = [
random.randint(10, 100),
random.randint(10, 100),
random.randint(10, 100),
random.randint(10, 100),
random.randint(10, 100),
]
self.items.append(item)
self._table = table
self._status = False
def start(self):
self._status = True
if self._table.getTable().empty:
items = self.items
for item in items:
self.addRow.emit(item)
while self._status:
self.Update(self.items)
def Update(self, items: list):
# This function is looking for items in table and changes data if its not up-to-date
data = self._table.getTable()
for item in items:
result = data.loc[
(data['Col1'] == item[0]) &
(data['Col2'] == item[1]) &
(data['Col3'] == item[2])
]
if len(result.values) != 0:
pass
# Code ...
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setObjectName("MainWindow")
self.resize(800, 600)
self.centralwidget = QtWidgets.QWidget(self)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout_2.setObjectName("gridLayout_2")
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
self.tableWidget = QtWidgets.QTableView(self.centralwidget)
self.tableWidget.setObjectName("tableWidget")
self.gridLayout.addWidget(self.tableWidget, 0, 0, 1, 1)
self.gridLayout_2.addLayout(self.gridLayout, 0, 0, 1, 1)
self.setCentralWidget(self.centralwidget)
header = ["Col1", "Col2", "Col3", "Col4", "Col5"]
self.model = TableModel(header)
self.tableWidget.setModel(self.model)
self._updater = Updater(self.model)
self._thread = QThread()
self._updater.moveToThread(self._thread)
self._thread.started.connect(self._updater.start)
self._thread.finished.connect(self._thread.deleteLater)
self._updater.addRow.connect(self.model.addRow, Qt.QueuedConnection)
self.show()
self._thread.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
Do not judge strictly. I've only recently started learning pyqt and I don't really understand it yet.Thank you in advance!

Synchronize two QGraphicsView with different images

I would like to show two images next to each other, such that when I zoom or pan on one image the other image follows along. My current approach is to emit a viewUpdated event after resolving mouse events. The event contains the viewportTransformation and is used to update the transform in the other view. This sort of works for the zoom part, but panning does not work.
The code below is based on the qt5 version provided in this answer: https://stackoverflow.com/a/35514531/185475
from PyQt5 import QtCore, QtGui, QtWidgets
# Code from https://stackoverflow.com/a/35514531
class PhotoViewer(QtWidgets.QGraphicsView):
photoClicked = QtCore.pyqtSignal(QtCore.QPoint)
viewUpdated = QtCore.pyqtSignal(QtGui.QTransform)
def __init__(self, parent):
super(PhotoViewer, self).__init__(parent)
self._zoom = 0
self._empty = True
self._scene = QtWidgets.QGraphicsScene(self)
self._photo = QtWidgets.QGraphicsPixmapItem()
self._scene.addItem(self._photo)
self.setScene(self._scene)
self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(30, 30, 30)))
self.setFrameShape(QtWidgets.QFrame.NoFrame)
def hasPhoto(self):
return not self._empty
def fitInView(self, scale=True):
rect = QtCore.QRectF(self._photo.pixmap().rect())
if not rect.isNull():
self.setSceneRect(rect)
if self.hasPhoto():
unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
self.scale(1 / unity.width(), 1 / unity.height())
viewrect = self.viewport().rect()
scenerect = self.transform().mapRect(rect)
factor = min(viewrect.width() / scenerect.width(),
viewrect.height() / scenerect.height())
self.scale(factor, factor)
self._zoom = 0
def setPhoto(self, pixmap=None):
self._zoom = 0
if pixmap and not pixmap.isNull():
self._empty = False
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
self._photo.setPixmap(pixmap)
else:
self._empty = True
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
self._photo.setPixmap(QtGui.QPixmap())
self.fitInView()
def wheelEvent(self, event):
if self.hasPhoto():
if event.angleDelta().y() > 0:
factor = 1.25
self._zoom += 1
else:
factor = 0.8
self._zoom -= 1
if self._zoom > 0:
self.scale(factor, factor)
elif self._zoom == 0:
self.fitInView()
else:
self._zoom = 0
self.viewUpdated.emit(self.viewportTransform())
def toggleDragMode(self):
if self.dragMode() == QtWidgets.QGraphicsView.ScrollHandDrag:
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
elif not self._photo.pixmap().isNull():
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
def mousePressEvent(self, event):
if self._photo.isUnderMouse():
self.photoClicked.emit(self.mapToScene(event.pos()).toPoint())
super(PhotoViewer, self).mousePressEvent(event)
self.viewUpdated.emit(self.viewportTransform())
def set_transform(self, transform):
self.setTransform(transform)
class Window(QtWidgets.QWidget):
def __init__(self):
super(Window, self).__init__()
self.viewer = PhotoViewer(self)
self.viewerSecondImage = PhotoViewer(self)
self.viewer.viewUpdated.connect(self.viewerSecondImage.set_transform)
self.viewerSecondImage.viewUpdated.connect(self.viewer.set_transform)
# 'Load image' button
self.btnLoad = QtWidgets.QToolButton(self)
self.btnLoad.setText('Load image')
self.btnLoad.clicked.connect(self.loadImage)
# Button to change from drag/pan to getting pixel info
self.btnPixInfo = QtWidgets.QToolButton(self)
self.btnPixInfo.setText('Enter pixel info mode')
self.btnPixInfo.clicked.connect(self.pixInfo)
self.editPixInfo = QtWidgets.QLineEdit(self)
self.editPixInfo.setReadOnly(True)
self.viewer.photoClicked.connect(self.photoClicked)
# Arrange layout
VBlayout = QtWidgets.QVBoxLayout(self)
HBlayoutImageViewers = QtWidgets.QHBoxLayout()
HBlayoutImageViewers.addWidget(self.viewer)
HBlayoutImageViewers.addWidget(self.viewerSecondImage)
VBlayout.addLayout(HBlayoutImageViewers)
HBlayout = QtWidgets.QHBoxLayout()
HBlayout.setAlignment(QtCore.Qt.AlignLeft)
HBlayout.addWidget(self.btnLoad)
HBlayout.addWidget(self.btnPixInfo)
HBlayout.addWidget(self.editPixInfo)
VBlayout.addLayout(HBlayout)
def loadImage(self):
self.viewer.setPhoto(QtGui.QPixmap('input/490px-Dostojka_adype.jpg'))
self.viewerSecondImage.setPhoto(QtGui.QPixmap('input/490px-Dostojka_adype.jpg'))
def pixInfo(self):
self.viewer.toggleDragMode()
def photoClicked(self, pos):
if self.viewer.dragMode() == QtWidgets.QGraphicsView.NoDrag:
self.editPixInfo.setText('%d, %d' % (pos.x(), pos.y()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 800, 600)
window.show()
sys.exit(app.exec_())
You only need to syncronize scrollbar values of two QGraphicsViews
def bindScrollBars(scrollBar1, scrollBar2):
# syncronizing scrollbars syncrnonously somehow breaks zooming and doesn't work
# scrollBar1.valueChanged.connect(lambda value: scrollBar2.setValue(value))
# scrollBar2.valueChanged.connect(lambda value: scrollBar1.setValue(value))
# syncronizing scrollbars asyncronously works ok
scrollBar1.valueChanged.connect(lambda _: QtCore.QTimer.singleShot(0, lambda: scrollBar2.setValue(scrollBar1.value())))
scrollBar2.valueChanged.connect(lambda _: QtCore.QTimer.singleShot(0, lambda: scrollBar1.setValue(scrollBar2.value())))
class Window(QtWidgets.QWidget):
def __init__(self):
...
bindScrollBars(self.viewer.horizontalScrollBar(), self.viewerSecondImage.horizontalScrollBar())
bindScrollBars(self.viewer.verticalScrollBar(), self.viewerSecondImage.verticalScrollBar())
Also wheelEvent can be simplified
def wheelEvent(self, event):
if self.hasPhoto():
factor = 1.25
if event.angleDelta().y() > 0:
self.scale(factor, factor)
else:
self.scale(1/factor, 1/factor)
self.viewUpdated.emit(self.transform())

Astropy Units equivalencies - interferometry baselines using classes and SI prefixes

As a following of Astropy Units equivalencies - interferometry baselines. I would like to ask about how I can use SI prefixes for my custom-defined unit. Until now, and following the recommendations from the attached link, I made a subclass from astropy Quantity and then I have overridden the .to method.
Here is my code:
class uWavelength(un.Quantity):
def __new__(cls, value, freq=None, dtype=None, copy=True, **kwargs):
unit = un.Unit(un.def_unit('lambdas', format={'format': r'\lambda'}, prefixes=True))
self = super().__new__(cls, value=value, unit=unit, dtype=dtype, copy=copy, **kwargs)
self.freq = freq
if self.freq is not None:
self.equivalencies = self.lambdas_equivalencies()
return self
#property
def freq(self):
return self._freq
#freq.setter
def freq(self, val):
if val is not None:
self._equivalencies = self.lambdas_equivalencies(restfreq=val)
self._freq = val
#property
def equivalencies(self):
return self._equivalencies
#equivalencies.setter
def equivalencies(self, val):
self._equivalencies = val
def lambdas_equivalencies(self, restfreq=None):
if self.freq is not None:
restfreq_hz = self.freq.to(un.Hz, equivalencies=un.spectral())
elif restfreq is not None:
restfreq_hz = restfreq.to(un.Hz, equivalencies=un.spectral())
else:
sys.exit("Frequency not provided")
eq = [
(self.unit, un.s, lambda x: x / restfreq_hz, lambda x: x * restfreq_hz),
(self.unit, un.m, lambda x: x / restfreq_hz * co.c.to(un.m / un.s).value,
lambda x: x / co.c.to(un.m / un.s).value * restfreq_hz),
(un.m, un.s, lambda x: x / co.c.to(un.m / un.s).value, lambda x: x * co.c.to(un.m / un.s).value),
]
return eq
def to(self, unit, restfreq=None, copy=True):
equiv = []
if restfreq is None:
equiv = self.equivalencies
else:
equiv = self.lambdas_equivalencies(restfreq=restfreq)
unit = un.Unit(unit)
if copy:
# Avoid using to_value to ensure that we make a copy. We also
# don't want to slow down this method (esp. the scalar case).
value = self._to_value(unit, equiv)
else:
# to_value only copies if necessary
value = self.to_value(unit, equiv)
return self._new_view(value, unit)class uWavelength(un.Quantity):
def __new__(cls, value, freq=None, dtype=None, copy=True, **kwargs):
unit = un.Unit(un.def_unit('lambdas', format={'format': r'\lambda'}, prefixes=True))
self = super().__new__(cls, value=value, unit=unit, dtype=dtype, copy=copy, **kwargs)
self.freq = freq
if self.freq is not None:
self.equivalencies = self.lambdas_equivalencies()
return self
#property
def freq(self):
return self._freq
#freq.setter
def freq(self, val):
if val is not None:
self._equivalencies = self.lambdas_equivalencies(restfreq=val)
self._freq = val
#property
def equivalencies(self):
return self._equivalencies
#equivalencies.setter
def equivalencies(self, val):
self._equivalencies = val
def lambdas_equivalencies(self, restfreq=None):
if self.freq is not None:
restfreq_hz = self.freq.to(un.Hz, equivalencies=un.spectral())
elif restfreq is not None:
restfreq_hz = restfreq.to(un.Hz, equivalencies=un.spectral())
else:
sys.exit("Frequency not provided")
eq = [
(self.unit, un.s, lambda x: x / restfreq_hz, lambda x: x * restfreq_hz),
(self.unit, un.m, lambda x: x / restfreq_hz * co.c.to(un.m / un.s).value,
lambda x: x / co.c.to(un.m / un.s).value * restfreq_hz),
(un.m, un.s, lambda x: x / co.c.to(un.m / un.s).value, lambda x: x * co.c.to(un.m / un.s).value),
]
return eq
def to(self, unit, restfreq=None, copy=True):
equiv = []
if restfreq is None:
equiv = self.equivalencies
else:
equiv = self.lambdas_equivalencies(restfreq=restfreq)
unit = un.Unit(unit)
if copy:
# Avoid using to_value to ensure that we make a copy. We also
# don't want to slow down this method (esp. the scalar case).
value = self._to_value(unit, equiv)
else:
# to_value only copies if necessary
value = self.to_value(unit, equiv)
return self._new_view(value, unit)
However when I use the class I can only use the unit lambda, but I would like to use klambda or mega-lambda, etc. According to astropy this can be done by using the parameter prefixes=True, however this does not seem to work.
I don't think you should be actually defining the unit inside your class's __new__, as it's not allowing you to actually set the unit when you instantiate a uWavelength.
Instead put this outside your class:
lambdas = u.def_unit('lambdas', format={'format': r'\lambda'})
Something I think the docs don't make really clear is when you use prefixes=True it doesn't really do anything unless you also provide a namespace= argument, and even then the docs don't make it super clear how to use namespaces. I think it would be better to go with astrofrog's suggestion of explicitly declaring the prefixed units needed like:
klambdas = u.def_unit('kilolambdas', represents=u.CompositeUnit(1e3, [lambdas], [1]), format={'format' : r'k\lambda'})```
unless you *really* need every imaginable SI prefix, you could try:
lambdas = u.def_unit('lambdas', format={'format': r'\lambda'}, prefixes=True, namespace=globals())
and it will inject every prefixed unit into your module namespace.
Then, since you want your default unit for `uWavelength` to be `lambdas`, then both to reduce confusion, and also add some documentation of this fact (through the signature of `__new__`) specify:
```python
class uWavelength(un.Quantity):
def __new__(cls, value, freq=None, unit=lambdas, dtype=None, copy=True, **kwargs):
and further, if you wanted, you could add a check like:
unit = u.Unit(unit)
assert unit.is_equivalent(lambdas), 'unit must be equivalent to lambdas'

More efficient fillna(numpy)

I need an array version of a function similar to Pandas.fillna, in the forum I collected a lot of answers to create the following function, but it is still 3 times times slower than Pandas.fillna, I want to know if there is a better way to optimize, thank you.
def fillna(self,axis=None,mask=None,value=None,method='pad'):
""" array fillna
Parameters
----------
self : 1d/2d
axis : axis(0 or 1)
mask : Custom mask, or Built np.isfinite(x)
value : int
method : 'back', 'pad', 'mean'
--------
"""
x = np.asarray(self)
if mask is None: mask = np.isfinite(x)
if (not value is None)|(method=='mean'):
out = x.copy()
if x.ndim == 1:
if method=='mean':
out[~mask] = np.nanmean(x)
else: out[~mask] = value
else:
vask = ~mask * (np.nanmean(x,1)[:,None] if axis==1 else np.nanmean(x,0))
out[~mask] = vask[~mask]
else:
if axis is None: axis = 0
if x.ndim==1:
if method=='pad':
idx = np.where(mask,np.arange(mask.shape[0]),0)
np.maximum.accumulate(idx,axis=0,out=idx)
return x[idx]
elif method=='back':
idx = np.where(mask[::-1],np.arange(mask.shape[0]),0)
np.maximum.accumulate(idx,axis=0,out=idx)
return x[mask.shape[0]-idx[::-1]-1]
else: return x
if axis==1:
if method=='back': mask = mask[:, ::-1]
idx = np.where(mask,np.arange(mask.shape[1]),0)
else:
if method=='back': mask = mask[::-1,:]
idx = np.where(mask,np.arange(mask.shape[0])[:,None],0)
np.maximum.accumulate(idx,axis=axis,out=idx)
if axis==1:
if method=='back': idx = idx.shape[1]-idx[:, ::-1] - 1
out = x[np.arange(idx.shape[0])[:,None], idx]
else:
if method=='back': idx = idx.shape[0]-idx[::-1, :] - 1
out = x[idx,np.arange(idx.shape[1])]
return out

Can this PyGame code run 60fps for >40 critters?

In my other question, some of the posters asked to see the code and suggested I make a new question. As requested, here is most of the code I'm using. I've removed the Vector class, simply because it's a lot of code. It's well-understood math that I got from someone else (https://gist.github.com/mcleonard/5351452), and cProfile didn't have much to say about any of the functions there. I've provided a link in the code, if you want to make this run-able.
This code should run, if you paste the the vector class where indicated in the code.
The problem is, once I get above 20 critters, the framerate drops rapidly from 60fps to 11fps around 50 critters.
Please excuse the spaghetti-code. Much of this is diagnostic kludging or pre-code that I intend to either remove, or turn into a behavior (instead of a hard-coded value).
This app is basically composed of 4 objects.
A Vector object provides abstracted vector operations.
A Heat Block is able to track it's own "heat" level, increase it and decrease it. It can also draw itself.
A Heat Map is composed of heat blocks which are tiled across the screen. When given coordinates, it can choose the block that those coordinates fall within.
A Critter has many features that make it able to wander around the screen, bump off of the walls and other critters, choose a new random direction, and die.
The main loop iterates through each critter in the "flock" and updates its "condition" (whether or not it's "dying"), its location, its orientation, and the heat block on which it is currently standing. The loop also iterates over each heat block to let it "cool down."
Then the main loop asks the heat map to draw itself, and then each critter in the flock to draw itself.
import pygame
from pygame import gfxdraw
import pygame.locals
import os
import math
import random
import time
(I got a nice vector class from someone else. It's large, and mostly likely not the problem.)
(INSERT CONTENTS OF VECTOR.PY FROM https://gist.github.com/mcleonard/5351452 HERE)
pygame.init()
#some global constants
BLUE = (0, 0, 255)
WHITE = (255,255,255)
diagnostic = False
SPAWN_TIME = 1 #number of seconds between creating new critters
FLOCK_LIMIT = 30 #number of critters at which the flock begins being culled
GUIDs = [0] #list of guaranteed unique IDs for identifying each critter
# Set the position of the OS window
position = (30, 30)
os.environ['SDL_VIDEO_WINDOW_POS'] = str(position[0]) + "," + str(position[1])
# Set the position, width and height of the screen [width, height]
size_x = 1000
size_y = 500
size = (size_x, size_y)
FRAMERATE = 60
SECS_FOR_DYING = 1
screen = pygame.display.set_mode(size)
screen.set_alpha(None)
pygame.display.set_caption("My Game")
# Used to manage how fast the screen updates
clock = pygame.time.Clock()
def random_float(lower, upper):
num = random.randint(lower*1000, upper*1000)
return num/1000
def new_GUID():
num = GUIDs[-1]
num = num + 1
while num in GUIDs:
num += 1
GUIDs.append(num)
return num
class HeatBlock:
def __init__(self,_tlx,_tly,h,w):
self.tlx = int(_tlx)
self.tly = int(_tly)
self.height = int(h)+1
self.width = int(w)
self.heat = 255.0
self.registered = False
def register_tresspasser(self):
self.registered = True
self.heat = max(self.heat - 1, 0)
def cool_down(self):
if not self.registered:
self.heat = min(self.heat + 0.1, 255)
self.registered = False
def hb_draw_self(self):
screen.fill((255,int(self.heat),int(self.heat)), [self.tlx, self.tly, self.width, self.height])
class HeatMap:
def __init__(self, _h, _v):
self.h_freq = _h #horizontal frequency
self.h_rez = size_x/self.h_freq #horizontal resolution
self.v_freq = _v #vertical frequency
self.v_rez = size_y/self.v_freq #vertical resolution
self.blocks = []
def make_map(self):
h_size = size_x/self.h_freq
v_size = size_y/self.v_freq
for h_count in range(0, self.h_freq):
TLx = h_count * h_size #TopLeft corner, x
col = []
for v_count in range(0, self.v_freq):
TLy = v_count * v_size #TopLeft corner, y
col.append(HeatBlock(TLx,TLy,v_size,h_size))
self.blocks.append(col)
def hm_draw_self(self):
for col in self.blocks:
for block in col:
block.cool_down()
block.hb_draw_self()
def register(self, x, y):
#convert the given coordinates of the trespasser into a col/row block index
col = max(int(math.floor(x / self.h_rez)),0)
row = max(int(math.floor(y / self.v_rez)),0)
self.blocks[col][row].register_tresspasser()
class Critter:
def __init__(self):
self.color = (random.randint(1, 200), random.randint(1, 200), random.randint(1, 200))
self.linear_speed = random_float(20, 100)
self.radius = int(round(10 * (100/self.linear_speed)))
self.angular_speed = random_float(0.1, 2)
self.x = int(random.randint(self.radius*2, size_x - (self.radius*2)))
self.y = int(random.randint(self.radius*2, size_y - (self.radius*2)))
self.orientation = Vector(0, 1).rotate(random.randint(-180, 180))
self.sensor = Vector(0, 20)
self.sensor_length = 20
self.new_orientation = self.orientation
self.draw_bounds = False
self.GUID = new_GUID()
self.condition = 0 #0 = alive, [1-fps] = dying, >fps = dead
self.delete_me = False
def c_draw_self(self):
#if we're alive and not dying, draw our normal self
if self.condition == 0:
#diagnostic
if self.draw_bounds:
pygame.gfxdraw.rectangle(screen, [int(self.x), int(self.y), 1, 1], BLUE)
temp = self.orientation * (self.linear_speed * 20)
pygame.gfxdraw.line(screen, int(self.x), int(self.y), int(self.x + temp[0]), int(self.y + temp[1]), BLUE)
#if there's a new orientation, match it gradually
temp = self.new_orientation * self.linear_speed
#draw my body
pygame.gfxdraw.aacircle(screen, int(self.x), int(self.y), self.radius, self.color)
#draw a line indicating my new direction
pygame.gfxdraw.line(screen, int(self.x), int(self.y), int(self.x + temp[0]), int(self.y + temp[1]), BLUE)
#draw my sensor (a line pointing forward)
self.sensor = self.orientation.normalize() * self.sensor_length
pygame.gfxdraw.line(screen, int(self.x), int(self.y), int(self.x + self.sensor[0]), int(self.y + self.sensor[1]), BLUE)
#otherwise we're dying, draw our dying animation
elif 1 <= self.condition <= FRAMERATE*SECS_FOR_DYING:
#draw some lines in a spinningi circle
for num in range(0,10):
line = Vector(0, 1).rotate((num*(360/10))+(self.condition*23))
line = line*self.radius
pygame.gfxdraw.line(screen, int(self.x), int(self.y), int(self.x+line[0]), int(self.y+line[1]), self.color)
def print_self(self):
#diagnostic
print("==============")
print("radius:", self.radius)
print("color:", self.color)
print("linear_speed:", self.linear_speed)
print("angular_speed:", self.angular_speed)
print("x:", self.x)
print("y:", int(self.y))
print("orientation:", self.orientation)
def avoid_others(self, _flock):
for _critter in _flock:
#if the critter isn't ME...
if _critter.GUID is not self.GUID and _critter.condition == 0:
#and it's touching me...
if self.x - _critter.x <= self.radius + _critter.radius:
me = Vector(self.x, int(self.y))
other_guy = Vector(_critter.x, _critter.y)
distance = me - other_guy
#give me new orientation that's away from the other guy
if distance.norm() <= ((self.radius) + (_critter.radius)):
new_direction = me - other_guy
self.orientation = self.new_orientation = new_direction.normalize()
def update_location(self, elapsed):
boundary = '?'
while boundary != 'X':
boundary = self.out_of_bounds()
if boundary == 'N':
self.orientation = self.new_orientation = Vector(0, 1).rotate(random.randint(-20, 20))
self.y = (self.radius) + 2
elif boundary == 'S':
self.orientation = self.new_orientation = Vector(0,-1).rotate(random.randint(-20, 20))
self.y = (size_y - (self.radius)) - 2
elif boundary == 'E':
self.orientation = self.new_orientation = Vector(-1,0).rotate(random.randint(-20, 20))
self.x = (size_x - (self.radius)) - 2
elif boundary == 'W':
self.orientation = self.new_orientation = Vector(1,0).rotate(random.randint(-20, 20))
self.x = (self.radius) + 2
point = Vector(self.x, self.y)
self.x, self.y = (point + (self.orientation * (self.linear_speed*(elapsed/1000))))
boundary = self.out_of_bounds()
def update_orientation(self, elapsed):
#randomly choose a new direction, from time to time
if random.randint(0, 100) > 98:
self.choose_new_orientation()
difference = self.orientation.argument() - self.new_orientation.argument()
self.orientation = self.orientation.rotate((difference * (self.angular_speed*(elapsed/1000))))
def still_alive(self, elapsed):
return_value = True #I am still alive
if self.condition == 0:
return_value = True
elif self.condition <= FRAMERATE*SECS_FOR_DYING:
self.condition = self.condition + (elapsed/17)
return_value = True
if self.condition > FRAMERATE*SECS_FOR_DYING:
return_value = False
return return_value
def choose_new_orientation(self):
if self.new_orientation:
if (self.orientation.argument() - self.new_orientation.argument()) < 5:
rotation = random.randint(-300, 300)
self.new_orientation = self.orientation.rotate(rotation)
def out_of_bounds(self):
if self.x >= (size_x - (self.radius)):
return 'E'
elif self.y >= (size_y - (self.radius)):
return 'S'
elif self.x <= (0 + (self.radius)):
return 'W'
elif self.y <= (0 + (self.radius)):
return 'N'
else:
return 'X'
# -------- Main Program Loop -----------
# generate critters
flock = [Critter()]
# generate heat map
heatMap = HeatMap(60, 40)
heatMap.make_map()
# set some settings
last_spawn = time.clock()
run_time = time.perf_counter()
frame_count = 0
max_time = 0
ms_elapsed = 1
avg_fps = [1]
# Loop until the user clicks the close button.
done = False
while not done:
# --- Main event loop only processes one event
frame_count = frame_count + 1
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
# --- Game logic should go here
#check if it's time to make another critter
if time.clock() - last_spawn > SPAWN_TIME:
flock.append(Critter())
last_spawn = time.clock()
if len(flock) >= FLOCK_LIMIT:
#if we're over the flock limit, cull the herd
counter = FLOCK_LIMIT
for critter in flock[0:len(flock)-FLOCK_LIMIT]:
#this code allows a critter to be "dying" for a while, to play an animation
if critter.condition == 0:
critter.condition = 1
elif not critter.still_alive(ms_elapsed):
critter.delete_me = True
counter = 0
#delete all the critters that have finished dying
while counter < len(flock):
if flock[counter].delete_me:
del flock[counter]
else:
counter = counter+1
#----loop on all critters once, doing all functions for each critter
for critter in flock:
if critter.condition == 0:
critter.avoid_others(flock)
if critter.condition == 0:
heatMap.register(critter.x, critter.y)
critter.update_location(ms_elapsed)
critter.update_orientation(ms_elapsed)
if diagnostic:
critter.print_self()
#----alternately, loop for each function. Speed seems to be similar either way
#for critter in flock:
# if critter.condition == 0:
# critter.update_location(ms_elapsed)
#for critter in flock:
# if critter.condition == 0:
# critter.update_orientation(ms_elapsed)
# --- Screen-clearing code goes here
# Here, we clear the screen to white. Don't put other drawing commands
screen.fill(WHITE)
# --- Drawing code should go here
#draw the heat_map
heatMap.hm_draw_self()
for critter in flock:
critter.c_draw_self()
#draw the framerate
myfont = pygame.font.SysFont("monospace", 15)
#average the framerate over 60 frames
temp = sum(avg_fps)/float(len(avg_fps))
text = str(round(((1/temp)*1000),0))+"FPS | "+str(len(flock))+" Critters"
label = myfont.render(text, 1, (0, 0, 0))
screen.blit(label, (5, 5))
# --- Go ahead and update the screen with what we've drawn.
pygame.display.update()
# --- Limit to 60 frames per second
#only run for 30 seconds
if time.perf_counter()-run_time >= 30:
done = True
#limit to 60fps
#add this frame's time to the list
avg_fps.append(ms_elapsed)
#remove any old frames
while len(avg_fps) > 60:
del avg_fps[0]
ms_elapsed = clock.tick(FRAMERATE)
#track longest frame
if ms_elapsed > max_time:
max_time = ms_elapsed
#print some stats once the program is finished
print("Count:", frame_count)
print("Max time since last flip:", str(max_time)+"ms")
print("Total Time:", str(int(time.perf_counter()-run_time))+"s")
print("Average time for a flip:", str(int(((time.perf_counter()-run_time)/frame_count)*1000))+"ms")
# Close the window and quit.
pygame.quit()
One thing you can already do to improve the performance is to use pygame.math.Vector2 instead of your Vector class, because it's implemented in C and therefore faster. Before I switched to pygame's vector class, I could have ~50 critters on the screen before the frame rate dropped below 60, and after the change up to ~100.
pygame.math.Vector2 doesn't have that argument method, so you need to extract it from the class and turn it into a function:
def argument(vec):
""" Returns the argument of the vector, the angle clockwise from +y."""
arg_in_rad = math.acos(Vector(0,1)*vec/vec.length())
arg_in_deg = math.degrees(arg_in_rad)
if vec.x < 0:
return 360 - arg_in_deg
else:
return arg_in_deg
And change .norm() to .length() everywhere in the program.
Also, define the font object (myfont) before the while loop. That's only a minor improvement, but every frame counts.
Another change that made a significant improvement was to streamline my collision-detection algorithm.
Formerly, I had been looping through every critter in the flock, and measuring the distance between it and every other critter in the flock. If that distance was small enough, I do something. That's n^2 checks, which is not awesome.
I'd thought about using a quadtree, but it didn't seem efficient to rebalance the whole tree every frame, because it will change every time a critter moves.
Well, I finally actually tried it, and it turns out that building a brand-new quadtree at the beginning of each frame is actually plenty fast. Once I have the tree, I pass it to the avoidance function where I just extract an intersection of any of the critters in that tree within a bounding box I care about. Then I just iterate on those neighbors to measure distances and update directions and whatnot.
Now I'm up to 150 or so critters before I start dropping frames (up from 40).
So the moral of the story is, trust evidence instead of intuition.