wrap_sketcher.txt 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383
  1. .. Automatically generated reST file from Doconce source
  2. (http://code.google.com/p/doconce/)
  3. Using Pysketcher to Create Principal Sketches of Physics Problems
  4. =================================================================
  5. :Author: Hans Petter Langtangen
  6. :Date: Apr 1, 2012
  7. *Abstract.* Pysketcher is a Python package which allows principal sketches of
  8. physics and mechanics problems to be realized through short programs
  9. instead of interactive (and potentially tedious and inaccurate)
  10. drawing. Elements of the sketch, such as lines, circles, angles,
  11. forces, coordinate systems, etc., are realized as objects and
  12. collected in hierarchical structures. Parts of the hierarchical
  13. structures can easily change line styles and colors, or be copied,
  14. scaled, translated, and rotated. These features make it
  15. straightforward to move parts of the sketch to create animation,
  16. usually in accordance with the physics of the underlying problem.
  17. Exact dimensioning of the elements in the sketch is trivial to obtain
  18. since distances are specified in computer code.
  19. Pysketcher is easy to learn from a number of examples. Beyond
  20. essential Python programming and a knowledge about mechanics problems,
  21. no further background is required.
  22. .. Task (can be questions): make sketches of physical problems, see fig
  23. .. through user-friendly composition of basic shapes
  24. .. Desired knowledge: plotting curves, basic OO (ch. X.Y, ...)
  25. .. Required knowledge?
  26. .. Learning Goals: these targets the inner workings of pysketcher,
  27. .. which is just a part of this document...
  28. A First Glimpse of Pysketcher
  29. =============================
  30. Formulation of physical problems makes heavy use of *principal sketches*
  31. such as the one in Figure :ref:`sketcher:fig:inclinedplane`.
  32. This particular sketch illustrates the classical mechanics problem
  33. of a rolling wheel on an inclined plane.
  34. The figure
  35. is made up many individual elements: a rectangle
  36. filled with a pattern (the inclined plane), a hollow circle with color
  37. (the wheel), arrows with label (the :math:`N` and :math:`Mg` forces, and the :math:`x`
  38. axis), an angle with symbol :math:`\theta`, and a dashed line indicating the
  39. starting location of the wheel. Drawing software and plotting
  40. programs can produce such figures quite easily in principle, but the
  41. amount of details the user needs to control with the mouse can be
  42. substantial. Software more tailored to producing sketches of this type
  43. would work with more convenient abstractions, such as circle, wall,
  44. angle, force arrow, axis, and so forth. And as soon we start
  45. *programming* to construct the figure we get a range of other
  46. powerful tools at disposal. For example, we can easily translate and
  47. rotate parts of the figure and make an animation that illustrates the
  48. physics of the problem.
  49. .. _sketcher:fig:inclinedplane:
  50. .. figure:: figs-sketcher/wheel_on_inclined_plane.png
  51. :width: 600
  52. *Sketch of a physics problem*
  53. Basic Construction of Sketches
  54. ------------------------------
  55. Before attacking real-life sketches as in Figure :ref:`sketcher:fig1`
  56. we focus on the significantly simpler drawing shown in
  57. in Figure :ref:`sketcher:fig:vehicle0`. This toy sketch consists of
  58. several elements: two circles, two rectangles, and a "ground" element.
  59. .. _sketcher:fig:vehicle0:
  60. .. figure:: figs-sketcher/vehicle0_dim.png
  61. :width: 600
  62. *Sketch of a simple figure*
  63. Basic Drawing
  64. ~~~~~~~~~~~~~
  65. A typical program creating these five elements is shown next.
  66. After importing the ``pysketcher`` package, the first task is always to
  67. define a coordinate system. Some graphics operations are done with
  68. a helper object called ``drawing_tool`` (imported from ``pysketcher``).
  69. With the drawing area in place we can make the first ``Circle`` object
  70. in an intuitive fashion:
  71. .. code-block:: python
  72. from pysketcher import *
  73. R = 1 # radius of wheel
  74. L = 4 # distance between wheels
  75. H = 2 # height of vehicle body
  76. w_1 = 5 # position of front wheel
  77. drawing_tool.set_coordinate_system(xmin=0, xmax=w_1 + 2*L + 3*R,
  78. ymin=-1, ymax=2*R + 3*H)
  79. wheel1 = Circle(center=(w_1, R), radius=R)
  80. By using symbols for characteristic lengths in the drawing, rather than
  81. absolute lengths, it is easier
  82. to change dimensions later.
  83. To translate the geometric information about the ``wheel1`` object to
  84. instructions for the plotting engine (in this case Matplotlib), one calls the
  85. ``wheel1.draw()``. To display all drawn objects, one issues
  86. ``drawing_tool.display()``. The typical steps are hence:
  87. .. code-block:: python
  88. wheel1 = Circle(center=(w_1, R), radius=R)
  89. wheel1.draw()
  90. # Define other objects and call their draw() methods
  91. drawing_tool.display()
  92. drawing_tool.savefig('tmp.png') # store picture
  93. The next wheel can be made by taking a copy of ``wheel1`` and
  94. translating the object a distance (to the right) described by the
  95. vector :math:`(4,0)`:
  96. .. code-block:: python
  97. wheel2 = wheel1.copy()
  98. wheel2.translate((L,0))
  99. The two rectangles are made in an intuitive way:
  100. .. code-block:: python
  101. under = Rectangle(lower_left_corner=(w_1-2*R, 2*R),
  102. width=2*R + L + 2*R, height=H)
  103. over = Rectangle(lower_left_corner=(w_1, 2*R + H),
  104. width=2.5*R, height=1.25*H)
  105. Groups of Objects
  106. ~~~~~~~~~~~~~~~~~
  107. Instead of calling the ``draw`` method of every object, we can
  108. group objects and call ``draw``, or perform other operations, for
  109. the whole group. For example, we may collect the two wheels
  110. in a ``wheels`` group and the ``over`` and ``under`` rectangles
  111. in a ``body`` group. The whole vehicle is a composition
  112. of its ``wheels`` and ``body`` groups. The codes goes like
  113. .. code-block:: python
  114. wheels = Composition({'wheel1': wheel1, 'wheel2': wheel2})
  115. body = Composition({'under': under, 'over': over})
  116. vehicle = Composition({'wheels': wheels, 'body': body})
  117. The ground is illustrated by an object of type ``Wall``,
  118. mostly used to indicate walls in sketches of mechanical systems.
  119. A ``Wall`` takes the ``x`` and ``y`` coordinates of some curve,
  120. and a ``thickness`` parameter, and creates a "thick" curve filled
  121. with a simple pattern. In this case the curve is just a flat
  122. line so the construction is made of two points on the
  123. ground line (:math:`(w_1-L,0)` and :math:`(w_1+3L,0)`):
  124. .. code-block:: python
  125. ground = Wall(x=[w_1 - L, w_1 + 3*L], y=[0, 0], thickness=-0.3*R)
  126. The negative thickness makes the pattern-filled rectangle appear below
  127. the defined line, otherwise it appears above.
  128. We may now collect all the objects in a "top" object that contains
  129. the whole figure:
  130. .. code-block:: python
  131. fig = Composition({'vehicle': vehicle, 'ground': ground})
  132. fig.draw() # send all figures to plotting backend
  133. drawing_tool.display()
  134. drawing_tool.savefig('tmp.png')
  135. The ``fig.draw()`` call will visit
  136. all subgroups, their subgroups,
  137. and so in the hierarchical tree structure that we have collected,
  138. and call ``draw`` for every object.
  139. Changing Line Styles and Colors
  140. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  141. Controlling the line style, line color, and line width is
  142. fundamental when designing figures. The ``pysketcher``
  143. package allows the user to control such properties in
  144. single objects, but also set global properties that are
  145. used if the object has no particular specification of
  146. the properties. Setting the global properties are done like
  147. .. code-block:: python
  148. drawing_tool.set_linestyle('dashed')
  149. drawing_tool.set_linecolor('black')
  150. drawing_tool.set_linewidth(4)
  151. At the object level the properties are specified in a similar
  152. way:
  153. .. code-block:: python
  154. wheel1.set_linestyle('solid')
  155. wheel1.set_linecolor('red')
  156. and so on.
  157. Geometric figures can be specified as *filled*, either with a color or with a
  158. special visual pattern:
  159. .. code-block:: py
  160. # Set filling of all curves
  161. drawing_tool.set_filled_curves(color='blue', pattern='/')
  162. # Turn off filling of all curves
  163. drawing_tool.set_filled_curves(False)
  164. # Fill the wheel with red color
  165. wheel1.set_filled_curves('red')
  166. .. http://packages.python.org/ete2/ for visualizing tree structures!
  167. The Figure Composition as an Object Hierarchy
  168. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  169. The composition of objects is hierarchical, as in a family, where
  170. each object has a parent and a number of children. Do a
  171. ``print fig`` to display the relations:
  172. .. code-block:: python
  173. ground
  174. wall
  175. vehicle
  176. body
  177. over
  178. rectangle
  179. under
  180. rectangle
  181. wheels
  182. wheel1
  183. arc
  184. wheel2
  185. arc
  186. The indentation reflects how deep down in the hierarchy (family)
  187. we are.
  188. This output is to be interpreted as follows:
  189. * ``fig`` contains two objects, ``ground`` and ``vehicle``
  190. * ``ground`` contains an object ``wall``
  191. * ``vehicle`` contains two objects, ``body`` and ``wheels``
  192. * ``body`` contains two objects, ``over`` and ``under``
  193. * ``wheels`` contains two objects, ``wheel1`` and ``wheel2``
  194. More detailed information can be printed by
  195. .. code-block:: python
  196. print fig.show_hierarchy('std')
  197. yielding the output
  198. .. code-block:: python
  199. ground (Wall):
  200. wall (Curve): 4 coords fillcolor='white' fillpattern='/'
  201. vehicle (Composition):
  202. body (Composition):
  203. over (Rectangle):
  204. rectangle (Curve): 5 coords
  205. under (Rectangle):
  206. rectangle (Curve): 5 coords
  207. wheels (Composition):
  208. wheel1 (Circle):
  209. arc (Curve): 181 coords
  210. wheel2 (Circle):
  211. arc (Curve): 181 coords
  212. Here we can see the class type for each figure object, how many
  213. coordinates that are involved in basic figures (``Curve`` objects), and
  214. special settings of the basic figure (fillcolor, line types, etc.).
  215. For example, ``wheel2`` is a ``Circle`` object consisting of an ``arc``,
  216. which is a ``Curve`` object consisting of 181 coordinates (the
  217. points needed to draw a smooth circle). The ``Curve`` objects are the
  218. only objects that really holds specific coordinates to be drawn.
  219. The other object types are just compositions used to group
  220. parts of the complete figure.
  221. One can also get a graphical overview of the hierarchy of figure objects
  222. that build up a particular figure ``fig``.
  223. Just call ``fig.graphviz_dot('fig')`` to produce a file ``fig.dot`` in
  224. the *dot format*. This file contains relations between parent and
  225. child objects in the figure and can be turned into an image,
  226. as in Figure :ref:`sketcher:fig:vehicle0:hier1`, by
  227. running the ``dot`` program:
  228. .. code-block:: console
  229. Terminal> dot -Tpng -o fig.png fig.dot
  230. .. _sketcher:fig:vehicle0:hier1:
  231. .. figure:: figs-sketcher/vehicle0_hier1.png
  232. :width: 500
  233. *Hierarchical relation between figure objects*
  234. The call ``fig.graphviz_dot('fig', classname=True)`` makes a ``fig.dot`` file
  235. where the class type of each object is also visible, see
  236. Figure :ref:`sketcher:fig:vehicle0:hier2`. The ability to write out the
  237. object hierarchy or view it graphically can be of great help when
  238. working with complex figures that involve layers of subfigures.
  239. .. _sketcher:fig:vehicle0:hier2:
  240. .. figure:: figs-sketcher/vehicle0_hier2.png
  241. :width: 500
  242. *Hierarchical relation between figure objects, including their class names*
  243. Any of the objects can in the program be reached through their names, e.g.,
  244. .. code-block:: python
  245. fig['vehicle']
  246. fig['vehicle']['wheels']
  247. fig['vehicle']['wheels']['wheel2']
  248. fig['vehicle']['wheels']['wheel2']['arc']
  249. fig['vehicle']['wheels']['wheel2']['arc'].x # x coords
  250. fig['vehicle']['wheels']['wheel2']['arc'].y # y coords
  251. fig['vehicle']['wheels']['wheel2']['arc'].linestyle
  252. fig['vehicle']['wheels']['wheel2']['arc'].linetype
  253. Grabbing a part of the figure this way is very handy for
  254. changing properties of that part, for example, colors, line styles
  255. (see Figure :ref:`sketcher:fig:vehicle0:v2`):
  256. .. code-block:: python
  257. fig['vehicle']['wheels'].set_filled_curves('blue')
  258. fig['vehicle']['wheels'].set_linewidth(6)
  259. fig['vehicle']['wheels'].set_linecolor('black')
  260. fig['vehicle']['body']['under'].set_filled_curves('red')
  261. fig['vehicle']['body']['over'].set_filled_curves(pattern='/')
  262. fig['vehicle']['body']['over'].set_linewidth(14)
  263. fig['vehicle']['body']['over']['rectangle'].linewidth = 4
  264. The last line accesses the ``Curve`` object directly, while the line above,
  265. accesses the ``Rectangle`` object which will then set the linewidth of
  266. its ``Curve`` object, and other objects if it had any.
  267. The result of the actions above is shown in Figure :ref:`sketcher:fig:vehicle0:v2`.
  268. .. _sketcher:fig:vehicle0:v2:
  269. .. figure:: figs-sketcher/vehicle0.png
  270. :width: 700
  271. *Left: Basic line-based drawing. Right: Thicker lines and filled parts*
  272. We can also change position of parts of the figure and thereby make
  273. animations, as shown next.
  274. Animation: Translating the Vehicle
  275. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  276. Can we make our little vehicle roll? A first attempt will be to
  277. fake rolling by just displacing all parts of the vehicle.
  278. The relevant parts constitute the ``fig['vehicle']`` object.
  279. This part of the figure can be translated, rotated, and scaled.
  280. A translation along the ground means a translation in :math:`x` direction,
  281. say a length :math:`L` to the right:
  282. .. code-block:: python
  283. fig['vehicle'].translate((L,0))
  284. You need to erase, draw, and display to see the movement:
  285. .. code-block:: python
  286. drawing_tool.erase()
  287. fig.draw()
  288. drawing_tool.display()
  289. Without erasing the old position of the vehicle will remain in
  290. the figure so you get two vehicles. Without ``fig.draw()`` the
  291. new coordinates of the vehicle will not be communicated to
  292. the drawing tool, and without calling display the updated
  293. drawing will not be visible.
  294. Let us make a velocity function and move the object according
  295. to that velocity in small steps of time:
  296. .. code-block:: python
  297. def v(t):
  298. return -8*R*t*(1 - t/(2*R))
  299. animate(fig, tp, action)
  300. For small time steps ``dt`` the corresponding displacement is
  301. well approximated by ``dt*v(t)`` (we could integrate the velocity
  302. to obtain the exact position, but we would anyway need to
  303. calculate the displacement from time step to time step).
  304. The ``animate`` function takes as arguments some figure ``fig``, a set of
  305. time points ``tp``, and a user function ``action``,
  306. and then a new figure is drawn for each time point and the user
  307. can through the provided ``action`` function modify desired parts
  308. of the figure. Here the ``action`` function will move the vehicle:
  309. .. code-block:: python
  310. def move_vehicle(t, fig):
  311. x_displacement = dt*v(t)
  312. fig['vehicle'].translate((x_displacement, 0))
  313. Defining a set of time points for the frames in the animation
  314. and performing the animation is done by
  315. .. code-block:: python
  316. import numpy
  317. tp = numpy.linspace(0, 2*R, 25)
  318. dt = tp[1] - tp[0] # time step
  319. animate(fig, tp, move_vehicle, pause_per_frame=0.2)
  320. The ``pause_per_frame`` adds a pause, here 0.2 seconds, between
  321. each frame.
  322. We can also make a movie file of the animation:
  323. .. code-block:: python
  324. files = animate(fig, tp, move_vehicle, moviefiles=True,
  325. pause_per_frame=0.2)
  326. The ``files`` variable holds a string with the family of
  327. files constituting the frames in the movie, here
  328. ``'tmp_frame*.png'``. Making a movie out of the individual
  329. frames can be done in many ways.
  330. A simple approach is to make an animated GIF file with help of
  331. ``convert``, a program in the ImageMagick software suite:
  332. .. code-block:: console
  333. Terminal> convert -delay 20 tmp_frame*.png anim.gif
  334. Terminal> animate anim.gif # play movie
  335. The delay between frames governs the speed of the movie.
  336. The ``anim.gif`` file can be embedded in a web page and shown as
  337. a movie the page is loaded into a web browser (just insert
  338. ``<img src="anim.gif">`` in the HTML code to play the GIF animation).
  339. The tool ``ffmpeg`` can alternatively be used, e.g.,
  340. .. code-block:: console
  341. Terminal> ffmpeg -i "tmp_frame_%04d.png" -b 800k -r 25 \
  342. -vcodec mpeg4 -y -qmin 2 -qmax 31 anim.mpeg
  343. An easy-to-use interface to movie-making tools is provided by the
  344. SciTools package:
  345. .. code-block:: python
  346. from scitools.std import movie
  347. # HTML page showing individual frames
  348. movie(files, encoder='html', output_file='anim.html')
  349. # Standard GIF file
  350. movie(files, encoder='convert', output_file='anim.gif')
  351. # AVI format
  352. movie('tmp_*.png', encoder='ffmpeg', fps=4,
  353. output_file='anim.avi') # requires ffmpeg package
  354. # MPEG format
  355. movie('tmp_*.png', encoder='ffmpeg', fps=4,
  356. output_file='anim2.mpeg', vodec='mpeg2video')
  357. # or
  358. movie(files, encoder='ppmtompeg', fps=24,
  359. output_file='anim.mpeg') # requires the netpbm package
  360. When difficulties with encoders and players arise, the simple
  361. web page for showing a movie, here ``anim.html`` (generated by the
  362. first ``movie`` command above), is a safe method that you always
  363. can rely on.
  364. You can try loading ``anim.html`` into a web browser, after first
  365. having run the present example in the file
  366. ``vehicle0.py` <http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/vehicle0.py>`_. Alternatively, you can view a ready-made `movie <http://hplgit.github.com/pysketcher/doc/src/sketcher/movies-sketcher/anim.html_vehicle0/anim.html>`_.
  367. .. _sketcher:vehicle1:anim:
  368. Animation: Rolling the Wheels
  369. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  370. It is time to show rolling wheels. To this end, we make somewhat
  371. more complicated wheels with spokes as on a bicyle, formed by
  372. two crossing lines, see Figure :ref:`sketcher:fig:vehicle1`.
  373. The construction of the wheels will now involve a circle
  374. and two lines:
  375. .. code-block:: python
  376. wheel1 = Composition({
  377. 'wheel': Circle(center=(w_1, R), radius=R),
  378. 'cross': Composition({'cross1': Line((w_1,0), (w_1,2*R)),
  379. 'cross2': Line((w_1-R,R), (w_1+R,R))})})
  380. wheel2 = wheel1.copy()
  381. wheel2.translate((L,0))
  382. Observe that ``wheel1.copy()`` copies all the objects that make
  383. up the first wheel, and ``wheel2.translate`` translates all
  384. the copied objects.
  385. .. _sketcher:fig:vehicle1:
  386. .. figure:: figs-sketcher/vehicle1.png
  387. :width: 400
  388. *Wheels with spokes to show rotation*
  389. The ``move_vehicle`` function now needs to displace all the objects in the
  390. entire vehicle and also rotate the ``cross1`` and ``cross2``
  391. objects in both wheels.
  392. The rotation angle follows from the fact that the arc length
  393. of a rolling wheel equals the displacement of the center of
  394. the wheel, leading to a rotation angle
  395. .. code-block:: python
  396. angle = - x_displacement/R
  397. With ``w_1`` tracking the :math:`x` coordinate of the center
  398. of the front wheel, we can rotate that wheel by
  399. .. code-block:: python
  400. w1 = fig['vehicle']['wheels']['wheel1']
  401. from math import degrees
  402. w1.rotate(degrees(angle), center=(w_1, R))
  403. The ``rotate`` function takes two parameters: the rotation angle
  404. (in degrees) and the center point of the rotation, which is the
  405. center of the wheel in this case. The other wheel is rotated by
  406. .. code-block:: python
  407. w2 = fig['vehicle']['wheels']['wheel2']
  408. w2.rotate(degrees(angle), center=(w_1 + L, R))
  409. That is, the angle is the same, but the rotation point is different.
  410. The update of the center point is done by ``w_1 += displacement[0]``.
  411. The complete ``move_vehicle`` function then becomes
  412. .. code-block:: python
  413. w_1 = w_1 + L # start position
  414. def move_vehicle(t, fig):
  415. x_displacement = dt*v(t)
  416. fig['vehicle'].translate((x_displacement, 0))
  417. # Rotate wheels
  418. global w_1
  419. w_1 += x_displacement
  420. # R*angle = -x_displacement
  421. angle = - x_displacement/R
  422. w1 = fig['vehicle']['wheels']['wheel1']
  423. w1.rotate(degrees(angle), center=(w_1, R))
  424. w2 = fig['vehicle']['wheels']['wheel2']
  425. w2.rotate(degrees(angle), center=(w_1 + L, R))
  426. The complete example is found in the file
  427. ``vehicle1.py` <http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/vehicle1.py>`_. You may run this file or watch a `ready-made movie <http://hplgit.github.com/pysketcher/doc/src/sketcher/movies-sketcher/anim.html_vehicle1/anim.html>`_.
  428. The advantages with making figures this way through programming,
  429. rather than using interactive drawing programs, are numerous. For
  430. example, the objects are parameterized by variables so that various
  431. dimensions can easily be changed. Subparts of the figure, possible
  432. involving a lot of figure objects, can change
  433. color, linetype, filling or other properties through a *single* function
  434. call. Subparts of the figure can be rotated, translated, or scaled.
  435. Subparts of the figure can also be copied and moved to other parts of the
  436. drawing area. However, the single most important feature is probably
  437. the ability to make animations governed by mathematical formulas or
  438. data coming from physics simulations of the problem sketched in
  439. the drawing, as very simplistically shown in the example above.
  440. Inner Workings of the Pysketcher Tool
  441. =====================================
  442. We shall now explain how we can, quite easily, realize software with
  443. the capabilities demonstrated in the previous examples. Each object in
  444. the figure is represented as a class in a class hierarchy. Using
  445. inheritance, classes can inherit properties from parent classes and
  446. add new geometric features.
  447. Class programming is a key technology for realizing Pysketcher.
  448. As soon as some classes are established, more are easily
  449. added. Enhanced functionality for all the classes is also easy to
  450. implement in common, generic code that can immediately be shared by
  451. all present and future classes. The fundamental data structure
  452. involved in the ``pysketcher`` package is a hierarchical tree, and much
  453. of the material on implementation issues targets how to traverse tree
  454. structures with recursive function calls in object hierarchies. This
  455. topic is of key relevance in a wide range of other applications as
  456. well. In total, the inner workings of Pysketcher constitute an
  457. excellent example on the power of class programming.
  458. Example of Classes for Geometric Objects
  459. ----------------------------------------
  460. We introduce class ``Shape`` as superclass for all specialized objects
  461. in a figure. This class does not store any data, but provides a
  462. series of functions that add functionality to all the subclasses.
  463. This will be shown later.
  464. Simple Geometric Objects
  465. ~~~~~~~~~~~~~~~~~~~~~~~~
  466. One simple subclass is ``Rectangle``:
  467. .. code-block:: python
  468. class Rectangle(Shape):
  469. def __init__(self, lower_left_corner, width, height):
  470. p = lower_left_corner # short form
  471. x = [p[0], p[0] + width,
  472. p[0] + width, p[0], p[0]]
  473. y = [p[1], p[1], p[1] + height,
  474. p[1] + height, p[1]]
  475. self.shapes = {'rectangle': Curve(x,y)}
  476. Any subclass of ``Shape`` will have a constructor which takes
  477. geometric information about the shape of the object and
  478. creates a dictionary ``self.shapes`` with the shape built of
  479. simpler shapes. The most fundamental shape is ``Curve``, which is
  480. just a collection of :math:`(x,y)` coordinates in two arrays ``x`` and ``y``.
  481. Drawing the ``Curve`` object is a matter of plotting ``y`` versus ``x``.
  482. The ``Rectangle`` class illustrates how the constructor takes information
  483. about the lower left corner, the width and the height, and
  484. creates coordinate arrays ``x`` and ``y`` consisting of the four corners,
  485. plus the first one repeated such that plotting ``x`` and ``y`` will
  486. form a closed four-sided rectangle. This construction procedure
  487. demands that the rectangle will always be aligned with the :math:`x` and
  488. :math:`y` axis. However, we may easily rotate the rectangle about
  489. any point once the object is constructed.
  490. Class ``Line`` constitutes a similar example:
  491. .. code-block:: python
  492. class Line(Shape):
  493. def __init__(self, start, end):
  494. x = [start[0], end[0]]
  495. y = [start[1], end[1]]
  496. self.shapes = {'line': Curve(x, y)}
  497. Here we only need two points, the start and end point on the line.
  498. However, we may add some useful functionality, e.g., the ability
  499. to give an :math:`x` coordinate and have the class calculate the
  500. corresponding :math:`y` coordinate:
  501. .. code-block:: python
  502. def __call__(self, x):
  503. """Given x, return y on the line."""
  504. x, y = self.shapes['line'].x, self.shapes['line'].y
  505. self.a = (y[1] - y[0])/(x[1] - x[0])
  506. self.b = y[0] - self.a*x[0]
  507. return self.a*x + self.b
  508. Unfortunately, this is too simplistic because vertical lines cannot
  509. be handled (infinite ``self.a``). The source code of ``Line`` therefore
  510. provides a more general solution at the cost of significantly
  511. longer code with more tests.
  512. A circle gives us somewhat increased complexity. Again we represent
  513. the geometric object by a ``Curve`` object, but this time the ``Curve``
  514. object needs to store a large number of points on the curve such
  515. that a plotting program produces a visually smooth curve.
  516. The points on the circle must be calculated manually in the constructor
  517. of class ``Circle``. The formulas for points :math:`(x,y)` on a curve with radius
  518. :math:`R` and center at :math:`(x_0, y_0)` are given by
  519. .. math::
  520. x &= x_0 + R\cos (t),\\
  521. y &= y_0 + R\sin (t),
  522. where :math:`t\in [0, 2\pi]`. A discrete set of :math:`t` values in this
  523. interval gives the corresponding set of :math:`(x,y)` coordinates on
  524. the circle. The user must specify the resolution, i.e., the number
  525. of :math:`t` values, or equivalently, points on the circle. The circle's
  526. radius and center must of course also be specified.
  527. We can write the ``Circle`` class as
  528. .. code-block:: python
  529. class Circle(Shape):
  530. def __init__(self, center, radius, resolution=180):
  531. self.center, self.radius = center, radius
  532. self.resolution = resolution
  533. t = linspace(0, 2*pi, resolution+1)
  534. x0 = center[0]; y0 = center[1]
  535. R = radius
  536. x = x0 + R*cos(t)
  537. y = y0 + R*sin(t)
  538. self.shapes = {'circle': Curve(x, y)}
  539. As in class ``Line`` we can offer the possibility to give an angle
  540. :math:`\theta` (equivalent to :math:`t` in the formulas above)
  541. and then get the corresponding :math:`x` and :math:`y` coordinates:
  542. .. code-block:: python
  543. def __call__(self, theta):
  544. """Return (x, y) point corresponding to angle theta."""
  545. return self.center[0] + self.radius*cos(theta), \
  546. self.center[1] + self.radius*sin(theta)
  547. There is one flaw with this method: it yields illegal values after
  548. a translation, scaling, or rotation of the circle.
  549. A part of a circle, an arc, is a frequent geometric object when
  550. drawing mechanical systems. The arc is constructed much like
  551. a circle, but :math:`t` runs in :math:`[\theta_0, \theta_1]`. Giving
  552. :math:`\theta_1` and :math:`\theta_2` the slightly more descriptive names
  553. ``start_angle`` and ``arc_angle``, the code looks like this:
  554. .. code-block:: python
  555. class Arc(Shape):
  556. def __init__(self, center, radius,
  557. start_angle, arc_angle,
  558. resolution=180):
  559. self.center = center
  560. self.radius = radius
  561. self.start_angle = start_angle*pi/180 # radians
  562. self.arc_angle = arc_angle*pi/180
  563. self.resolution = resolution
  564. t = linspace(self.start_angle,
  565. self.start_angle + self.arc_angle,
  566. resolution+1)
  567. x0 = center[0]; y0 = center[1]
  568. R = radius
  569. x = x0 + R*cos(t)
  570. y = y0 + R*sin(t)
  571. self.shapes = {'arc': Curve(x, y)}
  572. Having the ``Arc`` class, a ``Circle`` can alternatively befined as
  573. a subclass specializing the arc to a circle:
  574. .. code-block:: python
  575. class Circle(Arc):
  576. def __init__(self, center, radius, resolution=180):
  577. Arc.__init__(self, center, radius, 0, 360, resolution)
  578. A wall is about drawing a curve, displacing the curve vertically by
  579. some thickness, and then filling the space between the curves
  580. by some pattern. The input is the ``x`` and ``y`` coordinate arrays
  581. of the curve and a thickness parameter. The computed coordinates
  582. will be a polygon: going along the originally curve and then back again
  583. along the vertically displaced curve. The relevant code becomes
  584. .. code-block:: python
  585. class CurveWall(Shape):
  586. def __init__(self, x, y, thickness):
  587. # User's curve
  588. x1 = asarray(x, float)
  589. y1 = asarray(y, float)
  590. # Displaced curve (according to thickness)
  591. x2 = x1
  592. y2 = y1 + thickness
  593. # Combine x1,y1 with x2,y2 reversed
  594. from numpy import concatenate
  595. x = concatenate((x1, x2[-1::-1]))
  596. y = concatenate((y1, y2[-1::-1]))
  597. wall = Curve(x, y)
  598. wall.set_filled_curves(color='white', pattern='/')
  599. self.shapes = {'wall': wall}
  600. Class Curve
  601. ~~~~~~~~~~~
  602. Class ``Curve`` sits on the coordinates to be drawn, but how is
  603. that done? The constructor just stores the coordinates, while
  604. a method ``draw`` sends the coordinates to the plotting program
  605. to make a graph.
  606. Or more precisely, to avoid a lot of (e.g.) Matplotlib-specific
  607. plotting commands we have created a small layer with a
  608. simple programming interface to plotting programs. This makes it
  609. straightforward to change from Matplotlib to another plotting
  610. program. The programming interface is represented by the ``drawing_tool``
  611. object and has a few functions:
  612. * ``plot_curve`` for sending a curve in terms of :math:`x` and :math:`y` coordinates
  613. to the plotting program,
  614. * ``set_coordinate_system`` for specifying the graphics area,
  615. * ``erase`` for deleting all elements of the graph,
  616. * ``set_grid`` for turning on a grid (convenient while constructing the plot),
  617. * ``set_instruction_file`` for creating a separate file with all
  618. plotting commands (Matplotlib commands in our case),
  619. * a series of ``set_X`` functions where ``X`` is some property like
  620. ``linecolor``, ``linestyle``, ``linewidth``, ``filled_curves``.
  621. This is basically all we need to communicate to a plotting program.
  622. Any class in the ``Shape`` hierarchy inherits ``set_X`` functions for
  623. setting properties of curves. This information is propagated to
  624. all other shape objects that make up the figure. Class
  625. ``Curve`` stores the line properties together with the coordinates
  626. of its curve and propagates this information to the plotting program.
  627. When saying ``vehicle.set_linewidth(10)``, all objects that make
  628. up the ``vehicle`` object will get a ``set_linewidth(10)`` call,
  629. but only the ``Curve`` object at the end of the chain will actually
  630. store the information and send it to the plotting program.
  631. A rough sketch of class ``Curve`` reads
  632. .. code-block:: python
  633. class Curve(Shape):
  634. """General curve as a sequence of (x,y) coordintes."""
  635. def __init__(self, x, y):
  636. self.x = asarray(x, dtype=float)
  637. self.y = asarray(y, dtype=float)
  638. self.linestyle = None
  639. self.linewidth = None
  640. self.linecolor = None
  641. self.fillcolor = None
  642. self.fillpattern = None
  643. self.arrow = None
  644. def draw(self):
  645. drawing_tool.plot_curve(
  646. self.x, self.y,
  647. self.linestyle, self.linewidth, self.linecolor,
  648. self.arrow, self.fillcolor, self.fillpattern)
  649. def set_linewidth(self, width):
  650. self.linewidth = width
  651. det set_linestyle(self, style):
  652. self.linestyle = style
  653. ...
  654. Compound Geometric Objects
  655. ~~~~~~~~~~~~~~~~~~~~~~~~~~
  656. The simple classes ``Line``, ``Arc``, and ``Circle`` could define the geometric
  657. shape through just one ``Curve`` object. More complicated figure elements
  658. are built from instances of various subclasses of ``Shape``. Classes used
  659. for professional drawings soon get quite complex in composition and
  660. have a lot of geometric details, so here we prefer to make a very simple
  661. composition: the already drawn vehicle from
  662. Figure ref:ref:`sketcher:fig:vehicle0`.
  663. That is, instead of composing the drawing in a Python code we make a class
  664. ``Vehicle0`` for doing the same thing, and derive it from ``Shape``.
  665. The ``Shape`` hierarchy is found in the ``pysketcher`` package, so to use these
  666. classes or derive a new one, we need to import ``pysketcher``. The constructor
  667. of class ``Vehicle0`` performs approximately the same statements as
  668. in the example program we developed for making the drawing in
  669. Figure ref:ref:`sketcher:fig:vehicle0`.
  670. .. code-block:: python
  671. class Vehicle0(Shape):
  672. def __init__(self, w_1, R, L, H):
  673. wheel1 = Circle(center=(w_1, R), radius=R)
  674. wheel2 = wheel1.copy()
  675. wheel2.translate((L,0))
  676. under = Rectangle(lower_left_corner=(w_1-2*R, 2*R),
  677. width=2*R + L + 2*R, height=H)
  678. over = Rectangle(lower_left_corner=(w_1, 2*R + H),
  679. width=2.5*R, height=1.25*H)
  680. wheels = Composition(
  681. {'wheel1': wheel1, 'wheel2': wheel2})
  682. body = Composition(
  683. {'under': under, 'over': over})
  684. vehicle = Composition({'wheels': wheels, 'body': body})
  685. xmax = w_1 + 2*L + 3*R
  686. ground = Wall(x=[R, xmax], y=[0, 0], thickness=-0.3*R)
  687. self.shapes = {'vehicle': vehicle, 'ground': ground}
  688. Any subclass of ``Shape`` *must* define the ``shapes`` attribute, otherwise
  689. the inherited ``draw`` method (and a lot of other methods too) will
  690. not work.
  691. The painting of the vehicle could be offered by a method:
  692. .. code-block:: python
  693. def colorful(self):
  694. wheels = self.shapes['vehicle']['wheels']
  695. wheels.set_filled_curves('blue')
  696. wheels.set_linewidth(6)
  697. wheels.set_linecolor('black')
  698. under = self.shapes['vehicle']['body']['under']
  699. under.set_filled_curves('red')
  700. over = self.shapes['vehicle']['body']['over']
  701. over.set_filled_curves(pattern='/')
  702. over.set_linewidth(14)
  703. The usage of the class is simple: after having set up an appropriate
  704. coordinate system a s previously shown, we can do
  705. .. code-block:: python
  706. vehicle = Vehicle0(w_1, R, L, H)
  707. vehicle.draw()
  708. drawing_tool.display()
  709. The color from Figure :ref:`sketcher:fig:vehicle0:v2` is realized by
  710. .. code-block:: python
  711. drawing_tool.erase()
  712. vehicle.colorful()
  713. vehicle.draw()
  714. drawing_tool.display()
  715. A complete code defining and using class ``Vehicle0`` is found in the file
  716. ``vehicle2.py` <http://hplgit.github.com/pysketcher/doc/src/sketcher/src-sketcher/vehicle2.py>`_.
  717. The ``pysketcher`` package contains a wide range of classes for various
  718. geometrical objects, particularly those that are frequently used in
  719. drawings of mechanical systems.
  720. Adding Functionality via Recursion
  721. ==================================
  722. The really powerful feature of our class hierarchy is that we can add
  723. much functionality to the superclass ``Shape`` and to the "bottom" classes
  724. ``Curve``, and all other classes for all types of geometrical shapes
  725. immediately get the new functionality. To explain the idea we first have
  726. to look at the ``draw`` method, which all classes in the ``Shape``
  727. hierarchy must have. The inner workings of the ``draw`` method explain
  728. the secrets of how a series of other useful operations on figures
  729. can be implemented.
  730. Basic Principles of Recursion
  731. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  732. We work with two types of class hierarchies: one Python class hierarchy,
  733. with ``Shape`` as superclass, and one *object hierarchy* of figure elements
  734. in a specific figure. A subclass of ``Shape`` stores its figure in the
  735. ``self.shapes`` dictionary. This dictionary represents the object hierarchy
  736. of figure elements for that class. We want to make one ``draw`` call
  737. for an instance, say our class ``Vehicle0``, and then we want this call
  738. to be propagated to *all* objects that are contained in
  739. ``self.shapes`` and all is nested subdictionaries. How is this done?
  740. The natural starting point is to call ``draw`` for each ``Shape`` object
  741. in the ``self.shapes`` dictionary:
  742. .. code-block:: python
  743. def draw(self):
  744. for shape in self.shapes:
  745. self.shapes[shape].draw()
  746. This general method can be provided by class ``Shape`` and inherited in
  747. subclasses like ``Vehicle0``. Let ``v`` be a ``Vehicle0`` instance.
  748. Seemingly, a call ``v.draw()`` just calls
  749. .. code-block:: python
  750. v.shapes['vehicle'].draw()
  751. v.shapes['ground'].draw()
  752. However, in the former call we call the ``draw`` method of a ``Composition`` object
  753. whose ``self.shapes`` attributed has two elements: ``wheels`` and ``body``.
  754. Since class ``Composition`` inherits the same ``draw`` method, this method will
  755. run through ``self.shapes`` and call ``wheels.draw()`` and ``body.draw()``.
  756. Now, the ``wheels`` object is also a ``Composition`` with the same ``draw``
  757. method, which will run through the ``shapes`` dictionary, now containing
  758. the ``wheel1`` and and ``wheel2`` objects. The ``wheel1`` object is a ``Circle``,
  759. so calling ``wheel1.draw()`` calls the ``draw`` method in class ``Circle``,
  760. but this is the same ``draw`` method as shown above. This method will
  761. therefore traverse the circle's ``shapes`` dictionary, which we have seen
  762. consists of one ``Curve`` element.
  763. The ``Curve`` object holds the coordinates to be plotted so here ``draw``
  764. really needs to do something "physical", namely send the coordinates to
  765. the plotting program. The ``draw`` method is outlined in the short listing
  766. of class ``Curve`` shown previously.
  767. We can go to any of the other shape objects that appear in the figure
  768. hierarchy and follow their ``draw`` calls in the similar way. Every time,
  769. a ``draw`` call will invoke a new ``draw`` call, until we eventually hit
  770. a ``Curve`` object in the "botton" of the figure hierarchy, and then that part
  771. of the figure is really plotted (or more precisely, the coordinates
  772. are sent to a plotting program).
  773. When a method calls itself, such as ``draw`` does, the calls are known as
  774. *recursive* and the programming principle is referred to as
  775. *recursion*. This technique is very often used to traverse hierarchical
  776. structures like the figure structures we work with here. Even though the
  777. hierarchy of objects building up a figure are of different types, they
  778. all inherit the same ``draw`` method and therefore exhibit the same
  779. behavior with respect to drawing. Only the ``Curve`` object has a different
  780. ``draw`` method, which does not lead to more recursion. Without this
  781. different ``draw`` method in class ``Curve``, the repeated ``draw`` calls would
  782. go on forever.
  783. Explaining Recursion
  784. ~~~~~~~~~~~~~~~~~~~~
  785. Understanding recursion is usually a challenge. To get a better idea of
  786. how recursion works, we have equipped class ``Shape`` with a method ``recurse``
  787. which just visits all the objects in the ``shapes`` dictionary and prints
  788. out a message for each object.
  789. This feature allows us to trace the execution and see exactly where
  790. we are in the hierarchy and which objects that are visited.
  791. The ``recurse`` method is very similar to ``draw``:
  792. .. code-block:: python
  793. def recurse(self, name, indent=0):
  794. # print message where we are (name is where we come from)
  795. for shape in self.shapes:
  796. # print message about which object to visit
  797. self.shapes[shape].recurse(indent+2, shape)
  798. The ``indent`` parameter governs how much the message from this
  799. ``recurse`` method is intended. We increase ``indent`` by 2 for every level
  800. in the hierarchy. This makes it easy to see on the printout how far
  801. down in the hierarchy we are.
  802. A typical message written by ``recurse`` when ``name`` is ``body`` and
  803. the ``shapes`` dictionary contains two entries, ``over`` and ``under``,
  804. will be
  805. .. code-block:: python
  806. Composition: body.shapes has entries 'over', 'under'
  807. call body.shapes["over"].recurse("over", 6)
  808. The number of leading blanks on each line corresponds to the value of
  809. ``indent``. The code printing out such messages looks like
  810. .. code-block:: python
  811. def recurse(self, name, indent=0):
  812. space = ' '*indent
  813. print space, '%s: %s.shapes has entries' % \
  814. (self.__class__.__name__, name), \
  815. str(list(self.shapes.keys()))[1:-1]
  816. for shape in self.shapes:
  817. print space,
  818. print 'call %s.shapes["%s"].recurse("%s", %d)' % \
  819. (name, shape, shape, indent+2)
  820. self.shapes[shape].recurse(shape, indent+2)
  821. Let us follow a ``v.recurse('vehicle')`` call in detail, ``v`` being
  822. a ``Vehicle0`` instance. Before looking into the output from ``recurse``,
  823. let us get an overview of the figure hierarchy in the ``v`` object
  824. (as produced by ``print v``)
  825. .. code-block:: python
  826. ground
  827. wall
  828. vehicle
  829. body
  830. over
  831. rectangle
  832. under
  833. rectangle
  834. wheels
  835. wheel1
  836. arc
  837. wheel2
  838. arc
  839. The ``recurse`` method performs the same kind of traversal of the
  840. hierarchy, but writes out and explains a lot more.
  841. The data structure represented by ``v.shapes`` is known as a *tree*.
  842. As in physical trees, there is a *root*, here the ``v.shapes``
  843. dictionary. A graphical illustration of the tree (upside down) is
  844. shown in Figure :ref:`sketcher:fig:Vehicle0:hier2`.
  845. From the root there are one or more branches, here two:
  846. ``ground`` and ``vehicle``. Following the ``vehicle`` branch, it has two new
  847. branches, ``body`` and ``wheels``. Relationships as in family trees
  848. are often used to describe the relations in object trees too: we say
  849. that ``vehicle`` is the parent of ``body`` and that ``body`` is a child of
  850. ``vehicle``. The term *node* is also often used to describe an element
  851. in a tree. A node may have several other nodes as *descendants*.
  852. .. _sketcher:fig:Vehicle0:hier2:
  853. .. figure:: figs-sketcher/Vehicle0_hier2.png
  854. :width: 600
  855. *Hierarchy of figure elements in an instance of class `Vehicle0`*
  856. Recursion is the principal programming technique to traverse tree structures.
  857. Any object in the tree can be viewed as a root of a subtree. For
  858. example, ``wheels`` is the root of a subtree that branches into
  859. ``wheel1`` and ``wheel2``. So when processing an object in the tree,
  860. we imagine we process the root and then recurse into a subtree, but the
  861. first object we recurse into can be viewed as the root of the subtree, so the
  862. processing procedure of the parent object can be repeated.
  863. A recommended next step is to simulate the ``recurse`` method by hand and
  864. carefully check that what happens in the visits to ``recurse`` is
  865. consistent with the output listed below. Although tedious, this is
  866. a major exercise that guaranteed will help to demystify recursion.
  867. Also remember that it requires some efforts to understand recursion.
  868. A part of the printout of ``v.recurse('vehicle')`` looks like
  869. .. code-block:: python
  870. Vehicle0: vehicle.shapes has entries 'ground', 'vehicle'
  871. call vehicle.shapes["ground"].recurse("ground", 2)
  872. Wall: ground.shapes has entries 'wall'
  873. call ground.shapes["wall"].recurse("wall", 4)
  874. reached "bottom" object Curve
  875. call vehicle.shapes["vehicle"].recurse("vehicle", 2)
  876. Composition: vehicle.shapes has entries 'body', 'wheels'
  877. call vehicle.shapes["body"].recurse("body", 4)
  878. Composition: body.shapes has entries 'over', 'under'
  879. call body.shapes["over"].recurse("over", 6)
  880. Rectangle: over.shapes has entries 'rectangle'
  881. call over.shapes["rectangle"].recurse("rectangle", 8)
  882. reached "bottom" object Curve
  883. call body.shapes["under"].recurse("under", 6)
  884. Rectangle: under.shapes has entries 'rectangle'
  885. call under.shapes["rectangle"].recurse("rectangle", 8)
  886. reached "bottom" object Curve
  887. ...
  888. This example should clearly demonstrate the principle that we
  889. can start at any object in the tree and do a recursive set
  890. of calls with that object as root.
  891. .. _sketcher:scaling:
  892. Scaling, Translating, and Rotating a Figure
  893. -------------------------------------------
  894. With recursion, as explained in the previous section, we can within
  895. minutes equip *all* classes in the ``Shape`` hierarchy, both present and
  896. future ones, with the ability to scale the figure, translate it,
  897. or rotate it. This added functionality requires only a few lines
  898. of code.
  899. Scaling
  900. ~~~~~~~
  901. We start with the simplest of the three geometric transformations,
  902. namely scaling.
  903. For a ``Curve`` instance containing a set of :math:`n` coordinates
  904. :math:`(x_i,y_i)` that make up a curve, scaling by
  905. a factor :math:`a` means that we multiply all the :math:`x` and :math:`y` coordinates
  906. by :math:`a`:
  907. .. math::
  908. x_i \leftarrow ax_i,\quad y_i\leftarrow ay_i,
  909. \quad i=0,\ldots,n-1\thinspace .
  910. Here we apply the arrow as an assignment operator.
  911. The corresponding Python implementation in
  912. class ``Curve`` reads
  913. .. code-block:: python
  914. class Curve:
  915. ...
  916. def scale(self, factor):
  917. self.x = factor*self.x
  918. self.y = factor*self.y
  919. Note here that ``self.x`` and ``self.y`` are Numerical Python arrays,
  920. so that multiplication by a scalar number ``factor`` is
  921. a vectorized operation.
  922. An even more efficient implementation is
  923. to make use of in-place multiplication in the arrays, as this saves the creation
  924. of temporary arrays like ``factor*self.x``, which is then assigned to
  925. ``self.x``:
  926. .. code-block:: python
  927. class Curve:
  928. ...
  929. def scale(self, factor):
  930. self.x *= factor
  931. self.y *= factor
  932. In an instance of a subclass of ``Shape``,
  933. the meaning of a method ``scale`` is
  934. to run through all objects in the dictionary ``shapes`` and ask
  935. each object to scale itself. This is the same delegation of actions
  936. to subclass instances as we do in the ``draw`` (or ``recurse``) method. All
  937. all objects, except ``Curve`` instances, can share the same
  938. implementation of the ``scale`` method. Therefore, we place
  939. the ``scale`` method in the superclass ``Shape`` such that all
  940. subclasses inherit the method.
  941. Since ``scale`` and ``draw`` are so similar,
  942. we can easily implement the ``scale`` method in class ``Shape`` by
  943. copying and editing the ``draw`` method:
  944. .. code-block:: python
  945. class Shape:
  946. ...
  947. def scale(self, factor):
  948. for shape in self.shapes:
  949. self.shapes[shape].scale(factor)
  950. This is all we have to do in order to equip all subclasses of
  951. ``Shape`` with scaling functionality!
  952. Any piece of the figure will scale itself, in the same manner
  953. as it can draw itself.
  954. Translation
  955. ~~~~~~~~~~~
  956. A set of coordinates :math:`(x_i, y_i)` can be translated :math:`v_0` units in
  957. the :math:`x` direction and :math:`v_1` units in the :math:`y` direction using the formulas
  958. .. math::
  959. x_i\leftarrow x_i+v_0,\quad y_i\leftarrow y_i+v_1,\quad i=0,\ldots,n-1\thinspace .
  960. The natural specification of the translation is in terms of a
  961. vector :math:`v=(v_0,v_1)`.
  962. The corresponding Python implementation in class ``Curve`` becomes
  963. .. code-block:: python
  964. class Curve:
  965. ...
  966. def translate(self, v):
  967. self.x += v[0]
  968. self.y += v[1]
  969. The translation operation for a shape object is very similar to the
  970. scaling and drawing operations. This means that we can implement a
  971. common method ``translate`` in the superclass ``Shape``. The code
  972. is parallel to the ``scale`` method:
  973. .. code-block:: python
  974. class Shape:
  975. ....
  976. def translate(self, v):
  977. for shape in self.shapes:
  978. self.shapes[shape].translate(v)
  979. Rotation
  980. ~~~~~~~~
  981. Rotating a figure is more complicated than scaling and translating.
  982. A counter clockwise rotation of :math:`\theta` degrees for a set of
  983. coordinates :math:`(x_i,y_i)` is given by
  984. .. math::
  985. \bar x_i &\leftarrow x_i\cos\theta - y_i\sin\theta,\\
  986. \bar y_i &\leftarrow x_i\sin\theta + y_i\cos\theta\thinspace .
  987. This rotation is performed around the origin. If we want the figure
  988. to be rotated with respect to a general point :math:`(x,y)`, we need to
  989. extend the formulas above:
  990. .. math::
  991. \bar x_i &\leftarrow x + (x_i -x)\cos\theta - (y_i -y)\sin\theta,\\
  992. \bar y_i &\leftarrow y + (x_i -x)\sin\theta + (y_i -y)\cos\theta\thinspace .
  993. The Python implementation in class ``Curve``, assuming that :math:`\theta`
  994. is given in degrees and not in radians, becomes
  995. .. code-block:: python
  996. def rotate(self, angle, center):
  997. angle = radians(angle)
  998. x, y = center
  999. c = cos(angle); s = sin(angle)
  1000. xnew = x + (self.x - x)*c - (self.y - y)*s
  1001. ynew = y + (self.x - x)*s + (self.y - y)*c
  1002. self.x = xnew
  1003. self.y = ynew
  1004. The ``rotate`` method in class ``Shape`` is identical to the
  1005. ``draw``, ``scale``, and ``translate`` methods except that we
  1006. recurse into ``self.rotate(angle, center)``.
  1007. We have already seen the ``rotate`` method in action when animating the
  1008. rolling wheel at the end of the section :ref:`sketcher:vehicle1:anim`.