Astropy: Inititializing a user-defined Coordinate Frame - astropy

I am developing an application for Raspberry Pi to control an amateur telescope on AltAzimutal mount.
The azimutal axis is not perfectly aligned with the zenith. In this case one can point onto 2 or 3 stars and find the transformation matrix between apparent telescope and equatorial coordinates. The method can be seen here. I want to implement this method with astropy, but I don't understand how I can define the reference frame of the tilted telescope.
I calculate the transformatin matrix and give it as an argument for the new frame. But I get an error.
Here is the code:
# coding: utf-8
""" Astropy coordinate class for the tilted telescope coordinate system """
from __future__ import division, print_function
# Third-party
import numpy as np
from numpy import cos, sin
from astropy.coordinates import frame_transform_graph
from astropy.coordinates.angles import rotation_matrix
import astropy.coordinates as coord
import astropy.units as u
__all__ = ["MyFrame"]
import astropy.coordinates as coord
class MyFrame(coord.BaseCoordinateFrame):
"""
A topocentric spherical coordinate system defined by the telescope on tilted Altzimutal mount
http://www.geocities.jp/toshimi_taki/aim/aim.htm
Parameters
----------
matrix: the transformation matrix obtained by 2 stars method ( http://www.geocities.jp/toshimi_taki/aim/aim.htm)
representation : `BaseRepresentation` or None
A representation object or None to have no data (or use the other keywords)
Lambda : `Angle`, optional, must be keyword
The longitude-like angle corresponding to Sagittarius' orbit.
Beta : `Angle`, optional, must be keyword
The latitude-like angle corresponding to Sagittarius' orbit.
"""
default_representation = coord.UnitSphericalRepresentation
frame_specific_representation_info = {
'spherical': [coord.RepresentationMapping('lon', 'az'),
coord.RepresentationMapping('lat', 'alt'),
coord.RepresentationMapping('distance', 'distance')],
'unitspherical': [coord.RepresentationMapping('lon', 'az'),
coord.RepresentationMapping('lat', 'alt')]
}
def __init__(self,matrix,*args, **kwargs):
super(MyFrame, self).__init__(*args, **kwargs)
self.matrix=matrix
# equatorial (ICRS ) to tilted telescope AltAz coordinates
#frame_transform_graph.transform(coord.FunctionTransform, coord.ICRS, MyFrame)
def equatorial_to_telescope(icrs_frame, telescope_frame):
""" Compute the transformation from icrs spherical to
topocentric telescope coordinates.
"""
matrix=np.matrix(([1,0,0],[0,1,0],[0,0,1]))
C0=icrs_frame.represent_as(coord.CartesianRepresentation).xyz.value
l0=matrix.dot(C0)
altAZ=coord.SkyCoord(x=l0[0,0],y=l0[0,1],z=l0[0,2],frame='altaz',representation='cartesian').represent_as(coord.UnitSphericalRepresentation)
return MyFrame(az=altAZ.lon.to(u.deg), alt=altAZ.lat.to(*u.deg))
Here is the call of this class:
import myFrame as fr
import astropy.coordinates as coord
import astropy.units as u
import numpy as np
matrix=np.matrix(([1,0,0],[0,1,0],[0,0,1]))
m=fr.MyFrame()
icrs = coord.ICRS(152.88572*u.degree, 11.57281*u.degree)
mfr=icrs.transform_to(m)
Here is the error code:
TypeError Traceback (most recent call last)
<ipython-input-14-33f2cd1fa087> in <module>()
----> 1 mfr=icrs.transform_to(m)
/home/maksim/MyPython/astropy/coordinates/baseframe.pyc in transform_to(self, new_frame)
839 msg = 'Cannot transform from {0} to {1}'
840 raise ConvertError(msg.format(self.__class__, new_frame.__class__))
--> 841 return trans(self, new_frame)
842
843 def is_transformable_to(self, new_frame):
/home/maksim/MyPython/astropy/coordinates/transformations.pyc in __call__(self, fromcoord, toframe)
915 frattrs[inter_frame_attr_nm] = attr
916
--> 917 curr_toframe = t.tosys(**frattrs)
918 curr_coord = t(curr_coord, curr_toframe)
919
TypeError: __init__() takes at least 2 arguments (1 given)
I understand the error message: the definition of my constructor does not correspond to expectation of astropy BaseFrame.
How can I transfer extern matrix to BaseCoordinateFrame instance?

