Overloading (or alternatives) in Python API design - python

I have a large existing program library that currently has a .NET binding, and I'm thinking about writing a Python binding. The existing API makes extensive use of signature-based overloading. So, I have a large collection of static functions like:
Circle(p1, p2, p3) -- Creates a circle through three points
Circle(p, r) -- Creates a circle with given center point and radius
Circle(c1, c2, c3) -- Creates a circle tangent to three curves
There are a few cases where the same inputs must be used in different ways, so signature-based overloading doesn't work, and I have to use different function names, instead. For example
BezierCurve(p1,p2,p3,p4) -- Bezier curve using given points as control points
BezierCurveThroughPoints(p1,p2,p3,p4) -- Bezier curve passing through given points
I suppose this second technique (using different function names) could be used everywhere in the Python API. So, I would have
CircleThroughThreePoints(p1, p2, p3)
CircleCenterRadius(p, r)
CircleTangentThreeCurves(c1, c2, c3)
But the names look unpleasantly verbose (I don't like abbreviations), and inventing all of them will be quite a challenge, because the library has thousands of functions.
Low Priorities:
Effort (on my part) -- I don't care if I have to write a lot of code.
Performance
High Priorities:
Ease of use/understanding for callers (many will be programming newbies).
Easy for me to write good documentation.
Simplicity -- avoid the need for advanced concepts in caller's code.
I'm sure I'm not the first person who ever wished for signature-based overloading in Python. What work-arounds do people typically use?

One option is to exclusively keyword arguments in the constructor, and include logic to figure out what should be used:
class Circle(object):
def __init__(self, points=(), radius=None, curves=()):
if radius and len(points) == 1:
center_point = points[0]
# Create from radius/center point
elif curves and len(curves) == 3:
# create from curves
elif points and len(points) == 3:
# create from points
else:
raise ValueError("Must provide a tuple of three points, a point and a radius, or a tuple of three curves)
You can also use classmethods to make things easier for the users of the API:
class Circle(object):
def __init__(self, points=(), radius=None, curves=()):
# same as above
#classmethod
def from_points(p1, p2, p3):
return cls(points=(p1, p2, p3))
#classmethod
def from_point_and_radius(cls, point, radius):
return cls(points=(point,), radius=radius)
#classmethod
def from_curves(cls, c1, c2, c3):
return cls(curves=(c1, c2, c3))
Usage:
c = Circle.from_points(p1, p2, p3)
c = Circle.from_point_and_radius(p1, r)
c = Circle.from_curves(c1, c2, c3)

There are a couple of options.
You can have one constructor that accepts and arbitrary number of arguments (with *args and/or **varargs syntaxes) and does different things depending on the number and type the arguments have.
Or, you can write secondary constructors as class methods. These are known as "factory" methods. If you have multiple constructors that take the same number of objects of the same classes (as in your BezierCurve example), this is probably your only option.
If you don't mind overriding __new__ rather than __init__, you can even have both, with the __new__ method handling one form of arguments by itself and referring other kinds to the factory methods for regularizing. Here's an example of what that might look like, including doc strings for the multiple signatures to __new__:
class Circle(object):
"""Circle(center, radius) -> Circle object
Circle(point1, point2, point3) -> Circle object
Circle(curve1, curve2, curve3) -> Circle object
Return a Circle with the provided center and radius. If three points are given,
the center and radius will be computed so that the circle will pass through each
of the points. If three curves are given, the circle's center and radius will
be chosen so that the circle will be tangent to each of them."""
def __new__(cls, *args):
if len(args) == 2:
self = super(Circle, cls).__new__(cls)
self.center, self.radius = args
return self
elif len(args) == 3:
if all(isinstance(arg, Point) for arg in args):
return Circle.through_points(*args)
elif all(isinstance(arg, Curve) for arg in args):
return Circle.tangent_to_curves(*args)
raise TypeError("Invalid arguments to Circle()")
#classmethod
def through_points(cls, point1, point2, point3):
"""from_points(point1, point2, point3) -> Circle object
Return a Circle that touches three points."""
# compute center and radius from the points...
# then call back to the main constructor:
return cls(center, radius)
#classmethod
def tangent_to_curves(cls, curve1, curve2, curve3):
"""from_curves(curve1, curve2, curve3) -> Circle object
Return a Circle that is tangent to three curves."""
# here too, compute center and radius from curves ...
# then call back to the main constructor:
return cls(center, radius)

There are a number of modules in PyPI that can help you with signature based overloading and dispatch: multipledispatch, multimethods, Dispatching - none of which I have real experience with, but multipledispatch looks like what you want and it's well documented. Using your circle example:
from multipledispatch import dispatch
class Point(tuple):
pass
class Curve(object):
pass
#dispatch(Point, Point, Point)
def Circle(point1, point2, point3):
print "Circle(point1, point2, point3): point1 = %r, point2 = %r, point3 = %r" % (point1, point2, point3)
#dispatch(Point, int)
def Circle(centre, radius):
print "Circle(centre, radius): centre = %r, radius = %r" % (centre, radius)
#dispatch(Curve, Curve, Curve)
def Circle(curve1, curve2, curve3):
print "Circle(curve1, curve2, curve3): curve1 = %r, curve2 = %r, curve3 = %r" % (curve1, curve2, curve3)
>>> Circle(Point((10,10)), Point((20,20)), Point((30,30)))
Circle(point1, point2, point3): point1 = (10, 10), point2 = (20, 20), point3 = (30, 30)
>>> p1 = Point((25,10))
>>> p1
(10, 10)
>>> Circle(p1, 100)
Circle(centre, radius): centre = (25, 10), radius = 100
>>> Circle(*(Curve(),)*3)
Circle(curve1, curve2, curve3): curve1 = <__main__.Curve object at 0xa954d0>, curve2 = <__main__.Curve object at 0xa954d0>, curve3 = <__main__.Curve object at 0xa954d0>
>>> Circle()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/mhawke/virtualenvs/urllib3/lib/python2.7/site-packages/multipledispatch/dispatcher.py", line 143, in __call__
func = self.resolve(types)
File "/home/mhawke/virtualenvs/urllib3/lib/python2.7/site-packages/multipledispatch/dispatcher.py", line 184, in resolve
(self.name, str_signature(types)))
NotImplementedError: Could not find signature for Circle: <>
It's also possible to decorate instance methods, so you can provide multiple implementations of __init__(), which is quite nice. If you were implementing any actual behaviour within the class, e.g. Circle.draw(), you would need some logic to work out what values are available with to draw the circle (centre and radius, 3 points, etc). But as this is just to provide a set of bindings, you probably only need to call the correct native code function and pass on the parameters :
from numbers import Number
from multipledispatch import dispatch
class Point(tuple):
pass
class Curve(object):
pass
class Circle(object):
"A circle class"
# dispatch(Point, (int, float, Decimal....))
#dispatch(Point, Number)
def __init__(self, centre, radius):
"""Circle(Point, Number): create a circle from a Point and radius."""
print "Circle.__init__(): centre %r, radius %r" % (centre, radius)
#dispatch(Point, Point, Point)
def __init__(self, point1, point2, point3):
"""Circle(Point, Point, Point): create a circle from 3 points."""
print "Circle.__init__(): point1 %r, point2 %r, point3 = %r" % (point1, point2, point3)
#dispatch(Curve, Curve, Curve)
def __init__(self, curve1, curve2, curve3):
"""Circle(Curve, Curve, Curve): create a circle from 3 curves."""
print "Circle.__init__(): curve1 %r, curve2 %r, curve3 = %r" % (curve1, curve2, curve3)
__doc__ = '' if __doc__ is None else '{}\n\n'.format(__doc__)
__doc__ += '\n'.join(f.__doc__ for f in __init__.funcs.values())
>>> print Circle.__doc__
A circle class
Circle(Point, Number): create a circle from a Point and radius.
Circle(Point, Point, Point): create a circle from 3 points.
Circle(Curve, Curve, Curve): create a circle from 3 curves.
>>> for num in 10, 10.22, complex(10.22), True, Decimal(100):
... Circle(Point((10,20)), num)
...
Circle.__init__(): centre (10, 20), radius 10
<__main__.Circle object at 0x1d42fd0>
Circle.__init__(): centre (10, 20), radius 10.22
<__main__.Circle object at 0x1e3d890>
Circle.__init__(): centre (10, 20), radius (10.22+0j)
<__main__.Circle object at 0x1d42fd0>
Circle.__init__(): centre (10, 20), radius True
<__main__.Circle object at 0x1e3d890>
Circle.__init__(): centre (10, 20), radius Decimal('100')
<__main__.Circle object at 0x1d42fd0>
>>> Circle(Curve(), Curve(), Curve())
Circle.__init__(): curve1 <__main__.Curve object at 0x1e3db50>, curve2 <__main__.Curve object at 0x1d42fd0>, curve3 = <__main__.Curve object at 0x1d4b1d0>
<__main__.Circle object at 0x1d4b4d0>
>>> p1=Point((10,20))
>>> Circle(*(p1,)*3)
Circle.__init__(): point1 (10, 20), point2 (10, 20), point3 = (10, 20)
<__main__.Circle object at 0x1e3d890>
>>> Circle()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/mhawke/virtualenvs/urllib3/lib/python2.7/site-packages/multipledispatch/dispatcher.py", line 235, in __call__
func = self.resolve(types)
File "/home/mhawke/virtualenvs/urllib3/lib/python2.7/site-packages/multipledispatch/dispatcher.py", line 184, in resolve
(self.name, str_signature(types)))
NotImplementedError: Could not find signature for __init__: <>

