main_sketcher.txt 86 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441
  1. .. Automatically generated Sphinx-extended reStructuredText file from DocOnce source
  2. (https://github.com/hplgit/doconce/)
  3. .. Sphinx can only have title with less than 63 chars...
  4. .. Document title:
  5. Pysketcher: Create Principal Sketches of Physics Problems
  6. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  7. :Authors: Hans Petter Langtangen
  8. :Date: Jan 22, 2016
  9. .. The below box could be typeset as .. admonition: Attention
  10. but we have decided not to do so since the admon needs a title
  11. (the box formatting is therefore just ignored)
  12. This document is derived from Chapter 9 in the book
  13. `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,
  14. 4th edition, Springer, 2014.
  15. *Abstract.* Pysketcher is a Python package which allows principal sketches of
  16. physics and mechanics problems to be realized through short programs
  17. instead of interactive (and potentially tedious and inaccurate)
  18. drawing. Elements of the sketch, such as lines, circles, angles,
  19. forces, coordinate systems, etc., are realized as objects and
  20. collected in hierarchical structures. Parts of the hierarchical
  21. structures can easily change line styles and colors, or be copied,
  22. scaled, translated, and rotated. These features make it
  23. straightforward to move parts of the sketch to create animation,
  24. usually in accordance with the physics of the underlying problem.
  25. Exact dimensioning of the elements in the sketch is trivial to obtain
  26. since distances are specified in computer code.
  27. Pysketcher is easy to learn from a number of examples. Beyond
  28. essential Python programming and a knowledge about mechanics problems,
  29. no further background is required.
  30. .. Task (can be questions): make sketches of physical problems, see fig
  31. .. through user-friendly composition of basic shapes
  32. .. Desired knowledge: plotting curves, basic OO (ch. X.Y, ...)
  33. .. Required knowledge?
  34. .. Learning Goals: these targets the inner workings of pysketcher,
  35. .. which is just a part of this document...
  36. .. !split
  37. A first glimpse of Pysketcher
  38. =============================
  39. Formulation of physical problems makes heavy use of *principal sketches*
  40. such as the one in Figure :ref:`sketcher:fig:inclinedplane`.
  41. This particular sketch illustrates the classical mechanics problem
  42. of a rolling wheel on an inclined plane.
  43. The figure
  44. is made up many individual elements: a rectangle
  45. filled with a pattern (the inclined plane), a hollow circle with color
  46. (the wheel), arrows with labels (the :math:`N` and :math:`Mg` forces, and the :math:`x`
  47. axis), an angle with symbol :math:`\theta`, and a dashed line indicating the
  48. starting location of the wheel.
  49. Drawing software and plotting programs can produce such figures quite
  50. easily in principle, but the amount of details the user needs to
  51. control with the mouse can be substantial. Software more tailored to
  52. producing sketches of this type would work with more convenient
  53. abstractions, such as circle, wall, angle, force arrow, axis, and so
  54. forth. And as soon we start *programming* to construct the figure we
  55. get a range of other powerful tools at disposal. For example, we can
  56. easily translate and rotate parts of the figure and make an animation
  57. that illustrates the physics of the problem.
  58. Programming as a superior alternative to interactive drawing is
  59. the mantra of this section.
  60. .. _sketcher:fig:inclinedplane:
  61. .. figure:: wheel_on_inclined_plane.png
  62. :width: 400
  63. *Sketch of a physics problem*
  64. Basic construction of sketches
  65. ------------------------------
  66. Before attacking real-life sketches as in Figure :ref:`sketcher:fig:inclinedplane`
  67. we focus on the significantly simpler drawing shown
  68. in Figure :ref:`sketcher:fig:vehicle0`. This toy sketch consists of
  69. several elements: two circles, two rectangles, and a "ground" element.
  70. .. _sketcher:fig:vehicle0:
  71. .. figure:: vehicle0_dim.png
  72. :width: 600
  73. *Sketch of a simple figure*
  74. When the sketch is defined in terms of computer code, it is natural to
  75. parameterize geometric features, such as the radius of the wheel (:math:`R`),
  76. the center point of the left wheel (:math:`w_1`), as well as the height (:math:`H`) and
  77. length (:math:`L`) of the main part. The simple vehicle in
  78. Figure :ref:`sketcher:fig:vehicle0` is quickly drawn in almost any interactive
  79. tool. However, if we want to change the radius of the wheels, you need a
  80. sophisticated drawing tool to avoid redrawing the whole figure, while
  81. in computer code this is a matter of changing the :math:`R` parameter and
  82. rerunning the program.
  83. For example, Figure :ref:`sketcher:fig:vehicle0b` shows
  84. a variation of the drawing in
  85. Figure :ref:`sketcher:fig:vehicle0` obtained by just setting
  86. :math:`R=0.5`, :math:`L=5`, :math:`H=2`, and :math:`R=2`. Being able
  87. to quickly change geometric sizes is key to many problem settings in
  88. physics and engineering, but then a program must define the geometry.
  89. .. _sketcher:fig:vehicle0b:
  90. .. figure:: vehicle_v2.png
  91. :width: 500
  92. *Redrawing a figure with other geometric parameters*
  93. Basic drawing
  94. ~~~~~~~~~~~~~
  95. A typical program creating these five elements is shown next.
  96. After importing the ``pysketcher`` package, the first task is always to
  97. define a coordinate system:
  98. .. code-block:: python
  99. from pysketcher import *
  100. drawing_tool.set_coordinate_system(
  101. xmin=0, xmax=10, ymin=-1, ymax=8)
  102. Instead of working with lengths expressed by specific numbers it is
  103. highly recommended to use variables to parameterize lengths as
  104. this makes it easier to change dimensions later.
  105. Here we introduce some key lengths for the radius of the wheels,
  106. distance between the wheels, etc.:
  107. .. code-block:: python
  108. R = 1 # radius of wheel
  109. L = 4 # distance between wheels
  110. H = 2 # height of vehicle body
  111. w_1 = 5 # position of front wheel
  112. drawing_tool.set_coordinate_system(xmin=0, xmax=w_1 + 2*L + 3*R,
  113. ymin=-1, ymax=2*R + 3*H)
  114. With the drawing area in place we can make the first ``Circle`` object
  115. in an intuitive fashion:
  116. .. code-block:: python
  117. wheel1 = Circle(center=(w_1, R), radius=R)
  118. to change dimensions later.
  119. To translate the geometric information about the ``wheel1`` object to
  120. instructions for the plotting engine (in this case Matplotlib), one calls the
  121. ``wheel1.draw()``. To display all drawn objects, one issues
  122. ``drawing_tool.display()``. The typical steps are hence:
  123. .. code-block:: python
  124. wheel1 = Circle(center=(w_1, R), radius=R)
  125. wheel1.draw()
  126. # Define other objects and call their draw() methods
  127. drawing_tool.display()
  128. drawing_tool.savefig('tmp.png') # store picture
  129. The next wheel can be made by taking a copy of ``wheel1`` and
  130. translating the object to the right according to a
  131. displacement vector :math:`(L,0)`:
  132. .. code-block:: python
  133. wheel2 = wheel1.copy()
  134. wheel2.translate((L,0))
  135. The two rectangles are also made in an intuitive way:
  136. .. code-block:: python
  137. under = Rectangle(lower_left_corner=(w_1-2*R, 2*R),
  138. width=2*R + L + 2*R, height=H)
  139. over = Rectangle(lower_left_corner=(w_1, 2*R + H),
  140. width=2.5*R, height=1.25*H)
  141. Groups of objects
  142. ~~~~~~~~~~~~~~~~~
  143. Instead of calling the ``draw`` method of every object, we can
  144. group objects and call ``draw``, or perform other operations, for
  145. the whole group. For example, we may collect the two wheels
  146. in a ``wheels`` group and the ``over`` and ``under`` rectangles
  147. in a ``body`` group. The whole vehicle is a composition
  148. of its ``wheels`` and ``body`` groups. The code goes like
  149. .. code-block:: python
  150. wheels = Composition({'wheel1': wheel1, 'wheel2': wheel2})
  151. body = Composition({'under': under, 'over': over})
  152. vehicle = Composition({'wheels': wheels, 'body': body})
  153. The ground is illustrated by an object of type ``Wall``,
  154. mostly used to indicate walls in sketches of mechanical systems.
  155. A ``Wall`` takes the ``x`` and ``y`` coordinates of some curve,
  156. and a ``thickness`` parameter, and creates a thick curve filled
  157. with a simple pattern. In this case the curve is just a flat
  158. line so the construction is made of two points on the
  159. ground line (:math:`(w_1-L,0)` and :math:`(w_1+3L,0)`):
  160. .. code-block:: python
  161. ground = Wall(x=[w_1 - L, w_1 + 3*L], y=[0, 0], thickness=-0.3*R)
  162. The negative thickness makes the pattern-filled rectangle appear below
  163. the defined line, otherwise it appears above.
  164. We may now collect all the objects in a "top" object that contains
  165. the whole figure:
  166. .. code-block:: python
  167. fig = Composition({'vehicle': vehicle, 'ground': ground})
  168. fig.draw() # send all figures to plotting backend
  169. drawing_tool.display()
  170. drawing_tool.savefig('tmp.png')
  171. The ``fig.draw()`` call will visit
  172. all subgroups, their subgroups,
  173. and so forth in the hierarchical tree structure of
  174. figure elements,
  175. and call ``draw`` for every object.
  176. Changing line styles and colors
  177. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  178. Controlling the line style, line color, and line width is
  179. fundamental when designing figures. The ``pysketcher``
  180. package allows the user to control such properties in
  181. single objects, but also set global properties that are
  182. used if the object has no particular specification of
  183. the properties. Setting the global properties are done like
  184. .. code-block:: python
  185. drawing_tool.set_linestyle('dashed')
  186. drawing_tool.set_linecolor('black')
  187. drawing_tool.set_linewidth(4)
  188. At the object level the properties are specified in a similar
  189. way:
  190. .. code-block:: python
  191. wheels.set_linestyle('solid')
  192. wheels.set_linecolor('red')
  193. and so on.
  194. Geometric figures can be specified as *filled*, either with a color or with a
  195. special visual pattern:
  196. .. code-block:: python
  197. # Set filling of all curves
  198. drawing_tool.set_filled_curves(color='blue', pattern='/')
  199. # Turn off filling of all curves
  200. drawing_tool.set_filled_curves(False)
  201. # Fill the wheel with red color
  202. wheel1.set_filled_curves('red')
  203. .. `<http://packages.python.org/ete2/>`_ for visualizing tree structures!
  204. The figure composition as an object hierarchy
  205. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  206. The composition of objects making up the figure
  207. is hierarchical, similar to a family, where
  208. each object has a parent and a number of children. Do a
  209. ``print fig`` to display the relations:
  210. .. code-block:: text
  211. ground
  212. wall
  213. vehicle
  214. body
  215. over
  216. rectangle
  217. under
  218. rectangle
  219. wheels
  220. wheel1
  221. arc
  222. wheel2
  223. arc
  224. The indentation reflects how deep down in the hierarchy (family)
  225. we are.
  226. This output is to be interpreted as follows:
  227. * ``fig`` contains two objects, ``ground`` and ``vehicle``
  228. * ``ground`` contains an object ``wall``
  229. * ``vehicle`` contains two objects, ``body`` and ``wheels``
  230. * ``body`` contains two objects, ``over`` and ``under``
  231. * ``wheels`` contains two objects, ``wheel1`` and ``wheel2``
  232. In this listing there are also objects not defined by the
  233. programmer: ``rectangle`` and ``arc``. These are of type ``Curve``
  234. and automatically generated by the classes ``Rectangle`` and ``Circle``.
  235. More detailed information can be printed by
  236. .. code-block:: python
  237. print fig.show_hierarchy('std')
  238. yielding the output
  239. .. code-block:: text
  240. ground (Wall):
  241. wall (Curve): 4 coords fillcolor='white' fillpattern='/'
  242. vehicle (Composition):
  243. body (Composition):
  244. over (Rectangle):
  245. rectangle (Curve): 5 coords
  246. under (Rectangle):
  247. rectangle (Curve): 5 coords
  248. wheels (Composition):
  249. wheel1 (Circle):
  250. arc (Curve): 181 coords
  251. wheel2 (Circle):
  252. arc (Curve): 181 coords
  253. Here we can see the class type for each figure object, how many
  254. coordinates that are involved in basic figures (``Curve`` objects), and
  255. special settings of the basic figure (fillcolor, line types, etc.).
  256. For example, ``wheel2`` is a ``Circle`` object consisting of an ``arc``,
  257. which is a ``Curve`` object consisting of 181 coordinates (the
  258. points needed to draw a smooth circle). The ``Curve`` objects are the
  259. only objects that really holds specific coordinates to be drawn.
  260. The other object types are just compositions used to group
  261. parts of the complete figure.
  262. One can also get a graphical overview of the hierarchy of figure objects
  263. that build up a particular figure ``fig``.
  264. Just call ``fig.graphviz_dot('fig')`` to produce a file ``fig.dot`` in
  265. the *dot format*. This file contains relations between parent and
  266. child objects in the figure and can be turned into an image,
  267. as in Figure :ref:`sketcher:fig:vehicle0:hier1`, by
  268. running the ``dot`` program:
  269. .. code-block:: text
  270. Terminal> dot -Tpng -o fig.png fig.dot
  271. .. _sketcher:fig:vehicle0:hier1:
  272. .. figure:: vehicle0_hier1.png
  273. :width: 500
  274. *Hierarchical relation between figure objects*
  275. The call ``fig.graphviz_dot('fig', classname=True)`` makes a ``fig.dot`` file
  276. where the class type of each object is also visible, see
  277. Figure :ref:`sketcher:fig:vehicle0:hier2`. The ability to write out the
  278. object hierarchy or view it graphically can be of great help when
  279. working with complex figures that involve layers of subfigures.
  280. .. _sketcher:fig:vehicle0:hier2:
  281. .. figure:: Vehicle0_hier2.png
  282. :width: 500
  283. *Hierarchical relation between figure objects, including their class names*
  284. Any of the objects can in the program be reached through their names, e.g.,
  285. .. code-block:: python
  286. fig['vehicle']
  287. fig['vehicle']['wheels']
  288. fig['vehicle']['wheels']['wheel2']
  289. fig['vehicle']['wheels']['wheel2']['arc']
  290. fig['vehicle']['wheels']['wheel2']['arc'].x # x coords
  291. fig['vehicle']['wheels']['wheel2']['arc'].y # y coords
  292. fig['vehicle']['wheels']['wheel2']['arc'].linestyle
  293. fig['vehicle']['wheels']['wheel2']['arc'].linetype
  294. Grabbing a part of the figure this way is handy for
  295. changing properties of that part, for example, colors, line styles
  296. (see Figure :ref:`sketcher:fig:vehicle0:v2`):
  297. .. code-block:: python
  298. fig['vehicle']['wheels'].set_filled_curves('blue')
  299. fig['vehicle']['wheels'].set_linewidth(6)
  300. fig['vehicle']['wheels'].set_linecolor('black')
  301. fig['vehicle']['body']['under'].set_filled_curves('red')
  302. fig['vehicle']['body']['over'].set_filled_curves(pattern='/')
  303. fig['vehicle']['body']['over'].set_linewidth(14)
  304. fig['vehicle']['body']['over']['rectangle'].linewidth = 4
  305. The last line accesses the ``Curve`` object directly, while the line above,
  306. accesses the ``Rectangle`` object, which will then set the linewidth of
  307. its ``Curve`` object, and other objects if it had any.
  308. The result of the actions above is shown in Figure :ref:`sketcher:fig:vehicle0:v2`.
  309. .. _sketcher:fig:vehicle0:v2:
  310. .. figure:: vehicle0.png
  311. :width: 700
  312. *Left: Basic line-based drawing. Right: Thicker lines and filled parts*
  313. We can also change position of parts of the figure and thereby make
  314. animations, as shown next.
  315. Animation: translating the vehicle
  316. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  317. Can we make our little vehicle roll? A first attempt will be to
  318. fake rolling by just displacing all parts of the vehicle.
  319. The relevant parts constitute the ``fig['vehicle']`` object.
  320. This part of the figure can be translated, rotated, and scaled.
  321. A translation along the ground means a translation in :math:`x` direction,
  322. say a length :math:`L` to the right:
  323. .. code-block:: python
  324. fig['vehicle'].translate((L,0))
  325. You need to erase, draw, and display to see the movement:
  326. .. code-block:: python
  327. drawing_tool.erase()
  328. fig.draw()
  329. drawing_tool.display()
  330. Without erasing, the old drawing of the vehicle will remain in
  331. the figure so you get two vehicles. Without ``fig.draw()`` the
  332. new coordinates of the vehicle will not be communicated to
  333. the drawing tool, and without calling display the updated
  334. drawing will not be visible.
  335. A figure that moves in time is conveniently realized by the
  336. function ``animate``:
  337. .. code-block:: python
  338. animate(fig, tp, action)
  339. Here, ``fig`` is the entire figure, ``tp`` is an array of
  340. time points, and ``action`` is a user-specified function that changes
  341. ``fig`` at a specific time point. Typically, ``action`` will move
  342. parts of ``fig``.
  343. In the present case we can define the movement through a velocity
  344. function ``v(t)`` and displace the figure ``v(t)*dt`` for small time
  345. intervals ``dt``. A possible velocity function is
  346. .. code-block:: python
  347. def v(t):
  348. return -8*R*t*(1 - t/(2*R))
  349. Our action function for horizontal displacements ``v(t)*dt`` becomes
  350. .. code-block:: python
  351. def move(t, fig):
  352. x_displacement = dt*v(t)
  353. fig['vehicle'].translate((x_displacement, 0))
  354. Since our velocity is negative for :math:`t\in [0,2R]` the displacement is
  355. to the left.
  356. The ``animate`` function will for each time point ``t`` in ``tp`` erase
  357. the drawing, call ``action(t, fig)``, and show the new figure by
  358. ``fig.draw()`` and ``drawing_tool.display()``.
  359. Here we choose a resolution of the animation corresponding to
  360. 25 time points in the time interval :math:`[0,2R]`:
  361. .. code-block:: python
  362. import numpy
  363. tp = numpy.linspace(0, 2*R, 25)
  364. dt = tp[1] - tp[0] # time step
  365. animate(fig, tp, move, pause_per_frame=0.2)
  366. The ``pause_per_frame`` adds a pause, here 0.2 seconds, between
  367. each frame in the animation.
  368. We can also ask ``animate`` to store each frame in a file:
  369. .. code-block:: python
  370. files = animate(fig, tp, move_vehicle, moviefiles=True,
  371. pause_per_frame=0.2)
  372. The ``files`` variable, here ``'tmp_frame_%04d.png'``,
  373. is the printf-specification used to generate the individual
  374. plot files. We can use this specification to make a video
  375. file via ``ffmpeg`` (or ``avconv`` on Debian-based Linux systems such
  376. as Ubuntu). Videos in the Flash and WebM formats can be created
  377. by
  378. .. code-block:: text
  379. Terminal> ffmpeg -r 12 -i tmp_frame_%04d.png -vcodec flv mov.flv
  380. Terminal> ffmpeg -r 12 -i tmp_frame_%04d.png -vcodec libvpx mov.webm
  381. An animated GIF movie can also be made using the ``convert`` program
  382. from the ImageMagick software suite:
  383. .. code-block:: text
  384. Terminal> convert -delay 20 tmp_frame*.png mov.gif
  385. Terminal> animate mov.gif # play movie
  386. The delay between frames, in units of 1/100 s,
  387. governs the speed of the movie.
  388. To play the animated GIF file in a web page, simply insert
  389. ``<img src="mov.gif">`` in the HTML code.
  390. The individual PNG frames can be directly played in a web
  391. browser by running
  392. .. code-block:: text
  393. Terminal> scitools movie output_file=mov.html fps=5 tmp_frame*
  394. or calling
  395. .. code-block:: python
  396. from scitools.std import movie
  397. movie(files, encoder='html', output_file='mov.html')
  398. in Python. Load the resulting file ``mov.html`` into a web browser
  399. to play the movie.
  400. Try to run `vehicle0.py <http://tinyurl.com/ot733jn/vehicle0.py>`__ and
  401. then load ``mov.html`` into a browser, or play one of the ``mov.*``
  402. video files. Alternatively, you can view a ready-made `movie <http://tinyurl.com/oou9lp7/mov-tut/vehicle0.html>`__.
  403. .. _sketcher:vehicle1:anim:
  404. Animation: rolling the wheels
  405. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  406. It is time to show rolling wheels. To this end, we add spokes to the
  407. wheels, formed by two crossing lines, see Figure :ref:`sketcher:fig:vehicle1`.
  408. The construction of the wheels will now involve a circle and two lines:
  409. .. code-block:: python
  410. wheel1 = Composition({
  411. 'wheel': Circle(center=(w_1, R), radius=R),
  412. 'cross': Composition({'cross1': Line((w_1,0), (w_1,2*R)),
  413. 'cross2': Line((w_1-R,R), (w_1+R,R))})})
  414. wheel2 = wheel1.copy()
  415. wheel2.translate((L,0))
  416. Observe that ``wheel1.copy()`` copies all the objects that make
  417. up the first wheel, and ``wheel2.translate`` translates all
  418. the copied objects.
  419. .. _sketcher:fig:vehicle1:
  420. .. figure:: vehicle1.png
  421. :width: 400
  422. *Wheels with spokes to illustrate rolling*
  423. The ``move`` function now needs to displace all the objects in the
  424. entire vehicle and also rotate the ``cross1`` and ``cross2``
  425. objects in both wheels.
  426. The rotation angle follows from the fact that the arc length
  427. of a rolling wheel equals the displacement of the center of
  428. the wheel, leading to a rotation angle
  429. .. code-block:: python
  430. angle = - x_displacement/R
  431. With ``w_1`` tracking the :math:`x` coordinate of the center
  432. of the front wheel, we can rotate that wheel by
  433. .. code-block:: python
  434. w1 = fig['vehicle']['wheels']['wheel1']
  435. from math import degrees
  436. w1.rotate(degrees(angle), center=(w_1, R))
  437. The ``rotate`` function takes two parameters: the rotation angle
  438. (in degrees) and the center point of the rotation, which is the
  439. center of the wheel in this case. The other wheel is rotated by
  440. .. code-block:: python
  441. w2 = fig['vehicle']['wheels']['wheel2']
  442. w2.rotate(degrees(angle), center=(w_1 + L, R))
  443. That is, the angle is the same, but the rotation point is different.
  444. The update of the center point is done by ``w_1 += x_displacement``.
  445. The complete ``move`` function with translation of the entire
  446. vehicle and rotation of the wheels then becomes
  447. .. code-block:: python
  448. w_1 = w_1 + L # start position
  449. def move(t, fig):
  450. x_displacement = dt*v(t)
  451. fig['vehicle'].translate((x_displacement, 0))
  452. # Rotate wheels
  453. global w_1
  454. w_1 += x_displacement
  455. # R*angle = -x_displacement
  456. angle = - x_displacement/R
  457. w1 = fig['vehicle']['wheels']['wheel1']
  458. w1.rotate(degrees(angle), center=(w_1, R))
  459. w2 = fig['vehicle']['wheels']['wheel2']
  460. w2.rotate(degrees(angle), center=(w_1 + L, R))
  461. The complete example is found in the file
  462. `vehicle1.py <http://tinyurl.com/ot733jn/vehicle1.py>`__. You may run this file or watch a `ready-made movie <http://tinyurl.com/oou9lp7/mov-tut/vehicle1.html>`__.
  463. The advantages with making figures this way, through programming
  464. rather than using interactive drawing programs, are numerous. For
  465. example, the objects are parameterized by variables so that various
  466. dimensions can easily be changed. Subparts of the figure, possible
  467. involving a lot of figure objects, can change color, linetype, filling
  468. or other properties through a *single* function call. Subparts of the
  469. figure can be rotated, translated, or scaled. Subparts of the figure
  470. can also be copied and moved to other parts of the drawing
  471. area. However, the single most important feature is probably the
  472. ability to make animations governed by mathematical formulas or data
  473. coming from physics simulations of the problem, as shown in the example above.
  474. .. !split
  475. .. _sketcher:ex:pendulum:
  476. A simple pendulum
  477. =================
  478. .. _sketcher:ex:pendulum:basic:
  479. The basic physics sketch
  480. ------------------------
  481. We now want to make a sketch of simple pendulum from physics, as shown
  482. in Figure :ref:`sketcher:ex:pendulum:fig1`. A body with mass :math:`m` is attached
  483. to a massless, stiff rod, which can rotate about a point, causing the
  484. pendulum to oscillate.
  485. A suggested work flow is to
  486. first sketch the figure on a piece of paper and introduce a coordinate
  487. system. A simple coordinate system is indicated in Figure
  488. :ref:`sketcher:ex:pendulum:fig1wgrid`. In a code we introduce variables
  489. ``W`` and ``H`` for the width and height of the figure (i.e., extent of
  490. the coordinate system) and open the program like this:
  491. .. code-block:: python
  492. from pysketcher import *
  493. H = 7.
  494. W = 6.
  495. drawing_tool.set_coordinate_system(xmin=0, xmax=W,
  496. ymin=0, ymax=H,
  497. axis=True)
  498. drawing_tool.set_grid(True)
  499. drawing_tool.set_linecolor('blue')
  500. Note that when the sketch is ready for "production", we will (normally)
  501. set ``axis=False`` to remove the coordinate system and also remove the
  502. grid, i.e., delete or
  503. comment out the line ``drawing_tool.set_grid(True)``.
  504. Also note that we in this example let all lines be blue by default.
  505. .. _sketcher:ex:pendulum:fig1:
  506. .. figure:: pendulum1.png
  507. :width: 300
  508. *Sketch of a simple pendulum*
  509. .. _sketcher:ex:pendulum:fig1wgrid:
  510. .. figure:: pendulum1_wgrid.png
  511. :width: 400
  512. *Sketch with assisting coordinate system*
  513. The next step is to introduce variables for key quantities in the sketch.
  514. Let ``L`` be the length of the pendulum, ``P`` the rotation point, and let
  515. ``a`` be the angle the pendulum makes with the vertical (measured in degrees).
  516. We may set
  517. .. code-block:: python
  518. L = 5*H/7 # length
  519. P = (W/6, 0.85*H) # rotation point
  520. a = 40 # angle
  521. Be careful with integer division if you use Python 2! Fortunately, we
  522. started out with ``float`` objects for ``W`` and ``H`` so the expressions above
  523. are safe.
  524. What kind of objects do we need in this sketch? Looking at
  525. Figure :ref:`sketcher:ex:pendulum:fig1` we see that we need
  526. 1. a vertical, dashed line
  527. 2. an arc with no text but dashed line to indicate the *path* of the
  528. mass
  529. 3. an arc with name :math:`\theta` to indicate the *angle*
  530. 4. a line, here called *rod*, from the rotation point to the mass
  531. 5. a blue, filled circle representing the *mass*
  532. 6. a text :math:`m` associated with the mass
  533. 7. an indicator of the pendulum's *length* :math:`L`, visualized as
  534. a line with two arrows tips and the text :math:`L`
  535. 8. a gravity vector with the text :math:`g`
  536. Pysketcher has objects for each of these elements in our sketch.
  537. We start with the simplest element: the vertical line, going from
  538. ``P`` to ``P`` minus the length :math:`L` in :math:`y` direction:
  539. .. code-block:: python
  540. vertical = Line(P, P-point(0,L))
  541. The class ``point`` is very convenient: it turns its two coordinates into
  542. a vector with which we can compute, and is therefore one of the most
  543. widely used Pysketcher objects.
  544. The path of the mass is an arc that can be made by
  545. Pysketcher's ``Arc`` object:
  546. .. code-block:: python
  547. path = Arc(P, L, -90, a)
  548. The first argument ``P`` is the center point, the second is the
  549. radius (``L`` here), the next argument is the start angle, here
  550. it starts at -90 degrees, while the next argument is the angle of
  551. the arc, here ``a``.
  552. For the path of the mass, we also need an arc object, but this
  553. time with an associated text. Pysketcher has a specialized object
  554. for this purpose, ``Arc_wText``, since placing the text manually can
  555. be somewhat cumbersome.
  556. .. code-block:: python
  557. angle = Arc_wText(r'$\theta$', P, L/4, -90, a, text_spacing=1/30.)
  558. The arguments are as for ``Arc`` above, but the first one is the desired
  559. text. Remember to use a raw string since we want a LaTeX greek letter
  560. that contains a backslash.
  561. The ``text_spacing`` argument must often be tweaked. It is recommended
  562. to create only a few objects before rendering the sketch and then
  563. adjust spacings as one goes along.
  564. The rod is simply a line from ``P`` to the mass. We can easily
  565. compute the position of the mass from basic geometry considerations,
  566. but it is easier and safer to look up this point in other objects
  567. if it is already computed. In the present case,
  568. the ``path`` object stored its start and
  569. end points, so ``path.geometric_features()['end']`` is the end point
  570. of the path, which is the position of the mass. We can therefore
  571. create the rod simply as a line from ``P`` to this already computed end point:
  572. .. code-block:: python
  573. mass_pt = path.geometric_features()['end']
  574. rod = Line(P, mass_pt)
  575. The mass is a circle filled with color:
  576. .. code-block:: python
  577. mass = Circle(center=mass_pt, radius=L/20.)
  578. mass.set_filled_curves(color='blue')
  579. To place the :math:`m` correctly, we go a small distance in the direction of
  580. the rod, from the center of the circle. To this end, we need to
  581. compute the direction. This is easiest done by computing a vector
  582. from ``P`` to the center of the circle and calling ``unit_vec`` to make
  583. a unit vector in this direction:
  584. .. code-block:: python
  585. rod_vec = rod.geometric_features()['end'] - \
  586. rod.geometric_features()['start']
  587. unit_rod_vec = unit_vec(rod_vec)
  588. mass_symbol = Text('$m$', mass_pt + L/10*unit_rod_vec)
  589. Again, the distance ``L/10`` is something one has to experiment with.
  590. The next object is the length measure with the text :math:`L`. Such length
  591. measures are represented by Pysketcher's ``Distance_wText`` object.
  592. An easy construction is to first place this length measure along the
  593. rod and then translate it a little distance (``L/15``) in the
  594. normal direction of the rod:
  595. .. code-block:: python
  596. length = Distance_wText(P, mass_pt, '$L$')
  597. length.translate(L/15*point(cos(radians(a)), sin(radians(a))))
  598. For this translation we need a unit vector in the normal direction
  599. of the rod, which is from geometric considerations given by
  600. :math:`(\cos a, \sin a)`, when :math:`a` is the angle of the pendulum.
  601. Alternatively, we could have found the normal vector as a vector that
  602. is normal to ``unit_rod_vec``: ``point(-unit_rod_vec[1],unit_rod_vec[0])``.
  603. The final object is the gravity force vector, which is so common
  604. in physics sketches that Pysketcher has a ready-made object: ``Gravity``,
  605. .. code-block:: python
  606. gravity = Gravity(start=P+point(0.8*L,0), length=L/3)
  607. Since blue is the default color for
  608. lines, we want the dashed lines (for ``vertical`` and ``path``) to be black
  609. and with linewidth 1. These properties can be set one by one for each
  610. object, but we can also make a little helper function:
  611. .. code-block:: python
  612. def set_dashed_thin_blackline(*objects):
  613. """Set linestyle of objects to dashed, black, width=1."""
  614. for obj in objects:
  615. obj.set_linestyle('dashed')
  616. obj.set_linecolor('black')
  617. obj.set_linewidth(1)
  618. set_dashed_thin_blackline(vertical, path)
  619. Now, all objects are in place, so it remains to compose the final
  620. figure and draw the composition:
  621. .. code-block:: python
  622. fig = Composition(
  623. {'body': mass, 'rod': rod,
  624. 'vertical': vertical, 'theta': angle, 'path': path,
  625. 'g': gravity, 'L': length, 'm': mass_symbol})
  626. fig.draw()
  627. drawing_tool.display()
  628. drawing_tool.savefig('pendulum1')
  629. The body diagram
  630. ----------------
  631. Now we want to isolate the mass and draw all the forces that act on it.
  632. Figure :ref:`sketcher:ex:pendulum:fig2wgrid` shows the desired result, but
  633. embedded in the coordinate system.
  634. We consider three types of forces: the gravity force, the force from the
  635. rod, and air resistance. The body diagram is key for deriving the
  636. equation of motion, so it is illustrative to add useful mathematical
  637. quantities needed in the derivation, such as the unit vectors in polar
  638. coordinates.
  639. .. _sketcher:ex:pendulum:fig2wgrid:
  640. .. figure:: pendulum5_wgrid.png
  641. :width: 300
  642. *Body diagram of a simple pendulum*
  643. We start by listing the objects in the sketch:
  644. 1. a text :math:`(x_0,y_0)` representing the rotation point ``P``
  645. 2. unit vector :math:`\boldsymbol{i}_r` with text
  646. 3. unit vector :math:`\boldsymbol{i}_\theta` with text
  647. 4. a dashed vertical line
  648. 5. a dashed line along the rod
  649. 6. an arc with text :math:`\theta`
  650. 7. the gravity force with text :math:`mg`
  651. 8. the force in the rod with text :math:`S`
  652. 9. the air resistance force with text :math:`\sim |v|v`
  653. The first object, :math:`(x_0,y_0)`, is simply a plain text where we have
  654. to experiment with its position. The unit vectors in polar coordinates
  655. may be drawn using the Pysketcher's ``Force`` object since it has an
  656. arrow with a text. The first three objects can then be made as follows:
  657. .. code-block:: python
  658. x0y0 = Text('$(x_0,y_0)$', P + point(-0.4,-0.1))
  659. ir = Force(P, P + L/10*unit_vec(rod_vec),
  660. r'$\boldsymbol{i}_r$', text_pos='end',
  661. text_spacing=(0.015,0))
  662. ith = Force(P, P + L/10*unit_vec((-rod_vec[1], rod_vec[0])),
  663. r'$\boldsymbol{i}_{\theta}$', text_pos='end',
  664. text_spacing=(0.02,0.005))
  665. Note that tweaking of the position of ``x0y0`` use absolute coordinates, so
  666. if ``W`` or ``H`` is changed in the beginning of the figure, the tweaked position
  667. will most likely not look good. A better solution would be to express
  668. the tweaked displacement ``point(-0.4,-0.1)`` in terms of ``W`` and ``H``.
  669. The ``text_spacing`` values in the ``Force`` objects also use absolute
  670. coordinates. Very often, this is much more convenient when adjusting
  671. the objects, and global size parameters like ``W`` and ``H`` are in practice
  672. seldom changed, so the solution above is quite typical.
  673. The vertical, dashed line, the dashed rod, and the arc for :math:`\theta`
  674. are made by
  675. .. code-block:: python
  676. rod_start = rod.geometric_features()['start'] # Point P
  677. vertical2 = Line(rod_start, rod_start + point(0,-L/3))
  678. set_dashed_thin_blackline(vertical2)
  679. set_dashed_thin_blackline(rod)
  680. angle2 = Arc_wText(r'$\theta$', rod_start, L/6, -90, a,
  681. text_spacing=1/30.)
  682. Note how we reuse the earlier defined object ``rod``.
  683. The forces are constructed as shown below.
  684. .. code-block:: python
  685. mg_force = Force(mass_pt, mass_pt + L/5*point(0,-1),
  686. '$mg$', text_pos='end')
  687. rod_force = Force(mass_pt, mass_pt - L/3*unit_vec(rod_vec),
  688. '$S$', text_pos='end',
  689. text_spacing=(0.03, 0.01))
  690. air_force = Force(mass_pt, mass_pt -
  691. L/6*unit_vec((rod_vec[1], -rod_vec[0])),
  692. '$\sim|v|v$', text_pos='end',
  693. text_spacing=(0.04,0.005))
  694. Note that the drag force from the air is directed perpendicular to
  695. the rod, so we construct a unit vector in this direction directly from
  696. the ``rod_vec`` vector.
  697. All objects are in place, and we can compose a figure to be drawn:
  698. .. code-block:: python
  699. body_diagram = Composition(
  700. {'mg': mg_force, 'S': rod_force, 'rod': rod,
  701. 'vertical': vertical2, 'theta': angle2,
  702. 'body': mass, 'm': mass_symbol})
  703. body_diagram['air'] = air_force
  704. body_diagram['ir'] = ir
  705. body_diagram['ith'] = ith
  706. body_diagram['origin'] = x0y0
  707. Here, we exemplify that we can start out with a composition as a
  708. dictionary, but (as in ordinary Python dictionaries) add new
  709. elements later when desired.
  710. .. FIGURE: [fig-tut/pendulum1.png, width=300 frac=0.5] Sketch of a simple pendulum.
  711. .. _sketcher:ex:pendulum:anim:
  712. Animated body diagram
  713. ---------------------
  714. We want to make an animated body diagram so that we can see how forces
  715. develop in time according to the motion. This means that we must
  716. couple the sketch at each time level to a numerical solution for
  717. the motion of the pendulum.
  718. Function for drawing the body diagram
  719. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  720. The previous flat program for making sketches of the pendulum is not
  721. suitable when we want to make a sketch at a lot of different points
  722. in time, i.e., for a lot of different angles that the pendulum makes
  723. with the vertical. We therefore need to draw the body diagram in
  724. a function where the angle is a parameter. We also supply arrays
  725. containing the (numerically computed) values of the angle :math:`\theta` and
  726. the forces at various time levels, plus the desired time point and level
  727. for this particular sketch:
  728. .. code-block:: python
  729. from pysketcher import *
  730. H = 15.
  731. W = 17.
  732. drawing_tool.set_coordinate_system(xmin=0, xmax=W,
  733. ymin=0, ymax=H,
  734. axis=False)
  735. def pendulum(theta, S, mg, drag, t, time_level):
  736. drawing_tool.set_linecolor('blue')
  737. import math
  738. a = math.degrees(theta[time_level])
  739. L = 0.4*H # length
  740. P = (W/2, 0.8*H) # rotation point
  741. vertical = Line(P, P-point(0,L))
  742. path = Arc(P, L, -90, a)
  743. angle = Arc_wText(r'$\theta$', P, L/4, -90, a, text_spacing=1/30.)
  744. mass_pt = path.geometric_features()['end']
  745. rod = Line(P, mass_pt)
  746. mass = Circle(center=mass_pt, radius=L/20.)
  747. mass.set_filled_curves(color='blue')
  748. rod_vec = rod.geometric_features()['end'] - \
  749. rod.geometric_features()['start']
  750. unit_rod_vec = unit_vec(rod_vec)
  751. mass_symbol = Text('$m$', mass_pt + L/10*unit_rod_vec)
  752. length = Distance_wText(P, mass_pt, '$L$')
  753. # Displace length indication
  754. length.translate(L/15*point(cos(radians(a)), sin(radians(a))))
  755. gravity = Gravity(start=P+point(0.8*L,0), length=L/3)
  756. def set_dashed_thin_blackline(*objects):
  757. """Set linestyle of objects to dashed, black, width=1."""
  758. for obj in objects:
  759. obj.set_linestyle('dashed')
  760. obj.set_linecolor('black')
  761. obj.set_linewidth(1)
  762. set_dashed_thin_blackline(vertical, path)
  763. fig = Composition(
  764. {'body': mass, 'rod': rod,
  765. 'vertical': vertical, 'theta': angle, 'path': path,
  766. 'g': gravity, 'L': length})
  767. #fig.draw()
  768. #drawing_tool.display()
  769. #drawing_tool.savefig('tmp_pendulum1')
  770. drawing_tool.set_linecolor('black')
  771. rod_start = rod.geometric_features()['start'] # Point P
  772. vertical2 = Line(rod_start, rod_start + point(0,-L/3))
  773. set_dashed_thin_blackline(vertical2)
  774. set_dashed_thin_blackline(rod)
  775. angle2 = Arc_wText(r'$\theta$', rod_start, L/6, -90, a,
  776. text_spacing=1/30.)
  777. magnitude = 1.2*L/2 # length of a unit force in figure
  778. force = mg[time_level] # constant (scaled eq: about 1)
  779. force *= magnitude
  780. mg_force = Force(mass_pt, mass_pt + force*point(0,-1),
  781. '', text_pos='end')
  782. force = S[time_level]
  783. force *= magnitude
  784. rod_force = Force(mass_pt, mass_pt - force*unit_vec(rod_vec),
  785. '', text_pos='end',
  786. text_spacing=(0.03, 0.01))
  787. force = drag[time_level]
  788. force *= magnitude
  789. #print('drag(%g)=%g' % (t, drag[time_level]))
  790. air_force = Force(mass_pt, mass_pt -
  791. force*unit_vec((rod_vec[1], -rod_vec[0])),
  792. '', text_pos='end',
  793. text_spacing=(0.04,0.005))
  794. body_diagram = Composition(
  795. {'mg': mg_force, 'S': rod_force, 'air': air_force,
  796. 'rod': rod,
  797. 'vertical': vertical2, 'theta': angle2,
  798. 'body': mass})
  799. x0y0 = Text('$(x_0,y_0)$', P + point(-0.4,-0.1))
  800. ir = Force(P, P + L/10*unit_vec(rod_vec),
  801. r'$\boldsymbol{i}_r$', text_pos='end',
  802. text_spacing=(0.015,0))
  803. ith = Force(P, P + L/10*unit_vec((-rod_vec[1], rod_vec[0])),
  804. r'$\boldsymbol{i}_{\theta}$', text_pos='end',
  805. text_spacing=(0.02,0.005))
  806. #body_diagram['ir'] = ir
  807. #body_diagram['ith'] = ith
  808. #body_diagram['origin'] = x0y0
  809. drawing_tool.erase()
  810. body_diagram.draw(verbose=0)
  811. #drawing_tool.display('Body diagram')
  812. drawing_tool.savefig('tmp_%04d.png' % time_level, crop=False)
  813. # No cropping: otherwise movies will be very strange
  814. Equations for the motion and forces
  815. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  816. The modeling of the motion of a pendulum is most conveniently done in
  817. polar coordinates since then the unknown force in the rod is separated
  818. from the equation determining the motion :math:`\theta(t)`.
  819. The position vector for the mass is
  820. .. math::
  821. \boldsymbol{r} = x_0\boldsymbol{i} + y_0\boldsymbol{j} + L{\boldsymbol{i}_r}{\thinspace .}
  822. The corresponding acceleration becomes
  823. .. math::
  824. \ddot{\boldsymbol{r}} = L\ddot{\theta}{\boldsymbol{i}_{\theta}} - L\dot{\theta^2}{{\boldsymbol{i}_r}}{\thinspace .}
  825. .. Note: the extra braces help to render the equation correctly in sphinx!
  826. There are three forces on the mass: the gravity force
  827. :math:`mg\boldsymbol{j} = mg(-\cos\theta\,{\boldsymbol{i}_r} + \sin\theta\,\boldsymbol{i}_{\theta})`, the force in the rod
  828. :math:`-S{\boldsymbol{i}_r}`, and the drag force because of air resistance:
  829. .. math::
  830. -\frac{1}{2} C_D \varrho \pi R^2 |v|v\,\boldsymbol{i}_{\theta},
  831. where :math:`C_D\approx 0.4` is the drag coefficient for a sphere, :math:`\varrho`
  832. is the density of air, :math:`R` is the radius of the mass, and :math:`v` is the
  833. velocity (:math:`v=L\dot\theta`). The drag force acts in :math:`-\boldsymbol{i}_{\theta}` direction
  834. when :math:`v>0`.
  835. Newton's second law of motion for the pendulum now becomes
  836. .. math::
  837. mL\ddot\theta\boldsymbol{i}_{\theta} - mL\dot\theta^2{\boldsymbol{i}_r} = -mg(-\cos\theta\,{\boldsymbol{i}_r} +
  838. \sin\theta\,\boldsymbol{i}_{\theta})
  839. -S{\boldsymbol{i}_r} - \frac{1}{2} C_D \varrho \pi R^2 L^2|\dot\theta|\dot\theta\boldsymbol{i}_{\theta},
  840. which gives two component equations
  841. .. math::
  842. :label: sketcher:ex:pendulum:anim:eq:ith
  843. mL\ddot\theta + \frac{1}{2} C_D \varrho \pi R^2 L^2|\dot\theta|\dot\theta +
  844. mg\sin\theta = 0,
  845. .. math::
  846. :label: sketcher:ex:pendulum:anim:eq:ir
  847. S = mL\dot\theta^2 + mg\cos\theta
  848. {\thinspace .}
  849. It is almost always convenient to scale such equations. Introducing
  850. the dimensionless time
  851. .. math::
  852. \bar t = \frac{t}{t_c},\quad t_c = \sqrt{\frac{L}{g}},
  853. leads to
  854. .. math::
  855. :label: sketcher:ex:pendulum:anim:eq:ith:s
  856. \frac{d^2\theta}{d\bar t^2} +
  857. \alpha\left\vert\frac{d\theta}{d\bar t}\right\vert\frac{d\theta}{d\bar t} +
  858. \sin\theta = 0,
  859. .. math::
  860. :label: sketcher:ex:pendulum:anim:eq:ir:s
  861. \bar S = \left(\frac{d\theta}{d\bar t}\right)^2
  862. + \cos\theta,
  863. where :math:`\alpha` is a dimensionless drag coefficient
  864. .. math::
  865. \alpha = \frac{C_D\varrho\pi R^2L}{2m},
  866. and :math:`\bar S` is the scaled force
  867. .. math::
  868. \bar S = \frac{S}{mg}{\thinspace .}
  869. We see that :math:`\bar S = 1` for the equilibrium position :math:`\theta=0`, so this
  870. scaling of :math:`S` seems appropriate.
  871. The parameter :math:`\alpha` is about
  872. the ratio of the drag force and the gravity force:
  873. .. math::
  874. \frac{|\frac{1}{2} C_D\varrho \pi R^2 |v|v|}{|mg|}\sim
  875. \frac{C_D\varrho \pi R^2 L^2 t_c^{-2}}{mg}
  876. \left|\frac{d\bar\theta}{d\bar t}\right|\frac{d\bar\theta}{d\bar t}
  877. \sim \frac{C_D\varrho \pi R^2 L}{2m}\theta_0^2 = \alpha \theta_0^2{\thinspace .}
  878. (We have that :math:`\theta(t)/d\theta_0` is in :math:`[-1,1]`, so we expect
  879. since :math:`\theta_0^{-1}d\bar\theta/d\bar t` to be around unity. Here,
  880. :math:`\theta_0=\theta(0)`.)
  881. The next step is to write a numerical solver for
  882. :eq:`sketcher:ex:pendulum:anim:eq:ith:s`-:eq:`sketcher:ex:pendulum:anim:eq:ir:s`. To
  883. this end, we use the `Odespy <https://github.com/hplgit/odespy>`__
  884. package. The system of second-order ODEs must be expressed as a system
  885. of first-order ODEs. We realize that the unknown :math:`\bar S` is decoupled
  886. from :math:`\theta` in the sense that we can first use
  887. :eq:`sketcher:ex:pendulum:anim:eq:ith:s` to solve for :math:`\theta` and
  888. then compute :math:`\bar S` from :eq:`sketcher:ex:pendulum:anim:eq:ir:s`.
  889. The first-order ODEs become
  890. .. math::
  891. :label: _auto1
  892. \frac{d\omega}{d\bar t} = -\alpha\left\vert\omega\right\vert\omega
  893. - \sin\theta,
  894. .. math::
  895. :label: _auto2
  896. \frac{d\theta}{d\bar t} = \omega{\thinspace .}
  897. Then we compute
  898. .. math::
  899. :label: _auto3
  900. \bar S = \omega^2 + \cos\theta{\thinspace .}
  901. The dimensionless air resistance force can also be computed:
  902. .. math::
  903. :label: _auto4
  904. -\alpha|\omega|\omega{\thinspace .}
  905. Since we scaled the force :math:`S` by :math:`mg`, :math:`mg` is the natural force scale,
  906. and the :math:`mg` force itself is then unity.
  907. By updating :math:`\omega` in the first equation, we can use an Euler-Cromer
  908. scheme on Odespy (all other schemes are independent of whether the
  909. :math:`\theta` or :math:`\omega` equation comes first).
  910. Numerical solution
  911. ~~~~~~~~~~~~~~~~~~
  912. An appropriate solver is
  913. .. code-block:: python
  914. def simulate_pendulum(alpha, theta0, dt, T):
  915. import odespy
  916. def f(u, t, alpha):
  917. omega, theta = u
  918. return [-alpha*omega*abs(omega) - sin(theta),
  919. omega]
  920. import numpy as np
  921. Nt = int(round(T/float(dt)))
  922. t = np.linspace(0, Nt*dt, Nt+1)
  923. solver = odespy.RK4(f, f_args=[alpha])
  924. solver.set_initial_condition([0, theta0])
  925. u, t = solver.solve(t,
  926. terminate=lambda u, t, n: abs(u[n,1]) < 1E-3)
  927. omega = u[:,0]
  928. theta = u[:,1]
  929. S = omega**2 + np.cos(theta)
  930. drag = -alpha*np.abs(omega)*omega
  931. return t, theta, omega, S, drag
  932. Animation
  933. ~~~~~~~~~
  934. We can finally traverse the time array and draw a body diagram
  935. at each time level. The resulting sketches are saved to files
  936. ``tmp_%04d.png``, and these files can be combined to videos:
  937. .. code-block:: python
  938. def animate():
  939. # Clean up old plot files
  940. import os, glob
  941. for filename in glob.glob('tmp_*.png') + glob.glob('movie.*'):
  942. os.remove(filename)
  943. # Solve problem
  944. from math import pi, radians, degrees
  945. import numpy as np
  946. alpha = 0.4
  947. period = 2*pi
  948. T = 12*period
  949. dt = period/40
  950. a = 70
  951. theta0 = radians(a)
  952. t, theta, omega, S, drag = simulate_pendulum(alpha, theta0, dt, T)
  953. mg = np.ones(S.size)
  954. # Visualize drag force 5 times as large
  955. drag *= 5
  956. print('NOTE: drag force magnified 5 times!!')
  957. # Draw animation
  958. import time
  959. for time_level, t_ in enumerate(t):
  960. pendulum(theta, S, mg, drag, t_, time_level)
  961. time.sleep(0.2)
  962. # Make videos
  963. prog = 'ffmpeg'
  964. filename = 'tmp_%04d.png'
  965. fps = 6
  966. codecs = {'flv': 'flv', 'mp4': 'libx264',
  967. 'webm': 'libvpx', 'ogg': 'libtheora'}
  968. for ext in codecs:
  969. lib = codecs[ext]
  970. cmd = '%(prog)s -i %(filename)s -r %(fps)s ' % vars()
  971. cmd += '-vcodec %(lib)s movie.%(ext)s' % vars()
  972. print(cmd)
  973. os.system(cmd)
  974. This time we did not use the ``animate`` function from Pysketcher, but
  975. stored each sketch in a file with ``drawing_tool.savefig``. Note that
  976. the argument ``crop=False`` is key: otherwise each figure is cropped and
  977. it makes to sense to combine the images to a video. By default,
  978. Pysketcher crops (removes all exterior whitespace) from figures saved
  979. to file.
  980. .. raw:: html
  981. <div>
  982. <video loop controls width='640' height='365' preload='none'>
  983. <source src='https://github.com/hplgit/pysketcher/raw/master/doc/pub/tutorial/mov-tut/pendulum/movie.mp4' type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
  984. <source src='https://github.com/hplgit/pysketcher/raw/master/doc/pub/tutorial/mov-tut/pendulum/movie.webm' type='video/webm; codecs="vp8, vorbis"'>
  985. <source src='https://github.com/hplgit/pysketcher/raw/master/doc/pub/tutorial/mov-tut/pendulum/movie.ogg' type='video/ogg; codecs="theora, vorbis"'>
  986. </video>
  987. </div>
  988. <p><em>The drag force is magnified 5 times (compared to :math:`mg` and :math:`S`!</em></p>
  989. <!-- Issue warning if in a Safari browser -->
  990. <script language="javascript">
  991. if (!!(window.safari)) {
  992. document.write("<div style=\"width: 95%%; padding: 10px; border: 1px solid #100; border-radius: 4px;\"><p><font color=\"red\">The above movie will not play in Safari - use Chrome, Firefox, or Opera.</font></p></div>")}
  993. </script>
  994. .. !split
  995. Basic shapes
  996. ============
  997. This section presents many of the basic shapes in Pysketcher:
  998. ``Axis``, ``Distance_wText``, ``Rectangle``, ``Triangle``, ``Arc``,
  999. ``Spring``, ``Dashpot``, and ``Wavy``.
  1000. Each shape is demonstrated with a figure and a
  1001. unit test that shows how the figure is constructed in Python code.
  1002. These demos rely heavily on the method ``draw_dimensions`` in
  1003. the shape classes, which annotates the basic drawing of the shape
  1004. with the various geometric parameters that govern the shape.
  1005. Axis
  1006. ----
  1007. The ``Axis`` object gives the possibility draw a single axis to
  1008. notify a coordinate system. Here is an example where we
  1009. draw :math:`x` and :math:`y` axis of three coordinate systems of different
  1010. rotation:
  1011. |
  1012. |
  1013. .. figure:: Axis.png
  1014. :width: 500
  1015. |
  1016. |
  1017. The corresponding code looks like this:
  1018. .. code-block:: python
  1019. def test_Axis():
  1020. drawing_tool.set_coordinate_system(
  1021. xmin=0, xmax=15, ymin=-7, ymax=8, axis=True,
  1022. instruction_file='tmp_Axis.py')
  1023. # Draw normal x and y axis with origin at (7.5, 2)
  1024. # in the coordinate system of the sketch: [0,15]x[-7,8]
  1025. x_axis = Axis((7.5,2), 5, 'x', rotation_angle=0)
  1026. y_axis = Axis((7.5,2), 5, 'y', rotation_angle=90)
  1027. system = Composition({'x axis': x_axis, 'y axis': y_axis})
  1028. system.draw()
  1029. drawing_tool.display()
  1030. # Rotate this system 40 degrees counter clockwise
  1031. # and draw it with dashed lines
  1032. system.set_linestyle('dashed')
  1033. system.rotate(40, (7.5,2))
  1034. system.draw()
  1035. drawing_tool.display()
  1036. # Rotate this system another 220 degrees and show
  1037. # with dotted lines
  1038. system.set_linestyle('dotted')
  1039. system.rotate(220, (7.5,2))
  1040. system.draw()
  1041. drawing_tool.display()
  1042. drawing_tool.display('Axis')
  1043. Distance with text
  1044. ------------------
  1045. The object ``Distance_wText`` is used to display an arrow, to indicate
  1046. a distance in a sketch, with an additional text in the middle of the arrow.
  1047. The figure
  1048. |
  1049. |
  1050. .. figure:: Distance_wText.png
  1051. :width: 500
  1052. |
  1053. |
  1054. was produced by this code:
  1055. .. code-block:: python
  1056. def test_Distance_wText():
  1057. drawing_tool.set_coordinate_system(
  1058. xmin=0, xmax=10, ymin=0, ymax=6,
  1059. axis=True, instruction_file='tmp_Distance_wText.py')
  1060. fontsize=14
  1061. t = r'$ 2\pi R^2 $' # sample text
  1062. examples = Composition({
  1063. 'a0': Distance_wText((4,5), (8, 5), t, fontsize),
  1064. 'a6': Distance_wText((4,5), (4, 4), t, fontsize),
  1065. 'a1': Distance_wText((0,2), (2, 4.5), t, fontsize),
  1066. 'a2': Distance_wText((0,2), (2, 0), t, fontsize),
  1067. 'a3': Distance_wText((2,4.5), (0, 5.5), t, fontsize),
  1068. 'a4': Distance_wText((8,4), (10, 3), t, fontsize,
  1069. text_spacing=-1./60),
  1070. 'a5': Distance_wText((8,2), (10, 1), t, fontsize,
  1071. text_spacing=-1./40, alignment='right'),
  1072. 'c1': Text_wArrow('text_spacing=-1./60',
  1073. (4, 3.5), (9, 3.2),
  1074. fontsize=10, alignment='left'),
  1075. 'c2': Text_wArrow('text_spacing=-1./40, alignment="right"',
  1076. (4, 0.5), (9, 1.2),
  1077. fontsize=10, alignment='left'),
  1078. })
  1079. examples.draw()
  1080. drawing_tool.display('Distance_wText and text positioning')
  1081. Note the use of ``Text_wArrow`` to write an explaining text with an
  1082. associated arrow, here used to explain how
  1083. the ``text_spacing`` and ``alignment`` arguments can be used to adjust
  1084. the appearance of the text that goes with the distance arrow.
  1085. Rectangle
  1086. ---------
  1087. .. figure:: Rectangle.png
  1088. :width: 500
  1089. |
  1090. |
  1091. The above figure can be produced by the following code.
  1092. .. code-block:: python
  1093. def test_Rectangle():
  1094. L = 3.0
  1095. W = 4.0
  1096. drawing_tool.set_coordinate_system(
  1097. xmin=0, xmax=2*W, ymin=-L/2, ymax=2*L,
  1098. axis=True, instruction_file='tmp_Rectangle.py')
  1099. drawing_tool.set_linecolor('blue')
  1100. drawing_tool.set_grid(True)
  1101. xpos = W/2
  1102. r = Rectangle(lower_left_corner=(xpos,0), width=W, height=L)
  1103. r.draw()
  1104. r.draw_dimensions()
  1105. drawing_tool.display('Rectangle')
  1106. Note that the ``draw_dimension`` method adds explanation of dimensions and various
  1107. important argument in the construction of a shape. It adapts the annotations
  1108. to the geometry of the current shape.
  1109. Triangle
  1110. --------
  1111. .. figure:: Triangle.png
  1112. :width: 500
  1113. |
  1114. |
  1115. The code below produces the figure.
  1116. .. code-block:: python
  1117. def test_Triangle():
  1118. L = 3.0
  1119. W = 4.0
  1120. drawing_tool.set_coordinate_system(
  1121. xmin=0, xmax=2*W, ymin=-L/2, ymax=1.2*L,
  1122. axis=True, instruction_file='tmp_Triangle.py')
  1123. drawing_tool.set_linecolor('blue')
  1124. drawing_tool.set_grid(True)
  1125. xpos = 1
  1126. t = Triangle(p1=(W/2,0), p2=(3*W/2,W/2), p3=(4*W/5.,L))
  1127. t.draw()
  1128. t.draw_dimensions()
  1129. drawing_tool.display('Triangle')
  1130. Here, the ``draw_dimension`` method writes the name of the corners at the
  1131. position of the corners, which does not always look nice (the present figure
  1132. is an example). For a high-quality sketch one would add some spacing
  1133. to the location of the p1, p2, and even p3 texts.
  1134. Arc
  1135. ---
  1136. .. figure:: Arc.png
  1137. :width: 400
  1138. |
  1139. |
  1140. An arc like the one above is produced by
  1141. .. code-block:: python
  1142. def test_Arc():
  1143. L = 4.0
  1144. W = 4.0
  1145. drawing_tool.set_coordinate_system(
  1146. xmin=-W/2, xmax=W, ymin=-L/2, ymax=1.5*L,
  1147. axis=True, instruction_file='tmp_Arc.py')
  1148. drawing_tool.set_linecolor('blue')
  1149. drawing_tool.set_grid(True)
  1150. center = point(0,0)
  1151. radius = L/2
  1152. start_angle = 60
  1153. arc_angle = 45
  1154. a = Arc(center, radius, start_angle, arc_angle)
  1155. a.draw()
  1156. R1 = 1.25*radius
  1157. R2 = 1.5*radius
  1158. R = 2*radius
  1159. a.dimensions = {
  1160. 'start_angle':
  1161. Arc_wText(
  1162. 'start_angle', center, R1, start_angle=0,
  1163. arc_angle=start_angle, text_spacing=1/10.),
  1164. 'arc_angle':
  1165. Arc_wText(
  1166. 'arc_angle', center, R2, start_angle=start_angle,
  1167. arc_angle=arc_angle, text_spacing=1/20.),
  1168. 'r=0':
  1169. Line(center, center +
  1170. point(R*cos(radians(start_angle)),
  1171. R*sin(radians(start_angle)))),
  1172. 'r=start_angle':
  1173. Line(center, center +
  1174. point(R*cos(radians(start_angle+arc_angle)),
  1175. R*sin(radians(start_angle+arc_angle)))),
  1176. 'r=start+arc_angle':
  1177. Line(center, center +
  1178. point(R, 0)).set_linestyle('dashed'),
  1179. 'radius': Distance_wText(center, a(0), 'radius', text_spacing=1/40.),
  1180. 'center': Text('center', center-point(radius/10., radius/10.)),
  1181. }
  1182. for dimension in a.dimensions:
  1183. if dimension.startswith('r='):
  1184. dim = a.dimensions[dimension]
  1185. dim.set_linestyle('dashed')
  1186. dim.set_linewidth(1)
  1187. dim.set_linecolor('black')
  1188. a.draw_dimensions()
  1189. drawing_tool.display('Arc')
  1190. Spring
  1191. ------
  1192. .. figure:: Spring.png
  1193. :width: 800
  1194. |
  1195. |
  1196. The code for making these two springs goes like this:
  1197. .. code-block:: python
  1198. def test_Spring():
  1199. L = 5.0
  1200. W = 2.0
  1201. drawing_tool.set_coordinate_system(
  1202. xmin=0, xmax=7*W, ymin=-L/2, ymax=1.5*L,
  1203. axis=True, instruction_file='tmp_Spring.py')
  1204. drawing_tool.set_linecolor('blue')
  1205. drawing_tool.set_grid(True)
  1206. xpos = W
  1207. s1 = Spring((W,0), L, teeth=True)
  1208. s1_title = Text('Default Spring',
  1209. s1.geometric_features()['end'] + point(0,L/10))
  1210. s1.draw()
  1211. s1_title.draw()
  1212. #s1.draw_dimensions()
  1213. xpos += 3*W
  1214. s2 = Spring(start=(xpos,0), length=L, width=W/2.,
  1215. bar_length=L/6., teeth=False)
  1216. s2.draw()
  1217. s2.draw_dimensions()
  1218. drawing_tool.display('Spring')
  1219. Dashpot
  1220. -------
  1221. .. figure:: Dashpot.png
  1222. :width: 600
  1223. |
  1224. |
  1225. This dashpot is produced by
  1226. .. code-block:: python
  1227. def test_Dashpot():
  1228. L = 5.0
  1229. W = 2.0
  1230. xpos = 0
  1231. drawing_tool.set_coordinate_system(
  1232. xmin=xpos, xmax=xpos+5.5*W, ymin=-L/2, ymax=1.5*L,
  1233. axis=True, instruction_file='tmp_Dashpot.py')
  1234. drawing_tool.set_linecolor('blue')
  1235. drawing_tool.set_grid(True)
  1236. # Default (simple) dashpot
  1237. xpos = 1.5
  1238. d1 = Dashpot(start=(xpos,0), total_length=L)
  1239. d1_title = Text('Dashpot (default)',
  1240. d1.geometric_features()['end'] + point(0,L/10))
  1241. d1.draw()
  1242. d1_title.draw()
  1243. # Dashpot for animation with fixed bar_length, dashpot_length and
  1244. # prescribed piston_pos
  1245. xpos += 2.5*W
  1246. d2 = Dashpot(start=(xpos,0), total_length=1.2*L, width=W/2,
  1247. bar_length=W, dashpot_length=L/2, piston_pos=2*W)
  1248. d2.draw()
  1249. d2.draw_dimensions()
  1250. drawing_tool.display('Dashpot')
  1251. Wavy
  1252. ----
  1253. Looks strange. Fix x axis.
  1254. Stochastic curves
  1255. -----------------
  1256. The ``StochasticWavyCurve`` object offers three precomputed
  1257. graphics that have a random variation:
  1258. |
  1259. |
  1260. .. figure:: StochasticWavyCurve.png
  1261. :width: 600
  1262. |
  1263. |
  1264. The usage is simple. The construction
  1265. .. code-block:: python
  1266. curve = StochasticWavyCurve(curve_no=1, percentage=40)
  1267. picks the second curve (the three are numbered 0, 1, and 2),
  1268. and the first 40% of that curve. In case one desires another extent
  1269. of the axis, one can just scale the coordinates directly as these
  1270. are stored in the arrays ``curve.x[curve_no]`` and
  1271. ``curve.y[curve_no]``.
  1272. .. !split
  1273. Inner workings of the Pysketcher tool
  1274. =====================================
  1275. We shall now explain how we can, quite easily, realize software with
  1276. the capabilities demonstrated in the previous examples. Each object in
  1277. the figure is represented as a class in a class hierarchy. Using
  1278. inheritance, classes can inherit properties from parent classes and
  1279. add new geometric features.
  1280. .. index:: tree data structure
  1281. Class programming is a key technology for realizing Pysketcher.
  1282. As soon as some classes are established, more are easily
  1283. added. Enhanced functionality for all the classes is also easy to
  1284. implement in common, generic code that can immediately be shared by
  1285. all present and future classes. The fundamental data structure
  1286. involved in the ``pysketcher`` package is a hierarchical tree, and much
  1287. of the material on implementation issues targets how to traverse tree
  1288. structures with recursive function calls in object hierarchies. This
  1289. topic is of key relevance in a wide range of other applications as
  1290. well. In total, the inner workings of Pysketcher constitute an
  1291. excellent example on the power of class programming.
  1292. Example of classes for geometric objects
  1293. ----------------------------------------
  1294. We introduce class ``Shape`` as superclass for all specialized objects
  1295. in a figure. This class does not store any data, but provides a
  1296. series of functions that add functionality to all the subclasses.
  1297. This will be shown later.
  1298. Simple geometric objects
  1299. ~~~~~~~~~~~~~~~~~~~~~~~~
  1300. One simple subclass is ``Rectangle``, specified by the coordinates of
  1301. the lower left corner and its width and height:
  1302. .. code-block:: python
  1303. class Rectangle(Shape):
  1304. def __init__(self, lower_left_corner, width, height):
  1305. p = lower_left_corner # short form
  1306. x = [p[0], p[0] + width,
  1307. p[0] + width, p[0], p[0]]
  1308. y = [p[1], p[1], p[1] + height,
  1309. p[1] + height, p[1]]
  1310. self.shapes = {'rectangle': Curve(x,y)}
  1311. Any subclass of ``Shape`` will have a constructor that takes geometric
  1312. information about the shape of the object and creates a dictionary
  1313. ``self.shapes`` with the shape built of simpler shapes. The most
  1314. fundamental shape is ``Curve``, which is just a collection of :math:`(x,y)`
  1315. coordinates in two arrays ``x`` and ``y``. Drawing the ``Curve`` object is
  1316. a matter of plotting ``y`` versus ``x``. For class ``Rectangle`` the ``x``
  1317. and ``y`` arrays contain the corner points of the rectangle in
  1318. counterclockwise direction, starting and ending with in the lower left
  1319. corner.
  1320. Class ``Line`` is also a simple class:
  1321. .. code-block:: python
  1322. class Line(Shape):
  1323. def __init__(self, start, end):
  1324. x = [start[0], end[0]]
  1325. y = [start[1], end[1]]
  1326. self.shapes = {'line': Curve(x, y)}
  1327. Here we only need two points, the start and end point on the line.
  1328. However, we may want to add some useful functionality, e.g., the ability
  1329. to give an :math:`x` coordinate and have the class calculate the
  1330. corresponding :math:`y` coordinate:
  1331. .. code-block:: python
  1332. def __call__(self, x):
  1333. """Given x, return y on the line."""
  1334. x, y = self.shapes['line'].x, self.shapes['line'].y
  1335. self.a = (y[1] - y[0])/(x[1] - x[0])
  1336. self.b = y[0] - self.a*x[0]
  1337. return self.a*x + self.b
  1338. Unfortunately, this is too simplistic because vertical lines cannot be
  1339. handled (infinite ``self.a``). The true source code of ``Line`` therefore
  1340. provides a more general solution at the cost of significantly longer
  1341. code with more tests.
  1342. A circle implies a somewhat increased complexity. Again we represent
  1343. the geometric object by a ``Curve`` object, but this time the ``Curve``
  1344. object needs to store a large number of points on the curve such that
  1345. a plotting program produces a visually smooth curve. The points on
  1346. the circle must be calculated manually in the constructor of class
  1347. ``Circle``. The formulas for points :math:`(x,y)` on a curve with radius :math:`R`
  1348. and center at :math:`(x_0, y_0)` are given by
  1349. .. math::
  1350. x &= x_0 + R\cos (t),\\
  1351. y &= y_0 + R\sin (t),
  1352. where :math:`t\in [0, 2\pi]`. A discrete set of :math:`t` values in this
  1353. interval gives the corresponding set of :math:`(x,y)` coordinates on
  1354. the circle. The user must specify the resolution as the number
  1355. of :math:`t` values. The circle's radius and center must of course
  1356. also be specified.
  1357. We can write the ``Circle`` class as
  1358. .. code-block:: python
  1359. class Circle(Shape):
  1360. def __init__(self, center, radius, resolution=180):
  1361. self.center, self.radius = center, radius
  1362. self.resolution = resolution
  1363. t = linspace(0, 2*pi, resolution+1)
  1364. x0 = center[0]; y0 = center[1]
  1365. R = radius
  1366. x = x0 + R*cos(t)
  1367. y = y0 + R*sin(t)
  1368. self.shapes = {'circle': Curve(x, y)}
  1369. As in class ``Line`` we can offer the possibility to give an angle
  1370. :math:`\theta` (equivalent to :math:`t` in the formulas above)
  1371. and then get the corresponding :math:`x` and :math:`y` coordinates:
  1372. .. code-block:: python
  1373. def __call__(self, theta):
  1374. """Return (x, y) point corresponding to angle theta."""
  1375. return self.center[0] + self.radius*cos(theta), \
  1376. self.center[1] + self.radius*sin(theta)
  1377. There is one flaw with this method: it yields illegal values after
  1378. a translation, scaling, or rotation of the circle.
  1379. A part of a circle, an arc, is a frequent geometric object when
  1380. drawing mechanical systems. The arc is constructed much like
  1381. a circle, but :math:`t` runs in :math:`[\theta_s, \theta_s + \theta_a]`. Giving
  1382. :math:`\theta_s` and :math:`\theta_a` the slightly more descriptive names
  1383. ``start_angle`` and ``arc_angle``, the code looks like this:
  1384. .. code-block:: python
  1385. class Arc(Shape):
  1386. def __init__(self, center, radius,
  1387. start_angle, arc_angle,
  1388. resolution=180):
  1389. self.start_angle = radians(start_angle)
  1390. self.arc_angle = radians(arc_angle)
  1391. t = linspace(self.start_angle,
  1392. self.start_angle + self.arc_angle,
  1393. resolution+1)
  1394. x0 = center[0]; y0 = center[1]
  1395. R = radius
  1396. x = x0 + R*cos(t)
  1397. y = y0 + R*sin(t)
  1398. self.shapes = {'arc': Curve(x, y)}
  1399. Having the ``Arc`` class, a ``Circle`` can alternatively be defined as
  1400. a subclass specializing the arc to a circle:
  1401. .. code-block:: python
  1402. class Circle(Arc):
  1403. def __init__(self, center, radius, resolution=180):
  1404. Arc.__init__(self, center, radius, 0, 360, resolution)
  1405. Class curve
  1406. ~~~~~~~~~~~
  1407. Class ``Curve`` sits on the coordinates to be drawn, but how is that
  1408. done? The constructor of class ``Curve`` just stores the coordinates,
  1409. while a method ``draw`` sends the coordinates to the plotting program to
  1410. make a graph. Or more precisely, to avoid a lot of (e.g.)
  1411. Matplotlib-specific plotting commands in class ``Curve`` we have created
  1412. a small layer with a simple programming interface to plotting
  1413. programs. This makes it straightforward to change from Matplotlib to
  1414. another plotting program. The programming interface is represented by
  1415. the ``drawing_tool`` object and has a few functions:
  1416. * ``plot_curve`` for sending a curve in terms of :math:`x` and :math:`y` coordinates
  1417. to the plotting program,
  1418. * ``set_coordinate_system`` for specifying the graphics area,
  1419. * ``erase`` for deleting all elements of the graph,
  1420. * ``set_grid`` for turning on a grid (convenient while constructing the figure),
  1421. * ``set_instruction_file`` for creating a separate file with all
  1422. plotting commands (Matplotlib commands in our case),
  1423. * a series of ``set_X`` functions where ``X`` is some property like
  1424. ``linecolor``, ``linestyle``, ``linewidth``, ``filled_curves``.
  1425. This is basically all we need to communicate to a plotting program.
  1426. Any class in the ``Shape`` hierarchy inherits ``set_X`` functions for
  1427. setting properties of curves. This information is propagated to
  1428. all other shape objects in the ``self.shapes`` dictionary. Class
  1429. ``Curve`` stores the line properties together with the coordinates
  1430. of its curve and propagates this information to the plotting program.
  1431. When saying ``vehicle.set_linewidth(10)``, all objects that make
  1432. up the ``vehicle`` object will get a ``set_linewidth(10)`` call,
  1433. but only the ``Curve`` object at the end of the chain will actually
  1434. store the information and send it to the plotting program.
  1435. A rough sketch of class ``Curve`` reads
  1436. .. code-block:: python
  1437. class Curve(Shape):
  1438. """General curve as a sequence of (x,y) coordintes."""
  1439. def __init__(self, x, y):
  1440. self.x = asarray(x, dtype=float)
  1441. self.y = asarray(y, dtype=float)
  1442. def draw(self):
  1443. drawing_tool.plot_curve(
  1444. self.x, self.y,
  1445. self.linestyle, self.linewidth, self.linecolor, ...)
  1446. def set_linewidth(self, width):
  1447. self.linewidth = width
  1448. det set_linestyle(self, style):
  1449. self.linestyle = style
  1450. ...
  1451. Compound geometric objects
  1452. ~~~~~~~~~~~~~~~~~~~~~~~~~~
  1453. The simple classes ``Line``, ``Arc``, and ``Circle`` could can the geometric
  1454. shape through just one ``Curve`` object. More complicated shapes are
  1455. built from instances of various subclasses of ``Shape``. Classes used
  1456. for professional drawings soon get quite complex in composition and
  1457. have a lot of geometric details, so here we prefer to make a very
  1458. simple composition: the already drawn vehicle from Figure
  1459. :ref:`sketcher:fig:vehicle0`. That is, instead of composing the drawing
  1460. in a Python program as shown above, we make a subclass ``Vehicle0`` in
  1461. the ``Shape`` hierarchy for doing the same thing.
  1462. The ``Shape`` hierarchy is found in the ``pysketcher`` package, so to use these
  1463. classes or derive a new one, we need to import ``pysketcher``. The constructor
  1464. of class ``Vehicle0`` performs approximately the same statements as
  1465. in the example program we developed for making the drawing in
  1466. Figure :ref:`sketcher:fig:vehicle0`.
  1467. .. code-block:: python
  1468. from pysketcher import *
  1469. class Vehicle0(Shape):
  1470. def __init__(self, w_1, R, L, H):
  1471. wheel1 = Circle(center=(w_1, R), radius=R)
  1472. wheel2 = wheel1.copy()
  1473. wheel2.translate((L,0))
  1474. under = Rectangle(lower_left_corner=(w_1-2*R, 2*R),
  1475. width=2*R + L + 2*R, height=H)
  1476. over = Rectangle(lower_left_corner=(w_1, 2*R + H),
  1477. width=2.5*R, height=1.25*H)
  1478. wheels = Composition(
  1479. {'wheel1': wheel1, 'wheel2': wheel2})
  1480. body = Composition(
  1481. {'under': under, 'over': over})
  1482. vehicle = Composition({'wheels': wheels, 'body': body})
  1483. xmax = w_1 + 2*L + 3*R
  1484. ground = Wall(x=[R, xmax], y=[0, 0], thickness=-0.3*R)
  1485. self.shapes = {'vehicle': vehicle, 'ground': ground}
  1486. Any subclass of ``Shape`` *must* define the ``shapes`` attribute, otherwise
  1487. the inherited ``draw`` method (and a lot of other methods too) will
  1488. not work.
  1489. The painting of the vehicle, as shown in the right part of
  1490. Figure :ref:`sketcher:fig:vehicle0:v2`, could in class ``Vehicle0``
  1491. be offered by a method:
  1492. .. code-block:: python
  1493. def colorful(self):
  1494. wheels = self.shapes['vehicle']['wheels']
  1495. wheels.set_filled_curves('blue')
  1496. wheels.set_linewidth(6)
  1497. wheels.set_linecolor('black')
  1498. under = self.shapes['vehicle']['body']['under']
  1499. under.set_filled_curves('red')
  1500. over = self.shapes['vehicle']['body']['over']
  1501. over.set_filled_curves(pattern='/')
  1502. over.set_linewidth(14)
  1503. The usage of the class is simple: after having set up an appropriate
  1504. coordinate system as previously shown, we can do
  1505. .. code-block:: python
  1506. vehicle = Vehicle0(w_1, R, L, H)
  1507. vehicle.draw()
  1508. drawing_tool.display()
  1509. and go on the make a painted version by
  1510. .. code-block:: python
  1511. drawing_tool.erase()
  1512. vehicle.colorful()
  1513. vehicle.draw()
  1514. drawing_tool.display()
  1515. A complete code defining and using class ``Vehicle0`` is found in the file
  1516. `vehicle2.py <http://tinyurl.com/ot733jn/vehicle2.py>`__.
  1517. The ``pysketcher`` package contains a wide range of classes for various
  1518. geometrical objects, particularly those that are frequently used in
  1519. drawings of mechanical systems.
  1520. Adding functionality via recursion
  1521. ----------------------------------
  1522. .. index:: recursive function calls
  1523. The really powerful feature of our class hierarchy is that we can add
  1524. much functionality to the superclass ``Shape`` and to the "bottom" class
  1525. ``Curve``, and then all other classes for various types of geometrical shapes
  1526. immediately get the new functionality. To explain the idea we may
  1527. look at the ``draw`` method, which all classes in the ``Shape``
  1528. hierarchy must have. The inner workings of the ``draw`` method explain
  1529. the secrets of how a series of other useful operations on figures
  1530. can be implemented.
  1531. Basic principles of recursion
  1532. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  1533. Note that we work with two types of hierarchies in the
  1534. present documentation: one Python *class hierarchy*,
  1535. with ``Shape`` as superclass, and one *object hierarchy* of figure elements
  1536. in a specific figure. A subclass of ``Shape`` stores its figure in the
  1537. ``self.shapes`` dictionary. This dictionary represents the object hierarchy
  1538. of figure elements for that class. We want to make one ``draw`` call
  1539. for an instance, say our class ``Vehicle0``, and then we want this call
  1540. to be propagated to *all* objects that are contained in
  1541. ``self.shapes`` and all is nested subdictionaries. How is this done?
  1542. The natural starting point is to call ``draw`` for each ``Shape`` object
  1543. in the ``self.shapes`` dictionary:
  1544. .. code-block:: python
  1545. def draw(self):
  1546. for shape in self.shapes:
  1547. self.shapes[shape].draw()
  1548. This general method can be provided by class ``Shape`` and inherited in
  1549. subclasses like ``Vehicle0``. Let ``v`` be a ``Vehicle0`` instance.
  1550. Seemingly, a call ``v.draw()`` just calls
  1551. .. code-block:: python
  1552. v.shapes['vehicle'].draw()
  1553. v.shapes['ground'].draw()
  1554. However, in the former call we call the ``draw`` method of a ``Composition`` object
  1555. whose ``self.shapes`` attributed has two elements: ``wheels`` and ``body``.
  1556. Since class ``Composition`` inherits the same ``draw`` method, this method will
  1557. run through ``self.shapes`` and call ``wheels.draw()`` and ``body.draw()``.
  1558. Now, the ``wheels`` object is also a ``Composition`` with the same ``draw``
  1559. method, which will run through ``self.shapes``, now containing
  1560. the ``wheel1`` and ``wheel2`` objects. The ``wheel1`` object is a ``Circle``,
  1561. so calling ``wheel1.draw()`` calls the ``draw`` method in class ``Circle``,
  1562. but this is the same ``draw`` method as shown above. This method will
  1563. therefore traverse the circle's ``shapes`` dictionary, which we have seen
  1564. consists of one ``Curve`` element.
  1565. The ``Curve`` object holds the coordinates to be plotted so here ``draw``
  1566. really needs to do something "physical", namely send the coordinates to
  1567. the plotting program. The ``draw`` method is outlined in the short listing
  1568. of class ``Curve`` shown previously.
  1569. We can go to any of the other shape objects that appear in the figure
  1570. hierarchy and follow their ``draw`` calls in the similar way. Every time,
  1571. a ``draw`` call will invoke a new ``draw`` call, until we eventually hit
  1572. a ``Curve`` object at the "bottom" of the figure hierarchy, and then that part
  1573. of the figure is really plotted (or more precisely, the coordinates
  1574. are sent to a plotting program).
  1575. When a method calls itself, such as ``draw`` does, the calls are known as
  1576. *recursive* and the programming principle is referred to as
  1577. *recursion*. This technique is very often used to traverse hierarchical
  1578. structures like the figure structures we work with here. Even though the
  1579. hierarchy of objects building up a figure are of different types, they
  1580. all inherit the same ``draw`` method and therefore exhibit the same
  1581. behavior with respect to drawing. Only the ``Curve`` object has a different
  1582. ``draw`` method, which does not lead to more recursion.
  1583. Explaining recursion
  1584. ~~~~~~~~~~~~~~~~~~~~
  1585. Understanding recursion is usually a challenge. To get a better idea of
  1586. how recursion works, we have equipped class ``Shape`` with a method ``recurse``
  1587. that just visits all the objects in the ``shapes`` dictionary and prints
  1588. out a message for each object.
  1589. This feature allows us to trace the execution and see exactly where
  1590. we are in the hierarchy and which objects that are visited.
  1591. The ``recurse`` method is very similar to ``draw``:
  1592. .. code-block:: python
  1593. def recurse(self, name, indent=0):
  1594. # print message where we are (name is where we come from)
  1595. for shape in self.shapes:
  1596. # print message about which object to visit
  1597. self.shapes[shape].recurse(indent+2, shape)
  1598. The ``indent`` parameter governs how much the message from this
  1599. ``recurse`` method is intended. We increase ``indent`` by 2 for every
  1600. level in the hierarchy, i.e., every row of objects in Figure
  1601. :ref:`sketcher:fig:Vehicle0:hier2`. This indentation makes it easy to
  1602. see on the printout how far down in the hierarchy we are.
  1603. A typical message written by ``recurse`` when ``name`` is ``'body'`` and
  1604. the ``shapes`` dictionary has the keys ``'over'`` and ``'under'``,
  1605. will be
  1606. .. code-block:: text
  1607. Composition: body.shapes has entries 'over', 'under'
  1608. call body.shapes["over"].recurse("over", 6)
  1609. The number of leading blanks on each line corresponds to the value of
  1610. ``indent``. The code printing out such messages looks like
  1611. .. code-block:: python
  1612. def recurse(self, name, indent=0):
  1613. space = ' '*indent
  1614. print space, '%s: %s.shapes has entries' % \
  1615. (self.__class__.__name__, name), \
  1616. str(list(self.shapes.keys()))[1:-1]
  1617. for shape in self.shapes:
  1618. print space,
  1619. print 'call %s.shapes["%s"].recurse("%s", %d)' % \
  1620. (name, shape, shape, indent+2)
  1621. self.shapes[shape].recurse(shape, indent+2)
  1622. Let us follow a ``v.recurse('vehicle')`` call in detail, ``v`` being
  1623. a ``Vehicle0`` instance. Before looking into the output from ``recurse``,
  1624. let us get an overview of the figure hierarchy in the ``v`` object
  1625. (as produced by ``print v``)
  1626. .. code-block:: text
  1627. ground
  1628. wall
  1629. vehicle
  1630. body
  1631. over
  1632. rectangle
  1633. under
  1634. rectangle
  1635. wheels
  1636. wheel1
  1637. arc
  1638. wheel2
  1639. arc
  1640. The ``recurse`` method performs the same kind of traversal of the
  1641. hierarchy, but writes out and explains a lot more.
  1642. The data structure represented by ``v.shapes`` is known as a *tree*.
  1643. As in physical trees, there is a *root*, here the ``v.shapes``
  1644. dictionary. A graphical illustration of the tree (upside down) is
  1645. shown in Figure :ref:`sketcher:fig:Vehicle0:hier2`.
  1646. From the root there are one or more branches, here two:
  1647. ``ground`` and ``vehicle``. Following the ``vehicle`` branch, it has two new
  1648. branches, ``body`` and ``wheels``. Relationships as in family trees
  1649. are often used to describe the relations in object trees too: we say
  1650. that ``vehicle`` is the parent of ``body`` and that ``body`` is a child of
  1651. ``vehicle``. The term *node* is also often used to describe an element
  1652. in a tree. A node may have several other nodes as *descendants*.
  1653. .. _sketcher:fig:Vehicle0:hier2:
  1654. .. figure:: Vehicle0_hier2.png
  1655. :width: 600
  1656. *Hierarchy of figure elements in an instance of class `Vehicle0`*
  1657. Recursion is the principal programming technique to traverse tree structures.
  1658. Any object in the tree can be viewed as a root of a subtree. For
  1659. example, ``wheels`` is the root of a subtree that branches into
  1660. ``wheel1`` and ``wheel2``. So when processing an object in the tree,
  1661. we imagine we process the root and then recurse into a subtree, but the
  1662. first object we recurse into can be viewed as the root of the subtree, so the
  1663. processing procedure of the parent object can be repeated.
  1664. A recommended next step is to simulate the ``recurse`` method by hand and
  1665. carefully check that what happens in the visits to ``recurse`` is
  1666. consistent with the output listed below. Although tedious, this is
  1667. a major exercise that guaranteed will help to demystify recursion.
  1668. A part of the printout of ``v.recurse('vehicle')`` looks like
  1669. .. code-block:: text
  1670. Vehicle0: vehicle.shapes has entries 'ground', 'vehicle'
  1671. call vehicle.shapes["ground"].recurse("ground", 2)
  1672. Wall: ground.shapes has entries 'wall'
  1673. call ground.shapes["wall"].recurse("wall", 4)
  1674. reached "bottom" object Curve
  1675. call vehicle.shapes["vehicle"].recurse("vehicle", 2)
  1676. Composition: vehicle.shapes has entries 'body', 'wheels'
  1677. call vehicle.shapes["body"].recurse("body", 4)
  1678. Composition: body.shapes has entries 'over', 'under'
  1679. call body.shapes["over"].recurse("over", 6)
  1680. Rectangle: over.shapes has entries 'rectangle'
  1681. call over.shapes["rectangle"].recurse("rectangle", 8)
  1682. reached "bottom" object Curve
  1683. call body.shapes["under"].recurse("under", 6)
  1684. Rectangle: under.shapes has entries 'rectangle'
  1685. call under.shapes["rectangle"].recurse("rectangle", 8)
  1686. reached "bottom" object Curve
  1687. ...
  1688. This example should clearly demonstrate the principle that we
  1689. can start at any object in the tree and do a recursive set
  1690. of calls with that object as root.
  1691. .. _sketcher:scaling:
  1692. Scaling, translating, and rotating a figure
  1693. -------------------------------------------
  1694. With recursion, as explained in the previous section, we can within
  1695. minutes equip *all* classes in the ``Shape`` hierarchy, both present and
  1696. future ones, with the ability to scale the figure, translate it,
  1697. or rotate it. This added functionality requires only a few lines
  1698. of code.
  1699. Scaling
  1700. ~~~~~~~
  1701. We start with the simplest of the three geometric transformations,
  1702. namely scaling. For a ``Curve`` instance containing a set of :math:`n`
  1703. coordinates :math:`(x_i,y_i)` that make up a curve, scaling by a factor :math:`a`
  1704. means that we multiply all the :math:`x` and :math:`y` coordinates by :math:`a`:
  1705. .. math::
  1706. x_i \leftarrow ax_i,\quad y_i\leftarrow ay_i,
  1707. \quad i=0,\ldots,n-1\thinspace .
  1708. Here we apply the arrow as an assignment operator.
  1709. The corresponding Python implementation in
  1710. class ``Curve`` reads
  1711. .. code-block:: python
  1712. class Curve:
  1713. ...
  1714. def scale(self, factor):
  1715. self.x = factor*self.x
  1716. self.y = factor*self.y
  1717. Note here that ``self.x`` and ``self.y`` are Numerical Python arrays,
  1718. so that multiplication by a scalar number ``factor`` is
  1719. a vectorized operation.
  1720. An even more efficient implementation is to make use of in-place
  1721. multiplication in the arrays,
  1722. .. code-block:: python
  1723. class Curve:
  1724. ...
  1725. def scale(self, factor):
  1726. self.x *= factor
  1727. self.y *= factor
  1728. as this saves the creation of temporary arrays like ``factor*self.x``.
  1729. In an instance of a subclass of ``Shape``, the meaning of a method
  1730. ``scale`` is to run through all objects in the dictionary ``shapes`` and
  1731. ask each object to scale itself. This is the same delegation of
  1732. actions to subclass instances as we do in the ``draw`` (or ``recurse``)
  1733. method. All objects, except ``Curve`` instances, can share the same
  1734. implementation of the ``scale`` method. Therefore, we place the ``scale``
  1735. method in the superclass ``Shape`` such that all subclasses inherit the
  1736. method. Since ``scale`` and ``draw`` are so similar, we can easily
  1737. implement the ``scale`` method in class ``Shape`` by copying and editing
  1738. the ``draw`` method:
  1739. .. code-block:: python
  1740. class Shape:
  1741. ...
  1742. def scale(self, factor):
  1743. for shape in self.shapes:
  1744. self.shapes[shape].scale(factor)
  1745. This is all we have to do in order to equip all subclasses of
  1746. ``Shape`` with scaling functionality!
  1747. Any piece of the figure will scale itself, in the same manner
  1748. as it can draw itself.
  1749. Translation
  1750. ~~~~~~~~~~~
  1751. A set of coordinates :math:`(x_i, y_i)` can be translated :math:`v_0` units in
  1752. the :math:`x` direction and :math:`v_1` units in the :math:`y` direction using the formulas
  1753. .. math::
  1754. x_i\leftarrow x_i+v_0,\quad y_i\leftarrow y_i+v_1,
  1755. \quad i=0,\ldots,n-1\thinspace .
  1756. The natural specification of the translation is in terms of the
  1757. vector :math:`v=(v_0,v_1)`.
  1758. The corresponding Python implementation in class ``Curve`` becomes
  1759. .. code-block:: python
  1760. class Curve:
  1761. ...
  1762. def translate(self, v):
  1763. self.x += v[0]
  1764. self.y += v[1]
  1765. The translation operation for a shape object is very similar to the
  1766. scaling and drawing operations. This means that we can implement a
  1767. common method ``translate`` in the superclass ``Shape``. The code
  1768. is parallel to the ``scale`` method:
  1769. .. code-block:: python
  1770. class Shape:
  1771. ....
  1772. def translate(self, v):
  1773. for shape in self.shapes:
  1774. self.shapes[shape].translate(v)
  1775. Rotation
  1776. ~~~~~~~~
  1777. Rotating a figure is more complicated than scaling and translating.
  1778. A counter clockwise rotation of :math:`\theta` degrees for a set of
  1779. coordinates :math:`(x_i,y_i)` is given by
  1780. .. math::
  1781. \bar x_i &\leftarrow x_i\cos\theta - y_i\sin\theta,\\
  1782. \bar y_i &\leftarrow x_i\sin\theta + y_i\cos\theta\thinspace .
  1783. This rotation is performed around the origin. If we want the figure
  1784. to be rotated with respect to a general point :math:`(x,y)`, we need to
  1785. extend the formulas above:
  1786. .. math::
  1787. \bar x_i &\leftarrow x + (x_i -x)\cos\theta - (y_i -y)\sin\theta,\\
  1788. \bar y_i &\leftarrow y + (x_i -x)\sin\theta + (y_i -y)\cos\theta\thinspace .
  1789. The Python implementation in class ``Curve``, assuming that :math:`\theta`
  1790. is given in degrees and not in radians, becomes
  1791. .. code-block:: python
  1792. def rotate(self, angle, center):
  1793. angle = radians(angle)
  1794. x, y = center
  1795. c = cos(angle); s = sin(angle)
  1796. xnew = x + (self.x - x)*c - (self.y - y)*s
  1797. ynew = y + (self.x - x)*s + (self.y - y)*c
  1798. self.x = xnew
  1799. self.y = ynew
  1800. The ``rotate`` method in class ``Shape`` follows the principle of the
  1801. ``draw``, ``scale``, and ``translate`` methods.
  1802. We have already seen the ``rotate`` method in action when animating the
  1803. rolling wheel at the end of the section :ref:`sketcher:vehicle1:anim`.