You don't need or want to override the __init__ on your frame class to include the transformation matrix as an argument. Instead, because an specific frame defined in your frame class depends on this matrix it should be defined as a FrameAttribute as demonstrated in the example in the docs: http://docs.astropy.org/en/stable/coordinates/frames.html#defining-a-new-frame
Looking at those docs I can see that the purpose of defining frame attributes is not as clear as it could be. But in short, merely adding an __init__ argument tells the coordinate machinery nothing about the purpose of that argument / attribute--it doesn't in any way indicate that this is a defining parameter of frames in this class of frames. That's information that the framework needs to know about and keep track of.
When you define matrix as a frame attribute on your class it will automatically set up an accessor for the attribute (i.e. self.matrix) and will also accept the matrix as an argument to the frame's initializer.
One thing that I'm not sure this immediately supports is a FrameAttribute that is required to initialize the frame (usually each attribute has a default value). This could be implemented by a simple wrapper around the base __init__ though it also might not be a bad idea for a feature request.

Related

Folium map embedded within PyQt displays blank when adding GeoJson to layer [duplicate]

This question already has answers here:
Add a large shapefile to map in python using folium
(1 answer)
Use QWebEngineView to Display Something Larger Than 2MB?
(1 answer)
Closed 12 months ago.
I have PyQt5 app that embeds a folium Map within a QWidget.
Here's a minimal example of the class I wrote :
import folium
import io
from folium.plugins import Draw, MousePosition, HeatMap
from PySide2 import QtWidgets, QtWebEngineWidgets
class FoliumMap(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
self.layout = QtWidgets.QVBoxLayout()
m = folium.Map(
title='coastlines',
zoom_start=3)
data = io.BytesIO()
m.save(data, close_file=False)
webView = QtWebEngineWidgets.QWebEngineView()
webView.setHtml(data.getvalue().decode())
self.layout.addWidget(webView)
self.setLayout(self.layout)
If i run my program here's what I have :
Now I want to add a GeoJson layer within the __init__ of my class :
folium.GeoJson('data/custom.geo.json', name='coasts').add_to(m)
This results in having a blank Qwidget :
If I save the map under html format I am able to see the layer on my web browser:
Has anyone an idea on why the layer implementation makes the QWidget blank ? And how to fix this ?
The problem seems to be the following command :
webView.setHtml(data.getvalue().decode())
It is said in the Qt documentation that :
Content larger than 2 MB cannot be displayed, because setHtml() converts the provided HTML to percent-encoding and places data: in front of it to create the URL that it navigates to. Thereby, the provided code becomes a URL that exceeds the 2 MB limit set by Chromium. If the content is too large, the loadFinished() signal is triggered with success=false.
My html file weighs 2628 KB > 2MB. So we have to use webView.load() method instead.
This is the way to go :
import folium
import io
from folium.plugins import Draw, MousePosition, HeatMap
from PySide2 import QtWidgets, QtWebEngineWidgets, QtCore
class FoliumMap(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
self.layout = QtWidgets.QVBoxLayout()
m = folium.Map(
title='coastlines',
zoom_start=3)
url = "C:/MYPATH/TO/map.html"
m.save(url)
webView = QtWebEngineWidgets.QWebEngineView()
html_map = QtCore.QUrl.fromLocalFile(url)
webView.load(html_map)
self.layout.addWidget(webView)
self.setLayout(self.layout)

matplotlib animation embedded in tkinter : update function never called

I've been trying to embed a matplotlib animation into tkinter.
The goal of this app is to simulate some differentials equations with rk4 method and show a real time graph as the simulation goes.
In fact the plot is rightly embedded into the tkinter frame.
However, the animation never run, I've noticed that the update function is never called.
I've been searching everywhere but I didn't find anything.
Thanks for the help.
Here is a code sample of the GUI class showing where I execute the animation
# called when I click on a button "start simulation"
def plot_neutrons_flow(self):
# getting parameters from the graphical interface
if not self._started:
I0 = float(self._field_I0.get())
X0 = float(self._field_X0.get())
flow0 = float(self._field_flow0.get())
time_interval = float(self._field_time_interval.get())
stop = int(self._field_stop.get())
FLOW_CI = [I0, X0, flow0] # [I(T_0), X(T_0), PHI[T_0]]
self._simulation = NeutronsFlow(
edo=neutrons_flow_edo,
t0=0,
ci=FLOW_CI,
time_interval=time_interval,
stop=hour_to_seconds(stop)
)
# launch the animation
self._neutrons_flow_plot.animate(self._simulation)
self._started = True
Here is the code for the matplotlib animation :
import matplotlib
import tkinter as tk
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib import style
matplotlib.use("TkAgg")
style.use('seaborn-whitegrid')
class PlotAnimation(FigureCanvasTkAgg):
def __init__(self, tk_root):
self._figure = Figure(dpi=100)
# bind plot to tkinter frame
super().__init__(self._figure, tk_root)
x_label = "Temps (h)"
y_label = "Flux / Abondance"
self._axes = self._figure.add_subplot(111, xlabel=x_label, ylabel=y_label, yscale="log")
self.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
def update(self, interval):
# this is never called
# get data from rk4 simulation
time_set = self._simulation.get_time_set()
y_set = self._simulation.get_y_set()
self._axes.clear()
self._axes.plot(time_set, y_set, visible=True, linewidth=1)
self._axes.legend(fancybox=True)
# redraw canvas
self.draw_idle()
def animate(self, simulation):
# this is called
self._simulation = simulation
# simulate differential equations with rk4 method
self._simulation.resolve()
# https://github.com/matplotlib/matplotlib/issues/1656
anim = animation.FuncAnimation(
self._figure,
self.update,
interval=1000
)
EDIT :
The solution was to instantiate the FuncAnimation function directly in the init method
As indicated in the documentation of the animation module (emphasis mine)
(...) it is critical to keep a reference to the instance object. The
animation is advanced by a timer (typically from the host GUI
framework) which the Animation object holds the only reference to. If
you do not hold a reference to the Animation object, it (and hence the
timers), will be garbage collected which will stop the animation.
You need to return the anim object from your animate() function, and store it somewhere in your code so that it is not garbage-collected

How to fix overlapping Metpy/Cartopy images?

When I run this code
import Scientific.IO.NetCDF as S
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.pyplot as plt
import xarray as xr
import metpy
import numpy as N
from metpy.plots import ContourPlot, ImagePlot, MapPanel, PanelContainer
# Any import of metpy will activate the accessors
import metpy.calc as mpcalc
#from metpy.testing import get_test_data
from metpy.units import units
# Open the netCDF file as a xarray Datase
#
datadir='C:/Users/stratus/AppData/Local/lxss/home/stratus/PROJECT/NEWPROJECT/FEB012017/nam_218_20170131_1200_000.nc'
data = xr.open_dataset(datadir,decode_cf=True)
# To parse the full dataset, we can call parse_cf without an argument, and assign the returned
# Dataset.
data = data.metpy.parse_cf()
tempatt=data['TMP_P0_L100_GLC0'].attrs
# If we instead want just a single variable, we can pass that variable name to parse_cf and
# it will return just that data variable as a DataArray.
data_var = data.metpy.parse_cf('TMP_P0_L100_GLC0')
# To rename variables, supply a dictionary between old and new names to the rename method
data.rename({
'TMP_P0_L100_GLC0': 'temperature',
}, inplace=True)
data['temperature'].metpy.convert_units('degC')
# Get multiple coordinates (for example, in just the x and y direction)
x, y = data['temperature'].metpy.coordinates('x', 'y')
# If we want to get just a single coordinate from the coordinates method, we have to use
# tuple unpacking because the coordinates method returns a generator
vertical, = data['temperature'].metpy.coordinates('vertical')
data_crs = data['temperature'].metpy.cartopy_crs
# Or, we can just get a coordinate from the property
#time = data['temperature'].metpy.time
# To verify, we can inspect all their names
#print([coord.name for coord in (x, y, vertical, time)])
#
#heights = data['height'].metpy.loc[{'time': time[0], 'vertical': 850. * units.hPa}]
#lat, lon = xr.broadcast(y, x)
#f = mpcalc.coriolis_parameter(lat)
#dx, dy = mpcalc.grid_deltas_from_dataarray(heights)
#u_geo, v_geo = mpcalc.geostrophic_wind(heights, f, dx, dy)
#print(u_geo)
#print(v_geo)
fig=plt.figure(1)
# A very simple example example of a plot of 500 hPa heights
data_crs = data['temperature'].metpy.cartopy_crs
ax = plt.axes(projection=ccrs.LambertConformal())
data['temperature'].metpy.loc[{'vertical': 850. * units.hPa}].plot(ax=ax, transform=data_crs)
ax.add_feature(cfeature.LAND)
ax.add_feature(cfeature.OCEAN)
ax.add_feature(cfeature.COASTLINE)
plt.show()
#ax.set_extent([-120,-80,20,50])
plt.title("850 mb Temperature")
#plt.suptitle("Metpy Test")
plt.show()
I had to edit the code as per some of the answers but I am getting a mostly blank map now. 850 T Map fail I am mainly trying to have the temperatures at 850 mb overlap the US so I could show it to a friend to practice for a project I am helping him with. The filling of the parentheses for the data helped a bit which is why I edited it.
As pointed out in the comments it is difficult to answer without a reproducible example. However, the following may solve your issue:
data_crs = data['temperature'].metpy.cartopy_crs
ax = plt.axes(projection=ccrs.LambertConformal())
data['temperature'].metpy.loc[{'vertical': 1000. * units.hPa}].plot(ax=ax, transform=data_crs)
ax.add_feature(cfeature.LAND)
ax.add_feature(cfeature.OCEAN)
ax.add_feature(cfeature.COASTLINE)
plt.show()

Custom markers with screen coordinate size in (older) matplotlib

Originally, I was hoping that I could create a sort of a class for a marker in matplotlib, which would be a square with text showing the, say, x coordinate and a label, so I could instantiate it with something like (pseudocode):
plt.plot(..., marker=myMarkerClass(label="X:"), ... )
... but as far as I can see, you cannot do stuff like that.
However, it seems that customization of markers is not available in older matplotlib; so I'd like to reduce my question to: how to get custom (path) markers in older matplotlib, so their sizes are defined in screen coordinates (so markers don't scale upon zoom)? To clarify, here are some examples:
Default (uncustomized) markers
Below is an example with the default matplotlib markers, which works with older matplotlib. Note that I've tried instead of using pyplot.plot(), I'm trying to work with matplotlib.figure.Figure directly (as that is the form usually used with diverse backends), requiring use of "figure manager" (see also matplotlib-devel - Backends object structure):
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.figure
import numpy as np
t = np.arange(0.0,1.5,0.25)
s = np.sin(2*np.pi*t)
mfigure = matplotlib.figure.Figure(figsize=(5,4), dpi=100)
ax = mfigure.add_subplot(111)
ax.plot(t,s, marker='o', color='b', markerfacecolor='orange', markersize=10.0)
fig = plt.figure() # create something (fig num 1) for fig_manager
figman = matplotlib._pylab_helpers.Gcf.get_fig_manager(1)
figman.canvas.figure = mfigure # needed
mfigure.set_canvas(figman.canvas) # needed
plt.show()
If we do an arbitrary zoom rect here, the markers remain the same size:
Customized markers via Path
This is documented in artists (Line2D.set_marker) — Matplotlib 1.2.1 documentation; however, it doesn't work with older matplotlib; here's an example:
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.figure
import matplotlib.path
import numpy as np
print("matplotlib version {0}".format(matplotlib.__version__))
def getCustomSymbol1(inx, iny, sc, yasp):
verts = [
(-0.5, -0.5), # left, bottom
(-0.5, 0.5), # left, top
(0.5, 0.5), # right, top
(0.5, -0.5), # right, bottom
(-0.5, -0.5), # ignored
]
codes = [matplotlib.path.Path.MOVETO,
matplotlib.path.Path.LINETO,
matplotlib.path.Path.LINETO,
matplotlib.path.Path.LINETO,
matplotlib.path.Path.CLOSEPOLY,
]
pathCS1 = matplotlib.path.Path(verts, codes)
return pathCS1, verts
t = np.arange(0.0,1.5,0.25)
s = np.sin(2*np.pi*t)
mfigure = matplotlib.figure.Figure(figsize=(5,4), dpi=100)
ax = mfigure.add_subplot(111)
pthCS1, vrtCS1 = getCustomSymbol1(0,0, 1,1)
# here either marker=pthCS1 or marker=np.array(vrtCS1)
# have the same effect:
ax.plot(t,s, marker=pthCS1, markerfacecolor='orange', markersize=10.0)
#ax.plot(t,s, marker=np.array(vrtCS1), markerfacecolor='orange', markersize=10.0)
fig = plt.figure() # create something (fig num 1) for fig_manager
figman = matplotlib._pylab_helpers.Gcf.get_fig_manager(1)
figman.canvas.figure = mfigure # needed
mfigure.set_canvas(figman.canvas) # needed
plt.show()
This runs fine for me in newer matplotlib, but fails in the older:
$ python3.2 test.py
matplotlib version 1.2.0
$ python2.7 test.py # marker=pthCS1
matplotlib version 0.99.3
Traceback (most recent call last):
File "test.py", line 36, in <module>
ax.plot(t,s, marker=pthCS1, markerfacecolor='orange', markersize=10.0)
...
File "/usr/lib/pymodules/python2.7/matplotlib/lines.py", line 804, in set_marker
self._markerFunc = self._markers[marker]
KeyError: Path([[-0.5 -0.5]
[-0.5 0.5]
[ 0.5 0.5]
[ 0.5 -0.5]
[-0.5 -0.5]], [ 1 2 2 2 79])
$ python2.7 test.py # marker=np.array(vrtCS1)
matplotlib version 0.99.3
Traceback (most recent call last):
File "test.py", line 38, in <module>
ax.plot(t,s, marker=np.array(vrtCS1), markerfacecolor='orange', markersize=10.0)
...
File "/usr/lib/pymodules/python2.7/matplotlib/lines.py", line 798, in set_marker
if marker not in self._markers:
TypeError: unhashable type: 'numpy.ndarray'
However, when it works in Python 3.2, the markers again keep their size across zoom of the graph, as I'd expect:
... though note this issue: Custom marker created from vertex list scales wrong · Issue #1980 · matplotlib/matplotlib · GitHub, in respect to this type of custom markers.
Through Paths in PatchCollection
I've picked up parts of this code from some internet postings, but cannot find the links now. In any case, we can avoid drawing the markers and PatchCollection can be used, to draw what should be the markers. Here is the code, which runs in older matplotlib:
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.figure
import matplotlib.path, matplotlib.patches, matplotlib.collections
import numpy as np
def getCustomSymbol1(inx, iny, sc, yasp):
verts = [
(inx-0.5*sc, iny-0.5*sc*yasp), # (0., 0.), # left, bottom
(inx-0.5*sc, iny+0.5*sc*yasp), # (0., 1.), # left, top
(inx+0.5*sc, iny+0.5*sc*yasp), # (1., 1.), # right, top
(inx+0.5*sc, iny-0.5*sc*yasp), # (1., 0.), # right, bottom
(inx-0.5*sc, iny-0.5*sc*yasp), # (0., 0.), # ignored
]
codes = [matplotlib.path.Path.MOVETO,
matplotlib.path.Path.LINETO,
matplotlib.path.Path.LINETO,
matplotlib.path.Path.LINETO,
matplotlib.path.Path.CLOSEPOLY,
]
pathCS1 = matplotlib.path.Path(verts, codes)
return pathCS1
def getXyIter(inarr):
# this supports older numpy, where nditer is not available
if np.__version__ >= "1.6.0":
return np.nditer(inarr.tolist())
else:
dimensions = inarr.shape
xlen = dimensions[1]
xinds = np.arange(0, xlen, 1)
return np.transpose(np.take(inarr, xinds, axis=1))
t = np.arange(0.0,1.5,0.25)
s = np.sin(2*np.pi*t)
mfigure = matplotlib.figure.Figure(figsize=(5,4), dpi=100)
ax = mfigure.add_subplot(111)
ax.plot(t,s)
customMarkers=[]
for x, y in getXyIter(np.array([t,s])): #np.nditer([t,s]):
#printse("%f:%f\n" % (x,y))
pathCS1 = getCustomSymbol1(x,y,0.05,1.5*500.0/400.0)
patchCS1 = matplotlib.patches.PathPatch(pathCS1, facecolor='orange', lw=1) # no
customMarkers.append(patchCS1)
pcolm = matplotlib.collections.PatchCollection(customMarkers)
pcolm.set_alpha(0.9)
ax.add_collection(pcolm)
fig = plt.figure() # create something (fig num 1) for fig_manager
figman = matplotlib._pylab_helpers.Gcf.get_fig_manager(1)
figman.canvas.figure = mfigure # needed
mfigure.set_canvas(figman.canvas) # needed
plt.show()
Now, here I tried to take figure initial aspect ratio into consideration and indeed, at first render, the "markers" look right in respect to size - but...:
... when we try to do arbitrary zoom, it is obvious that the paths have been specified in data coordinates, and so their sizes change depending on the zoom rect. (Another nuissance is that facecolor='orange' is not obeyed; but that can be fixed with pcolm.set_facecolor('orange'))
So, is there a way that I can use PatchCollection as markers for older matplotlib, in the sense that the rendered paths would be defined in screen coordinates, so they would not change their size upon arbitrary zooming?
Many thanks to #tcaswell for the comment on blended transform - in trying to figure out why that doesn't work for me, I finally found the solution. First, the code - using, essentially, the default marker engine of matplotlib (depending on whether using older matplotlib (0.99) or newer one):
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.figure
import numpy as np
# create vertices and Path of custom symbol
def getCustomSymbol1():
verts = [
(0.0, 0.0), # left, bottom
(0.0, 0.7), # left, top
(1.0, 1.0), # right, top
(0.8, 0.0), # right, bottom
(0.0, 0.0), # ignored
]
codes = [matplotlib.path.Path.MOVETO,
matplotlib.path.Path.LINETO,
matplotlib.path.Path.LINETO,
matplotlib.path.Path.LINETO,
matplotlib.path.Path.CLOSEPOLY,
]
pathCS1 = matplotlib.path.Path(verts, codes)
return pathCS1, verts
if matplotlib.__version__ < "1.0.0":
# define a marker drawing function, that uses
# the above custom symbol Path
def _draw_mypath(self, renderer, gc, path, path_trans):
gc.set_snap(renderer.points_to_pixels(self._markersize) >= 2.0)
side = renderer.points_to_pixels(self._markersize)
transform = matplotlib.transforms.Affine2D().translate(-0.5, -0.5).scale(side)
rgbFace = self._get_rgb_face()
mypath, myverts = getCustomSymbol1()
renderer.draw_markers(gc, mypath, transform,
path, path_trans, rgbFace)
# add this function to the class prototype of Line2D
matplotlib.lines.Line2D._draw_mypath = _draw_mypath
# add marker shortcut/name/command/format spec '#' to Line2D class,
# and relate it to our custom marker drawing function
matplotlib.lines.Line2D._markers['#'] = '_draw_mypath'
matplotlib.lines.Line2D.markers = matplotlib.lines.Line2D._markers
else:
import matplotlib.markers
def _set_mypath(self):
self._transform = matplotlib.transforms.Affine2D().translate(-0.5, -0.5)
self._snap_threshold = 2.0
mypath, myverts = getCustomSymbol1()
self._path = mypath
self._joinstyle = 'miter'
matplotlib.markers.MarkerStyle._set_mypath = _set_mypath
matplotlib.markers.MarkerStyle.markers['#'] = 'mypath'
matplotlib.lines.Line2D.markers = matplotlib.markers.MarkerStyle.markers
# proceed as usual - use the new marker '#'
t = np.arange(0.0,1.5,0.25)
s = np.sin(2*np.pi*t)
mfigure = matplotlib.figure.Figure(figsize=(5,4), dpi=100)
ax = mfigure.add_subplot(111)
ax.plot(t,s, marker='#', color='b', markerfacecolor='orange', markersize=20.0)
fig = plt.figure() # create something (fig num 1) for fig_manager
figman = matplotlib._pylab_helpers.Gcf.get_fig_manager(1)
figman.canvas.figure = mfigure # needed
mfigure.set_canvas(figman.canvas) # needed
plt.show()
It shows a slightly "creative" marker (if I may say so myself :) ) - and this is how it behaves under zoom:
... that is, exactly as I want it - markers keep their position in data coordinates; however preserve their size under zooming.
Discussion
One of the most confusing things for me here, was the following: Essentially, you can specify a rectangle through an x,y coordinate as "location", and size (height/width). So if I specify a rectangle at x,y=(2,3); with halfsize 2 (so, square); then I can calculate its path as vertices through:
[(x-hs,y-hs), (x-hs,y+hs), (x+hs,y+hs), (x+hs,y-hs)]
This is, essentially, what getCustomSymbol1 was trying to return all along. In addition, for instance matplotlib.patches.Rectangle is instantiated through location and size, as in Rectangle((x,y), width, height).
Now, the problem is - what I actually wanted, is that markers, as shapes, stay on their position - in data coordinates, so moving and zooming the graph keeps their position as data; however, they should have kept their size under zooming.
This means that I want (x,y) specified in one coordinate system (data), and size (or width, height, or halfsize) specified in another coordinate system, in this case, the screen coordinate system: since I want the shapes to keep their size under zoom, I actually want to keep their size in screen pixels the same!
That is why no transformation as such would help in my case - any transformation would work on all the vertices of a path, as interpreted in a single coordinate system! Whereas, what I want, would be to get something like:
hsd = screen2dataTransform(10px)
[(x-hsd,y-hsd), (x-hsd,y+hsd), (x+hsd,y+hsd), (x+hsd,y-hsd)]
... and this recalculation of the vertices of the marker path would have to be repeated each time the zoom level, or the figure pan, or the window size changes.
As such, vertices/paths (and patches) and transformation alone cannot be used for this purpose. However, thankfully, we can use matplotlibs own engine; we simply have to know that any call to ax.plot(...marker=...) actually delegates the drawing of markers to matplotlib.lines.Line2D; and Line2D maintains an internal dict, which relates the marker one-character format specifier/command (like 'o', '*' etc) to a specific drawing function; and the drawing function, finally, does the size transformation in code like (in the solution code above, the drawing is for the most part taken from the 's' (square) marker implementation):
side = renderer.points_to_pixels(self._markersize)
transform = matplotlib.transforms.Affine2D().translate(-0.5, -0.5).scale(side)
Note that this is the case for older matplotlib (0.99.3 in my case); for newer matplotlib (1.2.0 in my case), there is a separate class MarkerStyle which maintains the relationship between the marker format specifier, and the function - and the function is not _draw_ anymore, it is just _set_ - but other than that, it is the same principle.
NB: I'm actually not sure when MarkerStyle was introduced, I could only find matplotlib.markers — Matplotlib 1.3.x documentation and it doesn't say; so that if matplotlib.__version__ < "1.0.0": in the code above could be wrong; but worksforme (fornow).
Because the markers size is, so to speak, managed separately (from its position) - that means that you don't have to do any special calculation in your custom marker Path specification; all you need to do is make sure its vertices fit in the range (0.0,0.0) to (1.0, 1.0) - the marker drawing engine will do the rest.
Well, I hope I understood this right - but if there are other approaches that work like this, I'd sure like to know about those :)
Hope this helps someone,
Cheers!