You could use a dictionary, like so
Circle({'points':[p1,p2,p3]})
Circle({'radius':r})
Circle({'curves':[c1,c2,c3])
And the initializer would say
def __init__(args):
if len(args)>1:
raise SomeError("only pass one of points, radius, curves")
if 'points' in args: {blah}
elsif 'radius' in args: {blahblah}
elsif 'curves' in args: {evenmoreblah}
else: raise SomeError("same as above")

One way would be to just write code parse the args yourself. Then you wouldn't have to change the API at all. You could even write a decorator so it'd be reusable:
import functools
def overload(func):
'''Creates a signature from the arguments passed to the decorated function and passes it as the first argument'''
#functools.wraps(func)
def inner(*args):
signature = tuple(map(type, args))
return func(signature, *args)
return inner
def matches(collection, sig):
'''Returns True if each item in collection is an instance of its respective item in signature'''
if len(sig)!=len(collection):
return False
return all(issubclass(i, j) for i,j in zip(collection, sig))
#overload
def Circle1(sig, *args):
if matches(sig, (Point,)*3):
#do stuff with args
print "3 points"
elif matches(sig, (Point, float)):
#as before
print "point, float"
elif matches(sig, (Curve,)*3):
#and again
print "3 curves"
else:
raise TypeError("Invalid argument signature")
# or even better
#overload
def Circle2(sig, *args):
valid_sigs = {(Point,)*3: CircleThroughThreePoints,
(Point, float): CircleCenterRadius,
(Curve,)*3: CircleTangentThreeCurves
}
try:
return (f for s,f in valid_sigs.items() if matches(sig, s)).next()(*args)
except StopIteration:
raise TypeError("Invalid argument signature")
How it appears to API users:
This is the best part. To an API user, they just see this:
>>> help(Circle)
Circle(*args)
Whatever's in Circle's docstring. You should put info here about valid signatures.
They can just call Circle like you showed in your question.
How it works:
The whole idea is to hide the signature-matching from the API. This is accomplished by using a decorator to create a signature, basically a tuple containing the types of each of the arguments, and passing that as the first argument to the functions.
overload:
When you decorate a function with #overload, overload is called with that function as an argument. Whatever is returned (in this case inner) replaces the decorated function. functools.wraps ensures that the new function has the same name, docstring, etc.
Overload is a fairly simple decorator. All it does is make a tuple of the types of each argument and pass that tuple as the first argument to the decorated function.
Circle take 1:
This is the simplest approach. At the beginning of the function, just test the signature against all valid ones.
Circle take 2:
This is a little more fancy. The benefit is that you can define all of your valid signatures together in one place. The return statement uses a generator to filter the matching valid signature from the dictionary, and .next() just gets the first one. Since that entire statement returns a function, you can just stick a () afterwards to call it. If none of the valid signatures match, .next() raises a StopIteration.
All in all, this function just returns the result of the function with the matching signature.
final notes:
One thing you see a lot in this bit of code is the *args construct. When used in a function definition, it just stores all the arguments in a list named "args". Elsewhere, it expands a list named args so that each item becomes an argument to a function (e.g. a = func(*args)).
I don't think it's terribly uncommon to do odd things like this to present clean APIs in Python.

Related

Is there a way to retrieve the specific parameters used in a random torchvision transform?

I can augment my data during training by applying a random transform (rotation/translation/rescaling) but I don't know the value that was selected.
I need to know what values were applied. I can manually set these values, but then I lose a lot of the benefits that torch vision transforms provide.
Is there an easy way to get these values are implement them in a sensible way to apply during training?
Here is an example. I would love to be able print out the rotation angle, translation/rescaling being applied at each image:
import numpy as np
import matplotlib.pyplot as plt
from torchvision import transforms
RandAffine = transforms.RandomAffine(degrees=0, translate=(0.1, 0.1), scale=(0.8, 1.2))
rotate = transforms.RandomRotation(degrees=45)
shift = RandAffine
composed = transforms.Compose([rotate,
shift])
# Apply each of the above transforms on sample.
fig = plt.figure()
sample = np.zeros((28,28))
sample[5:15,7:20] = 255
sample = transforms.ToPILImage()(sample.astype(np.uint8))
title = ['None', 'Rot','Aff','Comp']
for i, tsfrm in enumerate([None,rotate, shift, composed]):
if tsfrm:
t_sample = tsfrm(sample)
else:
t_sample = sample
ax = plt.subplot(1, 5, i + 2)
plt.tight_layout()
ax.set_title(title[i])
ax.imshow(np.reshape(np.array(list(t_sample.getdata())), (-1,28)), cmap='gray')
plt.show()
I'm afraid there is no easy way around it: Torchvision's random transforms utilities are built in such a way that the transform parameters will be sampled when called. They are unique random transforms, in the sense that (1) parameters used are not accessible by the user and (2) the same random transformation is not repeatable.
As of Torchvision 0.8.0, random transforms are generally built with two main functions:
get_params: which will sample based on the transform's hyperparameters (what you have provided when you initialized the transform operator, namely the parameters' range of values)
forward: the function that gets executed when applying the transform. The important part is it gets its parameters from get_params then applies it to the input using the associated deterministic function. For RandomRotation, F.rotate will get called. Similarly, RandomAffine will use F.affine.
One solution to your problem is sampling the parameters from get_params yourself and calling the functional - deterministic - API instead. So you wouldn't be using RandomRotation, RandomAffine, nor any other Random* transformation for that matter.
For instance, let's look at T.RandomRotation (I have removed the comments for conciseness).
class RandomRotation(torch.nn.Module):
def __init__(
self, degrees, interpolation=InterpolationMode.NEAREST, expand=False,
center=None, fill=None, resample=None):
# ...
#staticmethod
def get_params(degrees: List[float]) -> float:
angle = float(torch.empty(1).uniform_(float(degrees[0]), \
float(degrees[1])).item())
return angle
def forward(self, img):
fill = self.fill
if isinstance(img, Tensor):
if isinstance(fill, (int, float)):
fill = [float(fill)] * F._get_image_num_channels(img)
else:
fill = [float(f) for f in fill]
angle = self.get_params(self.degrees)
return F.rotate(img, angle, self.resample, self.expand, self.center, fill)
def __repr__(self):
# ...
With that in mind, here is a possible override to modify T.RandomRotation:
class RandomRotation(T.RandomRotation):
def __init__(*args, **kwargs):
super(RandomRotation, self).__init__(*args, **kwargs) # let super do all the work
self.angle = self.get_params(self.degrees) # initialize your random parameters
def forward(self): # override T.RandomRotation's forward
fill = self.fill
if isinstance(img, Tensor):
if isinstance(fill, (int, float)):
fill = [float(fill)] * F._get_image_num_channels(img)
else:
fill = [float(f) for f in fill]
return F.rotate(img, self.angle, self.resample, self.expand, self.center, fill)
I've essentially copied T.RandomRotation's forward function, the only difference being that the parameters are sampled in __init__ (i.e. once) instead of inside the forward (i.e. on every call). Torchvision's implementation covers all cases, you generally won't need to copy the full forward. In some cases, you can just call the functional version pretty much straight away. For example, if you don't need to set the fill parameters, you can just discard that part and only use:
class RandomRotation(T.RandomRotation):
def __init__(*args, **kwargs):
super(RandomRotation, self).__init__(*args, **kwargs) # let super do all the work
self.angle = self.get_params(self.degrees) # initialize your random parameters
def forward(self): # override T.RandomRotation's forward
return F.rotate(img, self.angle, self.resample, self.expand, self.center)
If you want to override other random transforms you can look at the source code. The API is fairly self-explanatory and you shouldn't have too many issues implementing an override for each transform.

