Source code for bdsim.graphics
import sys
import matplotlib
import matplotlib.pyplot as plt
from matplotlib import animation
from bdsim.components import SinkBlock
[docs]class GraphicsBlock(SinkBlock):
"""
A GraphicsBlock is a subclass of SinkBlock that represents a block that has inputs
but no outputs and creates/updates a graphical display.
"""
blockclass = "graphics"
[docs] def __init__(self, movie=None, **blockargs):
"""
Create a graphical display block.
:param movie: Save animation in this file in MP4 format, defaults to None
:type movie: str, optional
:param blockargs: |BlockOptions|
:type blockargs: dict
:return: transfer function block base class
:rtype: TransferBlock
This is the parent class of all graphic display blocks.
"""
super().__init__(**blockargs)
self._graphics = True
self.movie = movie
[docs] def start(self, simstate):
# plt.draw()
# plt.show(block=False)
self._simstate = simstate
self._enabled = simstate.options.graphics
if self.movie is not None and not simstate.options.animation:
print(
"enabling global animation option to allow movie option on block", self
)
if not simstate.options.animation:
print("must enable animation to render a movie")
if self.movie is not None:
try:
self.writer = animation.FFMpegWriter(
fps=10, extra_args=["-vcodec", "libx264"]
)
self.writer.setup(fig=self.fig, outfile=self.movie)
print("movie block", self, " --> ", self.movie)
except FileNotFoundError:
self.fatal("cannot save movie, please install ffmpeg")
[docs] def step(self, t, inports):
super().step(t, inports)
# bring the figure up to date in a backend-specific way
if self._simstate.options.animation:
if self._simstate.backend == "TkAgg":
self.fig.canvas.flush_events()
plt.show(block=False)
plt.show(block=False)
elif self._simstate.backend == "Qt5Agg":
self.fig.canvas.flush_events()
self.fig.canvas.draw()
else:
self.fig.canvas.draw()
if self.movie is not None:
try:
self.writer.grab_frame()
except AttributeError:
self.fatal("cannot save movie, please install ffmpeg")
[docs] def done(self, block=False):
if self.fig is not None:
self.fig.canvas.start_event_loop(0.001)
if self.movie is not None:
self.writer.finish()
# self.cleanup()
plt.show(block=block)
[docs] def savefig(self, filename=None, format="pdf", **kwargs):
"""
Save the figure as an image file
:param fname: Name of file to save graphics to
:type fname: str
:param ``**kwargs``: Options passed to `savefig <https://matplotlib.org/3.2.2/api/_as_gen/matplotlib.pyplot.savefig.html>`_
The file format is taken from the file extension and can be
jpeg, png or pdf.
"""
try:
plt.figure(self.fig.number) # make block's figure the current one
if filename is None:
filename = self.name
filename += "." + format
print("saved {} -> {}".format(str(self), filename))
plt.savefig(filename, **kwargs) # save the current figure
except:
pass
[docs] def create_figure(self, state):
def move_figure(f, x, y):
"""Move figure's upper left corner to pixel (x, y)"""
backend = matplotlib.get_backend()
x = int(x) + gstate.xoffset
y = int(y)
if backend == "TkAgg":
f.canvas.manager.window.wm_geometry("+%d+%d" % (x, y))
elif backend == "WXAgg":
f.canvas.manager.window.SetPosition((x, y))
else:
# This works for QT and GTK
# You can also use window.setGeometry
try:
f.canvas.manager.window.move(x, y)
except AttributeError:
pass # can't do this for MacOSX
gstate = state
options = state.options
self.bd.runtime.DEBUG(
"graphics", "{} matplotlib figures exist", len(plt.get_fignums())
)
if gstate.fignum == 0:
# no figures yet created, lazy initialization
self.bd.runtime.DEBUG("graphics", "lazy initialization")
if options.backend is None:
if sys.platform == "darwin":
# for MacOS, use Qt5Agg if its installed
# otherwise use default (MacOSX)
if "Qt5Agg" in matplotlib.rcsetup.all_backends:
try:
import PyQt5
matplotlib.use("Qt5Agg")
print(
"no graphics backend specified: Qt5Agg found, using"
" instead of MacOSX"
)
except:
pass
else:
try:
matplotlib.use(options.backend)
except ImportError:
self.fatal(f"can't select backend: {options.backend}")
mpl_backend = matplotlib.get_backend()
gstate.backend = mpl_backend
self.bd.runtime.DEBUG("graphics", " backend={:s}", mpl_backend)
# split the string
ntiles = [int(x) for x in options.tiles.split("x")]
xoffset = 0
if options.shape is None:
if mpl_backend == "Qt5Agg":
# next line actually creates a figure if none already exist
QScreen = plt.get_current_fig_manager().canvas.screen()
# this is a QScreenClass object, see https://doc.qt.io/qt-5/qscreen.html#availableGeometry-prop
# next line creates a figure
sz = QScreen.availableSize()
dpiscale = (
QScreen.devicePixelRatio()
) # is 2.0 for Mac laptop screen
self.bd.runtime.DEBUG(
"graphics",
" {} x {} @ {}dpi",
sz.width(),
sz.height(),
dpiscale,
)
# check for a second screen
if options.altscreen:
vsize = QScreen.availableVirtualGeometry().getCoords()
if vsize[0] < 0:
# extra monitor to the left
xoffset = vsize[0]
elif vsize[0] >= sz.width():
# extra monitor to the right
xoffset = vsize[0]
self.bd.runtime.DEBUG(
"graphics", " altscreen offset {}", xoffset
)
screen_width, screen_height = sz.width(), sz.height()
dpi = QScreen.physicalDotsPerInch()
f = plt.gcf()
elif mpl_backend == "TkAgg":
window = plt.get_current_fig_manager().window
screen_width, screen_height = (
window.winfo_screenwidth(),
window.winfo_screenheight(),
)
dpiscale = 1
self.bd.runtime.DEBUG(
"graphics",
" screensize: {:d} x {:d}",
screen_width,
screen_height,
)
f = plt.gcf()
dpi = f.dpi
else:
# all other backends
f = plt.figure()
dpi = f.dpi
dpiscale = 2
screen_width, screen_height = f.get_size_inches() * f.dpi
# compute fig size in inches (width, height)
figsize = [
screen_width / ntiles[1] / dpi,
screen_height / ntiles[0] / dpi,
]
else:
# shape is given explictly
screen_width, screen_height = [int(x) for x in options.shape.split("x")]
f = plt.gcf()
f.canvas.manager.set_window_title(f"bdsim: Figure {f.number:d}")
# save graphics info away in state
gstate.figsize = figsize
gstate.dpi = dpi
gstate.screensize_pix = (screen_width, screen_height)
gstate.ntiles = ntiles
gstate.xoffset = xoffset
# resize the figure
f.set_dpi(gstate.dpi * dpiscale)
f.set_size_inches(figsize, forward=True)
plt.ion()
else:
# subsequent figures
f = plt.figure(figsize=gstate.figsize, dpi=gstate.dpi)
# move the figure to right place on screen
row = gstate.fignum // gstate.ntiles[0]
col = gstate.fignum % gstate.ntiles[0]
scale = 1.02
move_figure(
f,
col * gstate.figsize[0] * gstate.dpi * scale,
row * gstate.figsize[1] * gstate.dpi * scale,
)
gstate.fignum += 1
def onkeypress(event):
if event.key == "x":
print("\nclosing all windows")
plt.close("all")
elif event.key == "ctrl+c":
print("\nterminating bdsim")
sys.exit(1)
else:
print("key pressed", event.key)
f.canvas.mpl_connect("key_press_event", onkeypress)
self.bd.runtime.DEBUG(
"graphics", "create figure {:d} at ({:d}, {:d})", gstate.fignum, row, col
)
return f