pickable figures in matplotlib and Log10Transform

You may already know, that in matplotlib 1.2.0 there is a new experimental feature, that figures are pickable (they can be saved with pickle module).
However, it doesn't work when one uses logscale, eg.
import matplotlib.pyplot as plt
import numpy as np
import pickle
ax = plt.subplot(111)
x = np.linspace(0, 10)
y = np.exp(x)
plt.plot(x, y)
ax.set_yscale('log')
pickle.dump(ax, file('myplot.pickle', 'w'))
results in:
PicklingError: Can't pickle <class 'matplotlib.scale.Log10Transform'>: attribute lookup matplotlib.scale.Log10Transform failed
Anybody knows any solution/workaround to this?
I've opened this as a bug report on matplotlib's github issue tracker. Its a fairly easy fix to implement on the matplotlib repository side (simply don't nest the Log10Transform class inside the LogScale class), but that doesn't really help you in being able to use this with mpl 1.2.0...
There is a solution to getting this to work for you in 1.2.0, but I warn you - its not pretty!
Based on my answer to a pickling question it is possible to pickle nested classes (as Log10Transform is). All we need to do is to tell Log10Transform how to "reduce" itself:
import matplotlib.scale
class _NestedClassGetter(object):
"""
When called with the containing class as the first argument,
the name of the nested class as the second argument,
and the state of the object as the third argument,
returns an instance of the nested class.
"""
def __call__(self, containing_class, class_name, state):
nested_class = getattr(containing_class, class_name)
# return an instance of a nested_class. Some more intelligence could be
# applied for class construction if necessary.
c = nested_class.__new__(nested_class)
c.__setstate__(state)
return c
def _reduce(self):
# return a class which can return this class when called with the
# appropriate tuple of arguments
cls_name = matplotlib.scale.LogScale.Log10Transform.__name__
call_args = (matplotlib.scale.LogScale, cls_name, self.__getstate__())
return (_NestedClassGetter(), call_args)
matplotlib.scale.LogScale.Log10Transform.__reduce__ = _reduce
You might also decide to do this for other Log based transforms/classes, but for your example, you can now pickle (and successfully unpickle) your example figure:
import matplotlib.pyplot as plt
import numpy as np
import pickle
ax = plt.subplot(111)
x = np.linspace(0, 10)
y = np.exp(x)
plt.plot(x, y)
ax.set_yscale('log')
pickle.dump(ax, file('myplot.pickle', 'w'))
plt.savefig('pickle_log.pre.png')
plt.close()
pickle.load(file('myplot.pickle', 'r'))
plt.savefig('pickle_log.post.png')
I'm going to get on and fix this for mpl 1.3.x so that this nasty workaround isn't needed in the future :-) .
HTH,