How to combine same variables in classes

Trying to create a n-body simulation and currently creating a particle class. I initialise the class by inputing a number of variables such as position and velocity. I was wondering if I could somehow combine all the same variable into one array. For instance when I call upon the Particle class it saves all the variables but the variables are all about that particular particle and tied to it. I was wondering if I would be able to find a way to return the velocities for all the particles in one array.
class Particle:
Position = np.full((1, 3), 0, dtype=float)
Velocity = np.full((1, 3), 0, dtype=float)
Acceleration = np.full((1, 3), 0, dtype=float)
Name = np.full((1, 1), 0, dtype=float)
Mass = np.full((1, 1), 0, dtype=float)
"""Initialisng all all the data members for each Particle"""
def __init__(self, Name, Mass, initPosition, initVelocity, initAcceleration):
self.Name = Name
self.Mass = Mass
self.Position = np.array(initPosition)
self.Velocity = np.array(initVelocity)
self.Aceleration = np.array(initAcceleration)
def arrays(self, Positions, Velocities, Accelerations, Names, Masses):
Positions.append(self.Position)
Velocities.append(self.Velocity)
Accelerations.append(self.Acceleration)
Names.append(self.Name)
Masses.append(self.Mass)
my second definition "arrays" is trying to to that but unsuccessfully. The aim is so that I can type Positions and a (N,3) matrix is produced upon which I can perform calculations. Is this possible?
I am not sure what you want to do actually:
If you want to update and return all positions/velocities of one Particle object so you can define:
def arrays(self, Positions, Velocities, Accelerations, Names, Masses):
self.Positions = np.append(self.Position, Position)
self.Velocities = np.append(self.Velocity, Velocity)
self.Accelerations = np.append(self.Acceleration, Acceleration)
self.Names = np.append(self.Name, Name)
self.Masses = np.append(self.Mass, Mass)
and then you can access class properties like:
p1 = Particle(...)
p1.Positions
you can update your particle properties from outside and can access it.
However in your case(i guess at least) you will probably need multiple particle objects. So it is better define a new class that takes particles collection[Particle(*args, **kwargs), Particle(*args, **kwargs), ...] as an input and then you can access all the particles properties and do whatever you want.
Actually, 'numpy.ndarray' object has no attribute 'append'.
Use append of numpy:
a = np.array([1, 2])
a = np.append(a, 3)
And note that the result of the execution must be assigned, otherwise nothing will be added.
def arrays(self, Positions, Velocities, Accelerations, Names, Masses):
self.Positions.append(Position)
self.Velocities.append(Velocity)
self.Accelerations.append(Acceleration)
self.Names.append(Name)
self.Masses.append(Mass)

