Hans Petter Langtangen 10 年之前
父节点
当前提交
52f660cc1d
共有 15 个文件被更改,包括 233 次插入31 次删除
  1. 35 6
      README.do.txt
  2. 26 1
      README.md
  3. 7 3
      doc/src/tut/classes.do.txt
  4. 二进制
      examples/FE_comic_strip.pdf
  5. 二进制
      examples/FE_comic_strip.png
  6. 46 11
      examples/osc1.py
  7. 73 0
      examples/osc2.py
  8. 二进制
      examples/pendulum.pdf
  9. 二进制
      examples/pendulum.png
  10. 24 5
      examples/pendulum.py
  11. 二进制
      examples/pendulum2.pdf
  12. 二进制
      examples/pendulum2.png
  13. 二进制
      examples/pendulum_body_diagram.pdf
  14. 二进制
      examples/pendulum_body_diagram.png
  15. 22 5
      pysketcher/shapes.py

+ 35 - 6
README.do.txt

@@ -11,21 +11,26 @@ FIGURE: [doc/src/tut/fig-tut/wheel_on_inclined_plane, width=600 frac=0.6]
 Such figures can easily be *interactively* made using a lot of drawing programs.
 Such figures can easily be *interactively* made using a lot of drawing programs.
 A Pysketcher figure, however, is defined in terms of computer code. This gives
 A Pysketcher figure, however, is defined in terms of computer code. This gives
 a great advantage: geometric features can be parameterized in term
 a great advantage: geometric features can be parameterized in term
-of variables, as here:
+of variables. Geometric variations are then trivially generated, and
+complicated figures can be built as a hierarchy of simpler elements.
+
+Here is a very simple figure that illustrates how geometric features are
+parameterized by variables (H, R, L, etc.):
 
 
 FIGURE: [doc/src/tut/fig-tut/vehicle0_dim, width=600 frac=0.6]
 FIGURE: [doc/src/tut/fig-tut/vehicle0_dim, width=600 frac=0.6]
 
 
-One can then quickly change parameters, here to
+One can then quickly change parameters, below to
 `R=0.5; L=5; H=2` and `R=2; L=7; H=1`, and get new figures that would be
 `R=0.5; L=5; H=2` and `R=2; L=7; H=1`, and get new figures that would be
 tedious to draw manually in an interactive tool.
 tedious to draw manually in an interactive tool.
 
 
 FIGURE: [doc/src/tut/fig-tut/vehicle_v23, width=800]
 FIGURE: [doc/src/tut/fig-tut/vehicle_v23, width=800]
 
 
-Another major feature of Pysketcher is the ability to let animate the
-sketch. Here is an example of a very simple vehicle on a bumpy road,
+Another major feature of Pysketcher is the ability to let the
+sketch be dynamic and make an animation of the time evolution.
+Here is an example of a very simple vehicle on a bumpy road,
 where the solution of a differential equation (upper blue line) is fed
 where the solution of a differential equation (upper blue line) is fed
 back to the sketch to make a vertical displacement of the spring and
 back to the sketch to make a vertical displacement of the spring and
