basics.do.txt 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. # #ifdef PRIMER_BOOK
  2. idx{`pysketcher`}
  3. Implementing a drawing program provides a very good example on the
  4. usefulness of object-oriented programming. In the following we shall
  5. develop the simpler parts of a relatively small and compact drawing
  6. program for making sketches of the type shown in Figure
  7. ref{sketcher:fig:inclinedplane}. This is a typical *principal sketch*
  8. of a physics problem, here involving a rolling wheel on an inclined
  9. plane. The sketch
  10. # #else
  11. 2DO:
  12. * two wheels of different radii on inclined plane coupled to
  13. correct solution
  14. * "Pygame backend": "http://inventwithpython.com/chapter17.html"
  15. ======= A First Glimpse of Pysketcher =======
  16. Formulation of physical problems makes heavy use of *principal sketches*
  17. such as the one in Figure ref{sketcher:fig:inclinedplane}.
  18. This particular sketch illustrates the classical mechanics problem
  19. of a rolling wheel on an inclined plane.
  20. The figure
  21. # #endif
  22. is made up many individual elements: a rectangle
  23. filled with a pattern (the inclined plane), a hollow circle with color
  24. (the wheel), arrows with labels (the $N$ and $Mg$ forces, and the $x$
  25. axis), an angle with symbol $\theta$, and a dashed line indicating the
  26. starting location of the wheel.
  27. Drawing software and plotting programs can produce such figures quite
  28. easily in principle, but the amount of details the user needs to
  29. control with the mouse can be substantial. Software more tailored to
  30. producing sketches of this type would work with more convenient
  31. abstractions, such as circle, wall, angle, force arrow, axis, and so
  32. forth. And as soon we start *programming* to construct the figure we
  33. get a range of other powerful tools at disposal. For example, we can
  34. easily translate and rotate parts of the figure and make an animation
  35. that illustrates the physics of the problem.
  36. Programming as a superior alternative to interactive drawing is
  37. the mantra of this section.
  38. FIGURE: [fig-tut/wheel_on_inclined_plane, width=400 frac=0.5] Sketch of a physics problem. label{sketcher:fig:inclinedplane}
  39. # #ifdef PRIMER_BOOK
  40. Classes are very suitable for implementing the various components that
  41. build up a sketch. In particular, we shall demonstrate that as soon as
  42. some classes are established, more are easily added. Enhanced
  43. functionality for all the classes is also easy to implement in common,
  44. generic code that can immediately be shared by all present and future
  45. classes.
  46. The fundamental data structure involved in this case study is a
  47. hierarchical tree, and much of the material on implementation issues
  48. targets how to traverse tree structures with recursive function calls.
  49. This topic is of key relevance in a wide range of other applications
  50. as well.
  51. ===== Using the Object Collection =====
  52. We start by demonstrating a convenient user interface for making
  53. sketches of the type in Figure ref{sketcher:fig:inclinedplane}.
  54. However, it is more appropriate to start with a significantly simpler
  55. example as depicted in Figure ref{sketcher:fig:vehicle0}. This toy
  56. sketch consists of several elements: two circles, two rectangles, and
  57. a ``ground'' element.
  58. # #else
  59. ===== Basic Construction of Sketches =====
  60. Before attacking real-life sketches as in Figure ref{sketcher:fig:inclinedplane}
  61. we focus on the significantly simpler drawing shown
  62. in Figure ref{sketcher:fig:vehicle0}. This toy sketch consists of
  63. several elements: two circles, two rectangles, and a ``ground'' element.
  64. # #endif
  65. FIGURE: [fig-tut/vehicle0_dim, width=600] Sketch of a simple figure. label{sketcher:fig:vehicle0}
  66. === Basic Drawing ===
  67. A typical program creating these five elements is shown next.
  68. After importing the `pysketcher` package, the first task is always to
  69. define a coordinate system:
  70. !bc pycod
  71. from pysketcher import *
  72. drawing_tool.set_coordinate_system(
  73. xmin=0, xmax=10, ymin=-1, ymax=8)
  74. !ec
  75. Instead of working with lengths expressed by specific numbers it is
  76. highly recommended to use variables to parameterize lengths as
  77. this makes it easier to change dimensions later.
  78. Here we introduce some key lengths for the radius of the wheels,
  79. distance between the wheels, etc.:
  80. !bc pycod
  81. R = 1 # radius of wheel
  82. L = 4 # distance between wheels
  83. H = 2 # height of vehicle body
  84. w_1 = 5 # position of front wheel
  85. drawing_tool.set_coordinate_system(xmin=0, xmax=w_1 + 2*L + 3*R,
  86. ymin=-1, ymax=2*R + 3*H)
  87. !ec
  88. With the drawing area in place we can make the first `Circle` object
  89. in an intuitive fashion:
  90. !bc pycod
  91. wheel1 = Circle(center=(w_1, R), radius=R)
  92. !ec
  93. to change dimensions later.
  94. To translate the geometric information about the `wheel1` object to
  95. instructions for the plotting engine (in this case Matplotlib), one calls the
  96. `wheel1.draw()`. To display all drawn objects, one issues
  97. `drawing_tool.display()`. The typical steps are hence:
  98. !bc pycod
  99. wheel1 = Circle(center=(w_1, R), radius=R)
  100. wheel1.draw()
  101. # Define other objects and call their draw() methods
  102. drawing_tool.display()
  103. drawing_tool.savefig('tmp.png') # store picture
  104. !ec
  105. The next wheel can be made by taking a copy of `wheel1` and
  106. translating the object to the right according to a
  107. displacement vector $(L,0)$:
  108. !bc pycod
  109. wheel2 = wheel1.copy()
  110. wheel2.translate((L,0))
  111. !ec
  112. The two rectangles are also made in an intuitive way:
  113. !bc pycod
  114. under = Rectangle(lower_left_corner=(w_1-2*R, 2*R),
  115. width=2*R + L + 2*R, height=H)
  116. over = Rectangle(lower_left_corner=(w_1, 2*R + H),
  117. width=2.5*R, height=1.25*H)
  118. !ec
  119. === Groups of Objects ===
  120. Instead of calling the `draw` method of every object, we can
  121. group objects and call `draw`, or perform other operations, for
  122. the whole group. For example, we may collect the two wheels
  123. in a `wheels` group and the `over` and `under` rectangles
  124. in a `body` group. The whole vehicle is a composition
  125. of its `wheels` and `body` groups. The code goes like
  126. !bc pycod
  127. wheels = Composition({'wheel1': wheel1, 'wheel2': wheel2})
  128. body = Composition({'under': under, 'over': over})
  129. vehicle = Composition({'wheels': wheels, 'body': body})
  130. !ec
  131. The ground is illustrated by an object of type `Wall`,
  132. mostly used to indicate walls in sketches of mechanical systems.
  133. A `Wall` takes the `x` and `y` coordinates of some curve,
  134. and a `thickness` parameter, and creates a thick curve filled
  135. with a simple pattern. In this case the curve is just a flat
  136. line so the construction is made of two points on the
  137. ground line ($(w_1-L,0)$ and $(w_1+3L,0)$):
  138. !bc pycod
  139. ground = Wall(x=[w_1 - L, w_1 + 3*L], y=[0, 0], thickness=-0.3*R)
  140. !ec
  141. The negative thickness makes the pattern-filled rectangle appear below
  142. the defined line, otherwise it appears above.
  143. We may now collect all the objects in a ``top'' object that contains
  144. the whole figure:
  145. !bc pycod
  146. fig = Composition({'vehicle': vehicle, 'ground': ground})
  147. fig.draw() # send all figures to plotting backend
  148. drawing_tool.display()
  149. drawing_tool.savefig('tmp.png')
  150. !ec
  151. The `fig.draw()` call will visit
  152. all subgroups, their subgroups,
  153. and so forth in the hierarchical tree structure of
  154. figure elements,
  155. and call `draw` for every object.
  156. === Changing Line Styles and Colors ===
  157. Controlling the line style, line color, and line width is
  158. fundamental when designing figures. The `pysketcher`
  159. package allows the user to control such properties in
  160. single objects, but also set global properties that are
  161. used if the object has no particular specification of
  162. the properties. Setting the global properties are done like
  163. !bc pycod
  164. drawing_tool.set_linestyle('dashed')
  165. drawing_tool.set_linecolor('black')
  166. drawing_tool.set_linewidth(4)
  167. !ec
  168. At the object level the properties are specified in a similar
  169. way:
  170. !bc pycod
  171. wheels.set_linestyle('solid')
  172. wheels.set_linecolor('red')
  173. !ec
  174. and so on.
  175. Geometric figures can be specified as *filled*, either with a color or with a
  176. special visual pattern:
  177. !bc pycod
  178. # Set filling of all curves
  179. drawing_tool.set_filled_curves(color='blue', pattern='/')
  180. # Turn off filling of all curves
  181. drawing_tool.set_filled_curves(False)
  182. # Fill the wheel with red color
  183. wheel1.set_filled_curves('red')
  184. !ec
  185. # http://packages.python.org/ete2/ for visualizing tree structures!
  186. === The Figure Composition as an Object Hierarchy ===
  187. The composition of objects making up the figure
  188. is hierarchical, similar to a family, where
  189. each object has a parent and a number of children. Do a
  190. `print fig` to display the relations:
  191. !bc dat
  192. ground
  193. wall
  194. vehicle
  195. body
  196. over
  197. rectangle
  198. under
  199. rectangle
  200. wheels
  201. wheel1
  202. arc
  203. wheel2
  204. arc
  205. !ec
  206. The indentation reflects how deep down in the hierarchy (family)
  207. we are.
  208. This output is to be interpreted as follows:
  209. * `fig` contains two objects, `ground` and `vehicle`
  210. * `ground` contains an object `wall`
  211. * `vehicle` contains two objects, `body` and `wheels`
  212. * `body` contains two objects, `over` and `under`
  213. * `wheels` contains two objects, `wheel1` and `wheel2`
  214. In this listing there are also objects not defined by the
  215. programmer: `rectangle` and `arc`. These are of type `Curve`
  216. and automatically generated by the classes `Rectangle` and `Circle`.
  217. More detailed information can be printed by
  218. !bc pycod
  219. print fig.show_hierarchy('std')
  220. !ec
  221. yielding the output
  222. !bc dat
  223. ground (Wall):
  224. wall (Curve): 4 coords fillcolor='white' fillpattern='/'
  225. vehicle (Composition):
  226. body (Composition):
  227. over (Rectangle):
  228. rectangle (Curve): 5 coords
  229. under (Rectangle):
  230. rectangle (Curve): 5 coords
  231. wheels (Composition):
  232. wheel1 (Circle):
  233. arc (Curve): 181 coords
  234. wheel2 (Circle):
  235. arc (Curve): 181 coords
  236. !ec
  237. Here we can see the class type for each figure object, how many
  238. coordinates that are involved in basic figures (`Curve` objects), and
  239. special settings of the basic figure (fillcolor, line types, etc.).
  240. For example, `wheel2` is a `Circle` object consisting of an `arc`,
  241. which is a `Curve` object consisting of 181 coordinates (the
  242. points needed to draw a smooth circle). The `Curve` objects are the
  243. only objects that really holds specific coordinates to be drawn.
  244. The other object types are just compositions used to group
  245. parts of the complete figure.
  246. One can also get a graphical overview of the hierarchy of figure objects
  247. that build up a particular figure `fig`.
  248. Just call `fig.graphviz_dot('fig')` to produce a file `fig.dot` in
  249. the *dot format*. This file contains relations between parent and
  250. child objects in the figure and can be turned into an image,
  251. as in Figure ref{sketcher:fig:vehicle0:hier1}, by
  252. running the `dot` program:
  253. !bc sys
  254. Terminal> dot -Tpng -o fig.png fig.dot
  255. !ec
  256. FIGURE: [fig-tut/vehicle0_hier1, width=500 frac=0.8] Hierarchical relation between figure objects. label{sketcher:fig:vehicle0:hier1}
  257. The call `fig.graphviz_dot('fig', classname=True)` makes a `fig.dot` file
  258. where the class type of each object is also visible, see
  259. Figure ref{sketcher:fig:vehicle0:hier2}. The ability to write out the
  260. object hierarchy or view it graphically can be of great help when
  261. working with complex figures that involve layers of subfigures.
  262. FIGURE: [fig-tut/Vehicle0_hier2, width=500 frac=0.8] Hierarchical relation between figure objects, including their class names. label{sketcher:fig:vehicle0:hier2}
  263. Any of the objects can in the program be reached through their names, e.g.,
  264. !bc pycod
  265. fig['vehicle']
  266. fig['vehicle']['wheels']
  267. fig['vehicle']['wheels']['wheel2']
  268. fig['vehicle']['wheels']['wheel2']['arc']
  269. fig['vehicle']['wheels']['wheel2']['arc'].x # x coords
  270. fig['vehicle']['wheels']['wheel2']['arc'].y # y coords
  271. fig['vehicle']['wheels']['wheel2']['arc'].linestyle
  272. fig['vehicle']['wheels']['wheel2']['arc'].linetype
  273. !ec
  274. Grabbing a part of the figure this way is handy for
  275. changing properties of that part, for example, colors, line styles
  276. (see Figure ref{sketcher:fig:vehicle0:v2}):
  277. !bc pycod
  278. fig['vehicle']['wheels'].set_filled_curves('blue')
  279. fig['vehicle']['wheels'].set_linewidth(6)
  280. fig['vehicle']['wheels'].set_linecolor('black')
  281. fig['vehicle']['body']['under'].set_filled_curves('red')
  282. fig['vehicle']['body']['over'].set_filled_curves(pattern='/')
  283. fig['vehicle']['body']['over'].set_linewidth(14)
  284. fig['vehicle']['body']['over']['rectangle'].linewidth = 4
  285. !ec
  286. The last line accesses the `Curve` object directly, while the line above,
  287. accesses the `Rectangle` object, which will then set the linewidth of
  288. its `Curve` object, and other objects if it had any.
  289. The result of the actions above is shown in Figure ref{sketcher:fig:vehicle0:v2}.
  290. FIGURE: [fig-tut/vehicle0, width=700] Left: Basic line-based drawing. Right: Thicker lines and filled parts. label{sketcher:fig:vehicle0:v2}
  291. We can also change position of parts of the figure and thereby make
  292. animations, as shown next.
  293. === Animation: Translating the Vehicle ===
  294. Can we make our little vehicle roll? A first attempt will be to
  295. fake rolling by just displacing all parts of the vehicle.
  296. The relevant parts constitute the `fig['vehicle']` object.
  297. This part of the figure can be translated, rotated, and scaled.
  298. A translation along the ground means a translation in $x$ direction,
  299. say a length $L$ to the right:
  300. !bc pycod
  301. fig['vehicle'].translate((L,0))
  302. !ec
  303. You need to erase, draw, and display to see the movement:
  304. !bc pycod
  305. drawing_tool.erase()
  306. fig.draw()
  307. drawing_tool.display()
  308. !ec
  309. Without erasing, the old drawing of the vehicle will remain in
  310. the figure so you get two vehicles. Without `fig.draw()` the
  311. new coordinates of the vehicle will not be communicated to
  312. the drawing tool, and without calling display the updated
  313. drawing will not be visible.
  314. A figure that moves in time is conveniently realized by the
  315. function `animate`:
  316. !bc pycod
  317. animate(fig, tp, action)
  318. !ec
  319. Here, `fig` is the entire figure, `tp` is an array of
  320. time points, and `action` is a user-specified function that changes
  321. `fig` at a specific time point. Typically, `action` will move
  322. parts of `fig`.
  323. In the present case we can define the movement through a velocity
  324. function `v(t)` and displace the figure `v(t)*dt` for small time
  325. intervals `dt`. A possible velocity function is
  326. !bc pycod
  327. def v(t):
  328. return -8*R*t*(1 - t/(2*R))
  329. !ec
  330. Our action function for horizontal displacements `v(t)*dt` becomes
  331. !bc pycod
  332. def move(t, fig):
  333. x_displacement = dt*v(t)
  334. fig['vehicle'].translate((x_displacement, 0))
  335. !ec
  336. Since our velocity is negative for $t\in [0,2R]$ the displacement is
  337. to the left.
  338. The `animate` function will for each time point `t` in `tp` erase
  339. the drawing, call `action(t, fig)`, and show the new figure by
  340. `fig.draw()` and `drawing_tool.display()`.
  341. Here we choose a resolution of the animation corresponding to
  342. 25 time points in the time interval $[0,2R]$:
  343. !bc pycod
  344. import numpy
  345. tp = numpy.linspace(0, 2*R, 25)
  346. dt = tp[1] - tp[0] # time step
  347. animate(fig, tp, move, pause_per_frame=0.2)
  348. !ec
  349. The `pause_per_frame` adds a pause, here 0.2 seconds, between
  350. each frame in the animation.
  351. We can also ask `animate` to store each frame in a file:
  352. !bc pycod
  353. files = animate(fig, tp, move_vehicle, moviefiles=True,
  354. pause_per_frame=0.2)
  355. !ec
  356. The `files` variable, here `'tmp_frame_%04d.png'`,
  357. is the printf-specification used to generate the individual
  358. plot files. We can use this specification to make a video
  359. file via `ffmpeg` (or `avconv` on Debian-based Linux systems such
  360. as Ubuntu). Videos in the Flash and WebM formats can be created
  361. by
  362. !bc sys
  363. Terminal> ffmpeg -r 12 -i tmp_frame_%04d.png -vcodec flv mov.flv
  364. Terminal> ffmpeg -r 12 -i tmp_frame_%04d.png -vcodec libvpx mov.webm
  365. !ec
  366. An animated GIF movie can also be made using the `convert` program
  367. from the ImageMagick software suite:
  368. !bc sys
  369. Terminal> convert -delay 20 tmp_frame*.png mov.gif
  370. Terminal> animate mov.gif # play movie
  371. !ec
  372. The delay between frames, in units of 1/100 s,
  373. governs the speed of the movie.
  374. To play the animated GIF file in a web page, simply insert
  375. `<img src="mov.gif">` in the HTML code.
  376. The individual PNG frames can be directly played in a web
  377. browser by running
  378. !bc sys
  379. Terminal> scitools movie output_file=mov.html fps=5 tmp_frame*
  380. !ec
  381. or calling
  382. !bc pycod
  383. from scitools.std import movie
  384. movie(files, encoder='html', output_file='mov.html')
  385. !ec
  386. in Python. Load the resulting file `mov.html` into a web browser
  387. to play the movie.
  388. Try to run "`vehicle0.py`": "${src_path_pysketcher}/vehicle0.py" and
  389. then load `mov.html` into a browser, or play one of the `mov.*`
  390. video files. Alternatively, you can view a ready-made "movie":
  391. "${src_path_tut}/mov-tut/vehicle0.html".
  392. === Animation: Rolling the Wheels ===
  393. label{sketcher:vehicle1:anim}
  394. It is time to show rolling wheels. To this end, we add spokes to the
  395. wheels, formed by two crossing lines, see Figure ref{sketcher:fig:vehicle1}.
  396. The construction of the wheels will now involve a circle and two lines:
  397. !bc pycod
  398. wheel1 = Composition({
  399. 'wheel': Circle(center=(w_1, R), radius=R),
  400. 'cross': Composition({'cross1': Line((w_1,0), (w_1,2*R)),
  401. 'cross2': Line((w_1-R,R), (w_1+R,R))})})
  402. wheel2 = wheel1.copy()
  403. wheel2.translate((L,0))
  404. !ec
  405. Observe that `wheel1.copy()` copies all the objects that make
  406. up the first wheel, and `wheel2.translate` translates all
  407. the copied objects.
  408. FIGURE: [fig-tut/vehicle1, width=400 frac=0.8] Wheels with spokes to illustrate rolling. label{sketcher:fig:vehicle1}
  409. The `move` function now needs to displace all the objects in the
  410. entire vehicle and also rotate the `cross1` and `cross2`
  411. objects in both wheels.
  412. The rotation angle follows from the fact that the arc length
  413. of a rolling wheel equals the displacement of the center of
  414. the wheel, leading to a rotation angle
  415. !bc pycod
  416. angle = - x_displacement/R
  417. !ec
  418. With `w_1` tracking the $x$ coordinate of the center
  419. of the front wheel, we can rotate that wheel by
  420. !bc pycod
  421. w1 = fig['vehicle']['wheels']['wheel1']
  422. from math import degrees
  423. w1.rotate(degrees(angle), center=(w_1, R))
  424. !ec
  425. The `rotate` function takes two parameters: the rotation angle
  426. (in degrees) and the center point of the rotation, which is the
  427. center of the wheel in this case. The other wheel is rotated by
  428. !bc pycod
  429. w2 = fig['vehicle']['wheels']['wheel2']
  430. w2.rotate(degrees(angle), center=(w_1 + L, R))
  431. !ec
  432. That is, the angle is the same, but the rotation point is different.
  433. The update of the center point is done by `w_1 += x_displacement`.
  434. The complete `move` function with translation of the entire
  435. vehicle and rotation of the wheels then becomes
  436. !bc pycod
  437. w_1 = w_1 + L # start position
  438. def move(t, fig):
  439. x_displacement = dt*v(t)
  440. fig['vehicle'].translate((x_displacement, 0))
  441. # Rotate wheels
  442. global w_1
  443. w_1 += x_displacement
  444. # R*angle = -x_displacement
  445. angle = - x_displacement/R
  446. w1 = fig['vehicle']['wheels']['wheel1']
  447. w1.rotate(degrees(angle), center=(w_1, R))
  448. w2 = fig['vehicle']['wheels']['wheel2']
  449. w2.rotate(degrees(angle), center=(w_1 + L, R))
  450. !ec
  451. The complete example is found in the file
  452. "`vehicle1.py`": "${src_path_pysketcher}/vehicle1.py". You may run this file or watch a "ready-made movie": "${src_path_tut}/mov-tut/vehicle1.html".
  453. The advantages with making figures this way, through programming
  454. rather than using interactive drawing programs, are numerous. For
  455. example, the objects are parameterized by variables so that various
  456. dimensions can easily be changed. Subparts of the figure, possible
  457. involving a lot of figure objects, can change color, linetype, filling
  458. or other properties through a *single* function call. Subparts of the
  459. figure can be rotated, translated, or scaled. Subparts of the figure
  460. can also be copied and moved to other parts of the drawing
  461. area. However, the single most important feature is probably the
  462. ability to make animations governed by mathematical formulas or data
  463. coming from physics simulations of the problem, as shown in the example above.