How do I access a class method optionally without creating an instance

I'm relatively new to python and getting my head around OOP. I'm making a class to perform some basic methods on data, but ideally i'd like to access those class methods as regular functions without necessarily creating an instance first.
I have the following code set up for MyMethod containing a single method 'clip_sphere', which takes in xyz coordinates as an array and returns coordinates that are inside a sphere centered at 'center' with radius 'radius'
import numpy as np
class MyMethod:
def __init__(self,xyz):
self.xyz = xyz
def clip_sphere(self, center, radius):
self.xyz = self.xyz - np.array(center).reshape(1,-1)
r = (np.sum(self.xyz**2, axis = 1))**0.5
idx = r < radius
xyz_clip = self.xyz[idx,:]
self.clip = xyz_clip
return xyz_clip
what i would like to do is be able to run clip sphere in two ways, wither by 1:
C = MyMethod(xyz)
xyz_clip = C.clip_sphere(center =[0,0,0],radius = 2)
or simply by calling it as a function like:
xyz_clip = clip_sphere(xyz,center =[0,0,0],radius = 2)
Prefereably without rewriting as an ordinary function. Is this possible to do with some decorators? or is this even possible at all.
EDIT: After looking through some of the answers, I guess what I'm asking is how to get a function like numpy reshape. as this works by both allowing a statement like:
a = np.reshape(np.array([1,2,3]),[3,1])
Which is acting like a function
As well as:
a = np.array([1,2,3])
a.reshape([3,1])
which is acting like a class method
It is sort-of built-in - all you need to do is get the function from the class' namespace:
C = MyMethod(xyz)
xyz_clip = MyMethod.clip_sphere(C, center =[0,0,0], radius = 2)
However, this still requires that you have an instance of the class. The issue is that the code is written to find xyz etc. attributes in a specific named object, the one named self. (There is nothing special about the name self in Python beyond convention.)
If you really need to be able to use xyz for this functionality, then the sane approach is to just write a plain function that handles it:
# At top level
def clip(xyz, center, radius):
xyz -= np.array(center).reshape(1,-1)
r = (np.sum(xyz**2, axis = 1))**0.5
idx = r < radius
return xyz[idx,:]
And then to avoid repetitive code, you can use this to implement the method:
# inside the class
def clip_sphere(self, center, radius):
# This part of the process needs to be repeated, because the
# `xyz` variable during the `clip` call is a separate name.
# However, you should consider whether you *actually* want to modify
# the object's `self.xyz` as a part of this process. My guess is you do not.
self.xyz -= np.array(center).reshape(1,-1)
self.clip = clip(self.xyz, center, radius) # uses the top-level `clip`.
Keeping in mind how descriptors work, you can make a small modification to a properly written clip_sphere method that will let you run the method as either a method, or as a classmethod that accepts coordinates as the first argument:
def clip_sphere(self, center, radius):
xyz = self.xyz if isinstance(self, __class__) else self
xyz -= np.array(center).reshape(1,-1)
r = (np.sum(self.xyz**2, axis = 1))**0.5
idx = r < radius
xyz_clip = self.xyz[idx,:]
if isinstance(self, __class__):
self.clip = xyz_clip
self.xyz = xyz
return xyz_clip
You can call it either as a method:
>>> xyz = ...
>>> MyMethod().clip_sphere(...)
or as a classmethod:
>>> MyMethod.clip_sphere(xyz, ...)
Looks like you want a standalone function, rather than a class/object:
def clip_sphere(xyz, center, radius):
xyz = xyz - np.array(center).reshape(1,-1)
r = (np.sum(xyz**2, axis = 1))**0.5
idx = r < radius
xyz_clip = xyz[idx,:]
clip = xyz_clip
return xyz_clip
You can make a global function named clip_sphere():
def clip_sphere(xyz, center, radius):
return MyMethod(xyz).clip_sphere(center, radius)
But doing this makes me wonder why you need the class at all. From what I can tell that you are doing here, it would probably make more sense to create a Sphere class. This is clearly an object with some attributes: radius and center. Then you can add a clip() method to do what you want:
class Sphere:
def __init__(center, radius):
self.center = center
self.radius = radius
def clip(x, y, z):
# code goes here
Response to edit:
After looking through some of the answers, I guess what I'm asking is how to get a function like numpy reshape. as this works by both allowing a statement like:
a = np.reshape(np.array([1,2,3]),[3,1])
Which is acting like a function As well as:
a = np.array([1,2,3])
a.reshape([3,1])
This is two different functions, not two different ways of calling the same function.