-other objects in the vehicle, "view animation": "http://hplgit.github.io/bumpy/doc/src/mov-bumpy/m2_k1_5_b0_2/index.html" (the animation was created by
+other objects in the vehicle. "View animation": "http://hplgit.github.io/bumpy/doc/src/mov-bumpy/m2_k1_5_b0_2/index.html" (the animation was created by
 "this Pysketcher script": "https://github.com/hplgit/bumpy/blob/master/doc/src/fig-bumpy/bumpy_road_fig.py").
 "this Pysketcher script": "https://github.com/hplgit/bumpy/blob/master/doc/src/fig-bumpy/bumpy_road_fig.py").
 
 
 FIGURE: [http://hplgit.github.io/bumpy/doc/src/mov-bumpy/m2_k1_5_b0_2/tmp_frame_0030.png, width=600]
 FIGURE: [http://hplgit.github.io/bumpy/doc/src/mov-bumpy/m2_k1_5_b0_2/tmp_frame_0030.png, width=600]
@@ -36,6 +41,30 @@ FIGURE: [http://hplgit.github.io/bumpy/doc/src/mov-bumpy/m2_k1_5_b0_2/tmp_frame_
 For an introduction to Pysketcher, see the tutorial in "HTML": "http://hplgit.github.io/pysketcher/doc/pub/pysketcher.html", "Sphinx": "http://hplgit.github.io/pysketcher/doc/pub/html/index.html", or "PDF": "http://hplgit/github.io/pysketcher/doc/pub/pysketcher.pdf" format (or a simplified version of
 For an introduction to Pysketcher, see the tutorial in "HTML": "http://hplgit.github.io/pysketcher/doc/pub/pysketcher.html", "Sphinx": "http://hplgit.github.io/pysketcher/doc/pub/html/index.html", or "PDF": "http://hplgit/github.io/pysketcher/doc/pub/pysketcher.pdf" format (or a simplified version of
 the tutorial in Chapter 9 in "A Primer on Scientific Programming with Python": "http://www.amazon.com/Scientific-Programming-Computational-Science-Engineering/dp/3642549586/ref=sr_1_2?s=books&ie=UTF8&qid=1407225588&sr=1-2&keywords=langtangen", by H. P. Langtangen, Springer, 2014).
 the tutorial in Chapter 9 in "A Primer on Scientific Programming with Python": "http://www.amazon.com/Scientific-Programming-Computational-Science-Engineering/dp/3642549586/ref=sr_1_2?s=books&ie=UTF8&qid=1407225588&sr=1-2&keywords=langtangen", by H. P. Langtangen, Springer, 2014).
 
 
+===== Examples =====
+
+See the `examples` directory for some examples beyond the more basic
+ones in the tutorial.
+For example, a pendulum and its body diagram,
+
+FIGURE: [examples/pendulum2, width=800 frac=1]
+
+can be created by the program "`examples/pendulum.py`": "https://github.com/hplgit/pysketcher/tree/master/examples/pendulum.py".
+
+===== Technology =====
+
+Pysketcher applies Matplotlib to make the drawings, but it is quite
+easy to replace the backend `MatplotlibDraw.py` by similar code utilizing
+TikZ or another plotting package. The Pysketcher software is a thin
+layer basically constructing a tree structure of elements in the
+sketch. A lot of classes are offered for different type of basic
+elements, such as Circle, Rectangle, Text, Text with arrow, Force,
+arbitrary curve, etc.
+Complicated figures can be created by sticking one
+figure into another
+(i.e., hierarchical building of figures by sticking one tree
+structure into another).
+
 ===== Citation =====
 ===== Citation =====
 
 
 If you use Pysketcher and want to cite it, you can either cite this
 If you use Pysketcher and want to cite it, you can either cite this
@@ -90,4 +119,4 @@ Pysketcher was first constructed as a powerful educational example on
 object-oriented programming for the book
 object-oriented programming for the book
 *A Primer on Scientific Programming With Python*, but the tool quickly
 *A Primer on Scientific Programming With Python*, but the tool quickly
 became so useful for the author that it was further developed and
 became so useful for the author that it was further developed and
-heavily used for creating figures in other books by the author.
+heavily used for creating figures in other documents.

+ 26 - 1
README.md

@@ -40,6 +40,31 @@ other objects in the vehicle, [view animation](http://hplgit.github.io/bumpy/doc
 For an introduction to Pysketcher, see the tutorial in [HTML](http://hplgit.github.io/pysketcher/doc/pub/pysketcher.html), [Sphinx](http://hplgit.github.io/pysketcher/doc/pub/html/index.html), or [PDF](http://hplgit/github.io/pysketcher/doc/pub/pysketcher.pdf) format (or a simplified version of
 For an introduction to Pysketcher, see the tutorial in [HTML](http://hplgit.github.io/pysketcher/doc/pub/pysketcher.html), [Sphinx](http://hplgit.github.io/pysketcher/doc/pub/html/index.html), or [PDF](http://hplgit/github.io/pysketcher/doc/pub/pysketcher.pdf) format (or a simplified version of
 the tutorial in Chapter 9 in [A Primer on Scientific Programming with Python](http://www.amazon.com/Scientific-Programming-Computational-Science-Engineering/dp/3642549586/ref=sr_1_2?s=books&ie=UTF8&qid=1407225588&sr=1-2&keywords=langtangen), by H. P. Langtangen, Springer, 2014).
 the tutorial in Chapter 9 in [A Primer on Scientific Programming with Python](http://www.amazon.com/Scientific-Programming-Computational-Science-Engineering/dp/3642549586/ref=sr_1_2?s=books&ie=UTF8&qid=1407225588&sr=1-2&keywords=langtangen), by H. P. Langtangen, Springer, 2014).
 
 
+### Examples
+
+See the `examples` directory for some examples beyond the more basic
+ones in the tutorial.
+For example, a pendulum and its body diagram,
+
+<!-- <img src="examples/pendulum2.png" width=800> -->
+![](examples/pendulum2.png)
+
+can be created by the program [`examples/pendulum.py`](https://github.com/hplgit/pysketcher/tree/master/examples/pendulum.py).
+
+### Technology
+
+Pysketcher applies Matplotlib to make the drawings, but it is quite
+easy to replace the backend `MatplotlibDraw.py` by similar code utilizing
+TikZ or another plotting package. The Pysketcher software is a thin
+layer basically constructing a tree structure of elements in the
+sketch. A lot of classes are offered for different type of basic
+elements, such as Circle, Rectangle, Text, Text with arrow, Force,
+arbitrary curve, etc.
+Complicated figures can be created by sticking one
+figure into another
+(i.e., hierarchical building of figures by sticking one tree
+structure into another).
+
 ### Citation
 ### Citation
 
 
 If you use Pysketcher and want to cite it, you can either cite this
 If you use Pysketcher and want to cite it, you can either cite this
@@ -96,5 +121,5 @@ Pysketcher was first constructed as a powerful educational example on
 object-oriented programming for the book
 object-oriented programming for the book
 *A Primer on Scientific Programming With Python*, but the tool quickly
 *A Primer on Scientific Programming With Python*, but the tool quickly
 became so useful for the author that it was further developed and
 became so useful for the author that it was further developed and
-heavily used for creating figures in other books by the author.
+heavily used for creating figures in other documents.
 
 

+ 7 - 3
doc/src/tut/classes.do.txt

@@ -5,6 +5,9 @@ This section presents many of the basic shapes in Pysketcher:
 `Spring`, `Dashpot`, and `Wavy`.
 `Spring`, `Dashpot`, and `Wavy`.
 Each shape is demonstrated with a figure and a
 Each shape is demonstrated with a figure and a
 unit test that shows how the figure is constructed in Python code.
 unit test that shows how the figure is constructed in Python code.
+These demos rely heavily on the method `draw_dimensions` in
+the shape classes, which annotates the basic drawing of the shape
+with the various geometric parameters that govern the shape.
 
 
 
 
 ===== Axis =====
 ===== Axis =====
@@ -62,8 +65,9 @@ The above figure can be produced by the following code.
 
 
 @@@CODE ../../../pysketcher/shapes.py fromto: def test_Rectangle@drawing_tool.savefig\('tmp_Rectangle
 @@@CODE ../../../pysketcher/shapes.py fromto: def test_Rectangle@drawing_tool.savefig\('tmp_Rectangle
 
 
-The `draw_dimension` method adds explanation of dimensions and various
-important argument in the construction of a shape.
+Note that the `draw_dimension` method adds explanation of dimensions and various
+important argument in the construction of a shape. It adapts the annotations
+to the geometry of the current shape.
 
 
 ===== Triangle =====
 ===== Triangle =====
 
 
@@ -98,7 +102,7 @@ FIGURE: [fig-tut/Spring, width=800 frac=1]
 <linebreak>
 <linebreak>
 <linebreak>
 <linebreak>
 
 
-The code for making this spring is
+The code for making these two springs goes like this:
 
 
 @@@CODE ../../../pysketcher/shapes.py fromto: def test_Spring@drawing_tool.savefig\('tmp_Spring
 @@@CODE ../../../pysketcher/shapes.py fromto: def test_Spring@drawing_tool.savefig\('tmp_Spring
 
 

二进制
examples/FE_comic_strip.pdf


二进制
examples/FE_comic_strip.png


+ 46 - 11
examples/osc1.py

@@ -6,10 +6,11 @@ W = L/6
 
 
 xmax = L
 xmax = L
 drawing_tool.set_coordinate_system(xmin=-L, xmax=xmax,
 drawing_tool.set_coordinate_system(xmin=-L, xmax=xmax,
-                                   ymin=-1, ymax=L,
-                                   axis=True,
+                                   ymin=-1, ymax=L+H,
+                                   axis=False,
                                    instruction_file='tmp_mpl.py')
                                    instruction_file='tmp_mpl.py')
 x = 0
 x = 0
+drawing_tool.set_linecolor('black')
 
 
 def make_dashpot(x):
 def make_dashpot(x):
     d_start = (-L,2*H)
     d_start = (-L,2*H)
@@ -27,21 +28,55 @@ def make_spring(x):
 d = make_dashpot(0)
 d = make_dashpot(0)
 s = make_spring(0)
 s = make_spring(0)
 
 
-M = Rectangle((0,H), 4*H, 4*H)
-left_wall = Rectangle((-L,0),H/10,4*H).set_filled_curves(pattern='/')
+M = Rectangle((0,H), 4*H, 4*H).set_linewidth(4)
+left_wall = Rectangle((-L,0),H/10,L).set_filled_curves(pattern='/')
 ground = Wall(x=[-L/2,L], y=[0,0], thickness=-H/10)
 ground = Wall(x=[-L/2,L], y=[0,0], thickness=-H/10)
 wheel1 = Circle((H,H/2), H/2)
 wheel1 = Circle((H,H/2), H/2)
 wheel2 = wheel1.copy()
 wheel2 = wheel1.copy()
 wheel2.translate(point(2*H, 0))
 wheel2.translate(point(2*H, 0))
+
+fontsize = 18
+text_m = Text('$m$', (2*H, H+2*H), fontsize=fontsize)
+text_kx = Text('$kx$', (-L/2, H+4*H), fontsize=fontsize)
+text_bv = Text('$b\dot x$', (-L/2, H), fontsize=fontsize)
+x_axis = Axis((2*H, L), H, '$x(t)$', fontsize=fontsize,
+              label_spacing=(0.04, -0.01))
+x_axis_start = Line((2*H, L-H/4), (2*H, L+H/4)).set_linewidth(4)
+
 fig = Composition({
 fig = Composition({
-    'dashpot': d, 'spring': s, 'mass': M, 'left wall': left_wall,
-    'ground': ground, 'wheel1': wheel1, 'wheel2': wheel2})
+    'spring': s, 'mass': M, 'left wall': left_wall,
+    'ground': ground, 'wheel1': wheel1, 'wheel2': wheel2,
+    'text_m': text_m, 'text_kx': text_kx,
+    'x_axis': x_axis, 'x_axis_start': x_axis_start})
+
+fig.draw()
+drawing_tool.display()
+drawing_tool.savefig('tmp_oscillator_spring')
+
+drawing_tool.erase()
 
 
+fig['dashpot'] = d
+fig['text_bv'] = text_bv
+
+# or fig = Composition(dict(fig=fig, dashpot=d, text_bv=text_bv))
 fig.draw()
 fig.draw()
-#s.draw()
-print s
-print s.shapes['bar1']['line'].x, s.shapes['bar1']['line'].y
-print s.shapes['bar2']['line'].x, s.shapes['bar2']['line'].y
+
 drawing_tool.display()
 drawing_tool.display()
-drawing_tool.savefig('oscillator')
+drawing_tool.savefig('tmp_oscillator')
+
+drawing_tool.erase()
+
+text_kx = Text('$s(u)$', (-L/2, H+4*H), fontsize=fontsize)
+text_bv = Text('$f(\dot u)$', (-L/2, H), fontsize=fontsize)
+x_axis = Axis((2*H, L), H, '$u(t)$', fontsize=fontsize,
+              label_spacing=(0.04, -0.01))
+F_force = Force((4*H, H+2*H), (4*H+H, H+2*H), '$F(t)$',
+                text_spacing=(0.035, -0.01), text_pos='end', fontsize=fontsize)
+fig['text_kx'] = text_kx
+fig['text_bv'] = text_bv
+fig['x_axis'] = x_axis
+fig['F_force'] = F_force
+fig.draw()
+drawing_tool.savefig('tmp_oscillator_general')
+
 raw_input()
 raw_input()

+ 73 - 0
examples/osc2.py

@@ -0,0 +1,73 @@
+"""As osc.py, but without wheels."""
+from pysketcher import *
+
+L = 12.
+H = L/6
+W = L/6
+
+xmax = L
+drawing_tool.set_coordinate_system(xmin=-L, xmax=xmax,
+                                   ymin=-1, ymax=L+H,
+                                   axis=False,
+                                   instruction_file='tmp_mpl.py')
+x = 0
+drawing_tool.set_linecolor('black')
+
+def make_dashpot(x):
+    d_start = (-L,2*H)
+    d = Dashpot(start=d_start, total_length=L+x, width=W,
+                bar_length=3*H/2, dashpot_length=L/2, piston_pos=H+x)
+    d.rotate(-90, d_start)
+    return d
+
+def make_spring(x):
+    s_start = (-L,4*H)
+    s = Spring(start=s_start, length=L+x, bar_length=3*H/2, teeth=True)
+    s.rotate(-90, s_start)
+    return s
+
+d = make_dashpot(0)
+s = make_spring(0)
+
+M = Rectangle((0,H), 4*H, 4*H).set_linewidth(4)
+left_wall = Rectangle((-L,H),H/10,L-H).set_filled_curves(pattern='/')
+ground = Wall(x=[-L/2,L], y=[H,H], thickness=-H/10)
+
+fontsize = 18
+text_m = Text('$m$', (2*H, H+2*H), fontsize=fontsize)
+text_kx = Text('$s(u)$', (-L/2, H+4*H), fontsize=fontsize)
+text_bv = Text('$f(u)$', (-L/2, H), fontsize=fontsize)
+x_axis = Axis((2*H, L), H, '$u(t)$', fontsize=fontsize,
+              label_spacing=(0.04, -0.01))
+x_axis_start = Line((2*H, L-H/4), (2*H, L+H/4)).set_linewidth(4)
+
+fig = Composition({
+    'spring': s, 'mass': M, 'left wall': left_wall,
+    'ground': ground,
+    'text_m': text_m, 'text_kx': text_kx,
+    'x_axis': x_axis, 'x_axis_start': x_axis_start})
+
+fig.draw()
+drawing_tool.display()
+drawing_tool.savefig('tmp_oscillator2_spring')
+
+drawing_tool.erase()
+
+fig['dashpot'] = d
+fig['text_bv'] = text_bv
+
+# or fig = Composition(dict(fig=fig, dashpot=d, text_bv=text_bv))
+fig.draw()
+
+drawing_tool.display()
+drawing_tool.savefig('tmp_oscillator2')
+
+drawing_tool.erase()
+
+F_force = Force((4*H, H+2*H), (4*H+H, H+2*H), '$F(t)$',
+                text_spacing=(0.035, -0.01), text_pos='end', fontsize=fontsize)
+fig['F_force'] = F_force
+fig.draw()
+drawing_tool.savefig('tmp_oscillator2_force')
+
+raw_input()

二进制
examples/pendulum.pdf


二进制
examples/pendulum.png


+ 24 - 5
examples/pendulum.py

@@ -48,8 +48,6 @@ dims = Composition(
 
 
 fig = Composition({'body': mass, 'rod': rod, 'dims': dims})
 fig = Composition({'body': mass, 'rod': rod, 'dims': dims})
 
 
-#drawing_tool.ax.set_xlim(4,10)
-#drawing_tool.ax.set_ylim(1,8)
 fig.draw()
 fig.draw()
 drawing_tool.display()
 drawing_tool.display()
 drawing_tool.savefig('tmp_pendulum1')
 drawing_tool.savefig('tmp_pendulum1')
@@ -62,7 +60,7 @@ drawing_tool.erase()
 drawing_tool.set_linecolor('black')
 drawing_tool.set_linecolor('black')
 mg_force = Force(mass_pt, mass_pt + L/5*point(0,-1), '$mg$', text_pos='end')
 mg_force = Force(mass_pt, mass_pt + L/5*point(0,-1), '$mg$', text_pos='end')
 rod_force = Force(mass_pt, mass_pt - L/3*unit_vec(rod_vec),
 rod_force = Force(mass_pt, mass_pt - L/3*unit_vec(rod_vec),
-                  '$S$', text_pos='end')
+                  '$S$', text_pos='end', text_spacing=(0.03, 0.01))
 
 
 rod_start = rod.geometric_features()['start']
 rod_start = rod.geometric_features()['start']
 vertical2 = Line(rod_start, rod_start + point(0,-L/3))
 vertical2 = Line(rod_start, rod_start + point(0,-L/3))
@@ -82,11 +80,32 @@ body_diagram = Composition(
      'body': mass, 'm': mass_symbol})
      'body': mass, 'm': mass_symbol})
 
 
 body_diagram.draw()
 body_diagram.draw()
-drawing_tool.display('Body diagram')
+#drawing_tool.display('Body diagram')
 drawing_tool.savefig('tmp_pendulum2')
 drawing_tool.savefig('tmp_pendulum2')
 
 
 drawing_tool.adjust_coordinate_system(body_diagram.minmax_coordinates(), 90)
 drawing_tool.adjust_coordinate_system(body_diagram.minmax_coordinates(), 90)
-drawing_tool.display('Body diagram')
+#drawing_tool.display('Body diagram')
 drawing_tool.savefig('tmp_pendulum3')
 drawing_tool.savefig('tmp_pendulum3')
 
 
+drawing_tool.erase()
+air_force = Force(mass_pt, mass_pt - L/6*unit_vec((rod_vec[1], -rod_vec[0])),
+                  '$\sim|v|v$', text_pos='end', text_spacing=(0.04,0.005))
+
+body_diagram['air'] = air_force
+body_diagram.draw()
+#drawing_tool.display('Body diagram')
+drawing_tool.savefig('tmp_pendulum4')
+
+drawing_tool.erase()
+ir = Force(P, P + L/10*unit_vec(rod_vec),
+           r'${\bf i}_r$', text_pos='end', text_spacing=(0.015,0))
+ith = Force(P, P + L/10*unit_vec((-rod_vec[1], rod_vec[0])),
+           r'${\bf i}_{\theta}$', text_pos='end', text_spacing=(0.02,0.005))
+
+body_diagram['ir'] = ir
+body_diagram['ith'] = ith
+body_diagram.draw()
+#drawing_tool.display('Body diagram')
+drawing_tool.savefig('tmp_pendulum5')
+
 raw_input()
 raw_input()

二进制
examples/pendulum2.pdf


二进制
examples/pendulum2.png


二进制
examples/pendulum_body_diagram.pdf


二进制
examples/pendulum_body_diagram.png


+ 22 - 5
pysketcher/shapes.py

@@ -1348,10 +1348,13 @@ class Axis(Shape):
         Then return `rotation_angle` (in degrees).
         Then return `rotation_angle` (in degrees).
         The `label_spacing` denotes the space between the label
         The `label_spacing` denotes the space between the label
         and the arrow tip as a fraction of the length of the plot
         and the arrow tip as a fraction of the length of the plot
-        in x direction. With `label_alignment` one can place
+        in x direction. A tuple can be given to adjust the position
+        in both the x and y directions (with one parameter, the
+        x position is adjusted).
+        With `label_alignment` one can place
         the axis label text such that the arrow tip is to the 'left',
         the axis label text such that the arrow tip is to the 'left',
         'right', or 'center' with respect to the text field.
         'right', or 'center' with respect to the text field.
-        The `label_spacing` and `label_alignment` parameters can
+        The `label_spacing` and `label_alignment`parameters can
         be used to fine-tune the location of the label.
         be used to fine-tune the location of the label.
         """
         """
         # Arrow is vertical arrow, make it horizontal
         # Arrow is vertical arrow, make it horizontal
@@ -1387,7 +1390,15 @@ class Force(Arrow1):
     def __init__(self, start, end, text, text_spacing=1./60,
     def __init__(self, start, end, text, text_spacing=1./60,
                  fontsize=0, text_pos='start', text_alignment='center'):
                  fontsize=0, text_pos='start', text_alignment='center'):
         Arrow1.__init__(self, start, end, style='->')
         Arrow1.__init__(self, start, end, style='->')
-        spacing = drawing_tool.xrange*text_spacing
+        if isinstance(text_spacing, (tuple,list)):
+            if len(text_spacing) == 2:
+                spacing = point(drawing_tool.xrange*text_spacing[0],
+                                drawing_tool.xrange*text_spacing[1])
+            else:
+                spacing = drawing_tool.xrange*text_spacing[0]
+        else:
+            # just a number, this is x spacing
+            spacing = drawing_tool.xrange*text_spacing
         start, end = arr2D(start), arr2D(end)
         start, end = arr2D(start), arr2D(end)
 
 
         # Two cases: label at bottom of line or top, need more
         # Two cases: label at bottom of line or top, need more
@@ -1400,12 +1411,18 @@ class Force(Arrow1):
                 spacing_dir = unit_vec(start - end)
                 spacing_dir = unit_vec(start - end)
                 if upward:
                 if upward:
                     spacing *= 1.7
                     spacing *= 1.7
-                text_pos = start + spacing*spacing_dir
+                if isinstance(spacing, (int, float)):
+                    text_pos = start + spacing*spacing_dir
+                else:
+                    text_pos = start + spacing
             elif text_pos == 'end':
             elif text_pos == 'end':
                 spacing_dir = unit_vec(end - start)
                 spacing_dir = unit_vec(end - start)
                 if downward:
                 if downward:
                     spacing *= 1.7
                     spacing *= 1.7
-                text_pos = end + spacing*spacing_dir
+                if isinstance(spacing, (int, float)):
+                    text_pos = end + spacing*spacing_dir
+                else:
+                    text_pos = end + spacing
         self.shapes['text'] = Text(text, text_pos, fontsize=fontsize,
         self.shapes['text'] = Text(text, text_pos, fontsize=fontsize,
                                    alignment=text_alignment)
                                    alignment=text_alignment)