Finding and Connecting Multiple Canvas Items in Python

Background: I have a code which generates the cartesian coordinates of a network of regular shapes (in this case triangles), and then plots the vertices of the shapes on a Tkinter Canvas as small circles. The process is automated and requires only height and width of the network to obtain a canvas output. Each vertex has the tags 'Vertex' and the vertex's number. Problem: I want to automatically connect the vertices of the shapes together (i.e dot to dot), I have looked into using find_closest and find_overlapping methods to do this, but as the network is composed of vertices at angles to one another, I often find find_overlapping to be unreliable (due to relying on a rectangular envelope), and find_closest appears limited to finding only one connection. As the vertices aren't necessarily connected in order, it is not possible to create a loop to simply connect vertex 1 --> vertex 2 etc. Question: Is there a way to efficiently get all of a vertex's neighbouring vertices and then 'connect the dots' without relying on individually creating lines between points using a manual method such as self.c.create_line(vertex_coord[1], vertex_coord[0], fill='black') for each connection? And would it be possible to share a small example of such a code? Thank you in advance for any help!Below is an abbreviated version of the canvas components of my code.Prototype Method:
from data_generator import *
run_coordinate_gen=data_generator.network_coordinates()
run_coordinate_gen.generator_go()
class Network_Canvas:
def __init__(self, canvas):
self.canvas=canvas
canvas.focus_set()
self.canvas.create_oval(Vertex_Position[0], dimensions[0], fill='black', tags=('Vertex1', Network_Tag, Vertex_Tag))
self.canvas.create_oval(Vertex_Position[5], dimensions[5], fill='black', tags=('Vertex2', Network_Tag, Vertex_Tag))
try:
self.canvas.create_line(Line_Position[5] ,Line_Position[0] , fill='black' tags=(Network_Tag,'Line1', Line_Tag )) #Connection Between 1 and 6 (6_1), Line 1
except:
pass
#Note: Line_Position, Dimensions and Vertex_Position are all lists composed of (x,y) cartesian coordinates in this case.
This is of course then replicated for each line and vertex throughout the network, but was only used for 90 vertices. The new version requires orders of magnitude more vertices and I am doing this with:
New Method:
#Import updated coordinate generator and run it as before
class Network_Canvas:
def __init__(self, canvas):
self.canvas=canvas
canvas.focus_set()
for V in range(len(vertex_coord_xy)):
self.canvas.create_text(vertex_coord_xy[V]+Text_Distance, text=V+1, fill='black', tags=(V, 'Text'), font=('Helvetica', '9'))
self.canvas.create_oval(vertex_coord_xy[V],vertex_coord_xy[V]+Diameter, fill='black', outline='black', tags=(V, 'Vertex'))
#loop to fit connections here (?)
I think any kind of nearest-neighbor search is going to be waay more time-intensive than just keeping track of the vertices, and there's no "automatic" connect-the-dots method that I can think of (plus, I don't see why such a method should be any faster than drawing them with create_line). Also, how will a nearest-neighbor search algorithm distinguish between the vertices of two separate, nearby (or overlapping) shapes if you aren't keeping track? Anyhow, in my opinion you've already got the right method; there are probably ways to optimize it.
I think that since your shapes are numerous, and there are complicated things you need to do with them, I would make a class for them, like the one I implemented below. It includes the "click to see neighboring vertices" functionality. All of the following code ran without errors. Image of the output shown below.
import Tkinter as TK
import tkMessageBox
# [Credit goes to #NadiaAlramli](http://stackoverflow.com/a/1625023/1460057) for the grouping code
def group(seq, groupSize):
return zip(*(iter(seq),) * groupSize)
Network_Tag, Vertex_Tag, Line_Tag = "network", "vertex", "line"
class Shape:
def __init__(self, canvas, vertexCoords, vertexDiam):
self.vertexIDs = []
self.perimeterID = None
self.vertexCoords = vertexCoords
self.vertexRadius = vertexDiam/2
self.canvas = canvas
def deleteVertices(self):
for ID in self.vertexIDs:
self.canvas.delete(ID)
self.vertexIDs = []
def bindClickToVertices(self):
coordsGrouped = group(self.vertexCoords, 2)
num = len(coordsGrouped)
for k in range(len(self.vertexIDs)):
others = [coordsGrouped[(k-1)%num], coordsGrouped[(k+1)%num]]
self.canvas.tag_bind(self.vertexIDs[k], '<Button-1>',
lambda *args:tkMessageBox.showinfo("Vertex Click", "Neighboring vertices: "+str(others)))
def drawVertices(self):
for x, y in group(self.vertexCoords, 2):
self.vertexIDs.append(self.canvas.create_oval(x-self.vertexRadius, y-self.vertexRadius, x+self.vertexRadius, y+self.vertexRadius, fill='black', tags=(Network_Tag, Vertex_Tag)))
self.bindClickToVertices()
def updateVertices(self):
self.deleteVertices()
self.drawVertices()
def deletePerimeter(self):
if self.perimeterID is not None:
self.canvas.delete(self.perimeterID)
self.perimeterID = None
def drawPerimeter(self):
print "creating line:", (self.vertexCoords + self.vertexCoords[0:2])
self.perimeterID = self.canvas.create_line(*(self.vertexCoords + self.vertexCoords[0:2]), fill='black', tags=(Network_Tag, Line_Tag))
def updatePerimeter(self):
self.deletePerimeter()
self.drawPerimeter()
def deleteShape(self):
self.deleteVertices()
self.deletePerimeter()
def updateShape(self):
self.updateVertices()
self.updatePerimeter()
It can be used very simply, like this:
root = TK.Tk()
frame = TK.Frame(root)
canvas = TK.Canvas(frame, width=1000, height=1000)
frame.grid()
canvas.grid()
# create a bunch of isoceles triangles in different places:
shapes = []
for dx, dy in zip(range(0,1000, 30), range(0,1000, 30)):
shapes.append(Shape(canvas, [0+dx, 0+dy, 10+dx, 10+dy, 20+dx, 0+dy], 5))
# draw (or redraw) the shapes:
for shape in shapes:
shape.updateShape()
# move one of the shapes and change it to a square
shapes[10].vertexCoords = [50, 10, 60, 10, 60, 20, 50, 20]
shapes[10].updateShape()
# delete all the odd-numbered shapes, just for fun:
for k in range(len(shapes)):
if k%2 == 1:
shape.deleteShape()
root.mainloop()
Output:

NumPy odeint output extra variables

What is the easiest way to save intermediate variables during simulation with odeint in Numpy?
For example:
def dy(y,t)
x = np.rand(3,1)
return y + x.sum()
sim = odeint(dy,0,np.arange(0,1,0.1))
What would be the easiest way to save the data stored in x during simulation? Ideally at the points specified in the t argument passed to odeint.
A handy way to hack odeint, with some caveats, is to wrap your call to odeint in method in a class, with dy as another method, and pass self as an argument to your dy function. For example,
class WrapODE(object):
def __init__(self):
self.y_0 = 0.
self.L_x = []
self.timestep = 0
self.times = np.arange(0., 1., 0.1)
def run(self):
self.L_y = odeint(
self.dy,
self.y_0, self.times,
args=(self,))
#staticmethod
def dy(y, t, self):
""""
Discretized application of dudt
Watch out! Because this is a staticmethod, as required by odeint, self
is the third argument
"""
x = np.random.rand(3,1)
if t >= self.times[self.timestep]:
self.timestep += 1
self.L_x.append(x)
else:
self.L_x[-1] = x
return y + x.sum()
To be clear, this is a hack that is prone to pitfalls. For example, unless odeint is doing Euler stepping, dy is going to get called more times than the number of timesteps you specify. To make sure you get one x for each y, the monkey business in the if t >= self.times[self.timestep]: block picks a spot in an array for storing data for each time value from the times vector. Your particular application might lead to other crazy problems. Be sure to thoroughly validate this method for your application.

Categories

Resources