%23%20%2F%2F%2F%20script%0A%23%20requires-python%20%3D%20%22%3E%3D3.11%22%0A%23%20dependencies%20%3D%20%5B%22marimo%3E%3D0.23.9%22%2C%20%22numpy%3E%3D2.0%22%2C%20%22matplotlib%3E%3D3.9%22%2C%20%22urllib3%3E%3D2.2%22%5D%0A%23%20%2F%2F%2F%0A%0Aimport%20marimo%0A%0A__generated_with%20%3D%20%220.25.2%22%0Aapp%20%3D%20marimo.App(width%3D%22medium%22)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20Determination%20of%20a%20coordinate%20system%0A%0A%20%20%20%20%3E%20Marcos%20Duarte%2C%20Renato%20Naville%20Watanabe%3Cbr%3E%0A%20%20%20%20%3E%20%5BLaboratory%20of%20Biomechanics%20and%20Motor%20Control%5D(https%3A%2F%2Fbmclab.pesquisa.ufabc.edu.br%2F)%3Cbr%3E%0A%20%20%20%20%3E%20Federal%20University%20of%20ABC%2C%20Brazil%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20How%20to%20use%20this%20guide%0A%0A%20%20%20%20Put%20three%20markers%20on%20a%20body%20segment%2C%20record%20them%20with%20a%20motion%20capture%20system%2C%20and%20you%20get%20nine%20numbers%20per%20frame.%20Nine%20numbers%20are%20not%20an%20orientation.%20To%20say%20which%20way%20the%20thigh%20is%20pointing%2C%20or%20how%20far%20the%20shank%20has%20turned%2C%20you%20need%20a%20coordinate%20system%20attached%20to%20the%20segment%3A%20an%20origin%20and%20three%20perpendicular%20unit%20vectors%20that%20move%20with%20it.%0A%0A%20%20%20%20This%20notebook%20builds%20that%20coordinate%20system%20from%20three%20markers%2C%20and%20then%20asks%20the%20questions%20that%20decide%20whether%20you%20can%20trust%20it.%20Is%20it%20really%20orthonormal%2C%20and%20right-handed%3F%20Does%20it%20move%20with%20the%20segment%2C%20and%20only%20with%20the%20segment%3F%20And%20when%20every%20marker%20is%20off%20by%20a%20millimetre%2C%20as%20real%20markers%20are%2C%20how%20far%20off%20are%20its%20axes%3F%0A%0A%20%20%20%20Read%20it%20in%20order%20and%20run%20each%20cell%20as%20you%20reach%20it.%20Where%20you%20find%20a%20**Challenge**%20or%20a%20set%20of%20**Guiding%20questions**%2C%20stop%20and%20answer%20on%20a%20scratchpad%20before%20moving%20on.%20Several%20of%20them%20ask%20you%20to%20predict%20a%20number%20*before*%20the%20code%20prints%20it.%20Make%20the%20prediction%3B%20a%20wrong%20guess%20that%20the%20code%20then%20corrects%20teaches%20more%20than%20a%20right%20one.%0A%0A%20%20%20%20The%20**Exercises**%20come%20with%20solutions%2C%20folded%20away%20under%20each%20exercise.%20Open%20a%20solution%20only%20after%20you%20have%20an%20answer%20of%20your%20own%3B%20the%20solutions%20of%20the%20computational%20exercises%20are%20produced%20by%20cells%20whose%20code%20you%20can%20reveal%20and%20run.%0A%0A%20%20%20%20You%20will%20need%20the%20vector%20operations%20from%20%5BScalar%20and%20vector%5D(https%3A%2F%2Fmolab.marimo.io%2Fgithub%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fnotebooks%2Fscalar_vector.py)%3A%20the%20norm%2C%20the%20dot%20product%20and%20the%20cross%20product.%20For%20the%20wider%20picture%20(what%20frames%20of%20reference%20are%2C%20why%20inertial%20frames%20matter%2C%20and%20how%20anatomical%20conventions%20fix%20the%20direction%20of%20each%20axis)%2C%20see%20%5BFrame%20of%20reference%5D(https%3A%2F%2Fmolab.marimo.io%2Fgithub%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fnotebooks%2Freference_frame.py).%0A%0A%20%20%20%20**Challenge%200.**%20Imagine%20that%20you%20have%20three%20markers%20to%20put%20on%20your%20own%20forearm.%20Before%20reading%20on%2C%20decide%20where%20you%20would%20put%20them%20and%20write%20down%20why.%20Keep%20your%20answer%20nearby%3B%20the%20checkpoint%20questions%20ask%20you%20to%20come%20back%20to%20it.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Three%20markers%20on%20a%20shank%0A%0A%20%20%20%20A%20rigid%20plastic%20plate%20carrying%20three%20markers%20is%20strapped%20to%20the%20side%20of%20a%20runner's%20shank.%20This%20arrangement%2C%20called%20a%20*marker%20cluster*%2C%20is%20one%20of%20the%20most%20common%20in%20three-dimensional%20motion%20analysis.%20In%20every%20frame%20of%20the%20recording%2C%20the%20motion%20capture%20system%20reports%20nine%20numbers%3A%20the%20X%2C%20Y%20and%20Z%20coordinates%20of%20each%20marker%20in%20the%20laboratory.%0A%0A%20%20%20%20None%20of%20those%20nine%20numbers%20is%20the%20orientation%20of%20the%20shank.%20Every%20one%20of%20them%20changes%20when%20the%20shank%20turns%2C%20but%20also%20when%20the%20runner%20simply%20moves%20forward%2C%20and%20no%20single%20one%20of%20them%20says%20which%20way%20the%20shank%20is%20pointing.%20To%20get%20the%20orientation%2C%20we%20have%20to%20turn%20the%20three%20positions%20into%20a%20coordinate%20system%20that%20belongs%20to%20the%20shank.%0A%0A%20%20%20%20**Guiding%20questions%200.**%0A%0A%20%20%20%201.%20Why%20three%20markers%3F%20Would%20two%20be%20enough%20to%20know%20the%20orientation%20of%20the%20shank%3F%20Which%20rotation%20of%20the%20shank%20would%20two%20markers%20miss%3F%0A%20%20%20%202.%20Why%20must%20the%20three%20markers%20not%20lie%20on%20a%20straight%20line%3F%0A%20%20%20%203.%20If%20you%20could%20make%20the%20plate%20any%20size%20you%20liked%2C%20would%20you%20make%20it%20small%20or%20large%3F%20What%20stops%20you%20from%20making%20it%20as%20large%20as%20possible%3F%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Python%20setup%0A%0A%20%20%20%20NumPy%20does%20the%20numerical%20work%20and%20Matplotlib%20draws%20the%20figures.%20The%20function%20%60CCS%60%2C%20from%20the%20%60functions%60%20folder%20of%20this%20repository%2C%20draws%20coordinate%20systems%20in%20three%20dimensions%3B%20it%20is%20loaded%20where%20it%20is%20first%20needed.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20matplotlib%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%0A%20%20%20%20matplotlib.rc(%22axes%22%2C%20labelsize%3D13%2C%20titlesize%3D14)%0A%20%20%20%20matplotlib.rc(%22xtick%22%2C%20labelsize%3D11)%0A%20%20%20%20matplotlib.rc(%22ytick%22%2C%20labelsize%3D11)%0A%20%20%20%20matplotlib.rc(%22legend%22%2C%20fontsize%3D11)%0A%20%20%20%20return%20np%2C%20plt%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Frames%20of%20reference%20and%20coordinate%20systems%0A%0A%20%20%20%20In%20mechanics%2C%20a%20%5Bframe%20of%20reference%5D(https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FFrame_of_reference)%20is%20what%20we%20choose%20to%20describe%20the%20motion%20of%20a%20body%20with%20respect%20to.%20In%20this%20frame%20of%20reference%20we%20define%20a%20%5Bcoordinate%20system%5D(https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FCoordinate_system)%2C%20a%20set%20of%20axes%20along%20which%20we%20measure%20the%20motion%20of%20the%20body%20(although%20frame%20of%20reference%20and%20coordinate%20system%20are%20often%20used%20interchangeably).%0A%0A%20%20%20%20In%20biomechanics%2C%20we%20use%20different%20coordinate%20systems%20for%20convenience%2C%20and%20call%20them%20global%2C%20laboratory%2C%20local%2C%20anatomical%20or%20technical%20reference%20frames%20or%20coordinate%20systems.%20For%20example%2C%20in%20a%20standard%20gait%20analysis%20we%20define%20a%20global%20or%20laboratory%20coordinate%20system%2C%20and%20a%20different%20coordinate%20system%20for%20each%20segment%20of%20the%20body%2C%20so%20that%20the%20motion%20of%20a%20segment%20can%20be%20described%20in%20relation%20to%20the%20anatomical%20axes%20of%20another%20segment.%20To%20define%20this%20anatomical%20coordinate%20system%2C%20we%20place%20markers%20on%20anatomical%20landmarks%20of%20each%20segment.%20We%20may%20also%20use%20other%20markers%20on%20the%20segment%2C%20technical%20markers%2C%20to%20improve%20the%20motion%20analysis%2C%20and%20then%20we%20also%20have%20to%20define%20a%20technical%20coordinate%20system%20for%20each%20segment.%0A%0A%20%20%20%20As%20we%20perceive%20the%20surrounding%20space%20as%20three-dimensional%2C%20a%20convenient%20coordinate%20system%20to%20use%20is%20the%20%5BCartesian%20coordinate%20system%5D(https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FCartesian_coordinate_system)%2C%20with%20three%20orthogonal%20axes%20in%20%5BEuclidean%20space%5D(https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FEuclidean_space).%20From%20%5Blinear%20algebra%5D(https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FLinear_algebra)%2C%20a%20set%20of%20linearly%20independent%20unit%20vectors%20that%20can%20represent%20any%20vector%20via%20%5Blinear%20combination%5D(https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FLinear_combination)%20is%20called%20a%20%3Ca%20href%3D%22https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FBasis_(linear_algebra)%22%3Ebasis%3C%2Fa%3E%3B%20when%20the%20vectors%20are%20also%20mutually%20orthogonal%2C%20it%20is%20an%20orthonormal%20basis.%20The%20figure%20below%20shows%20a%20point%2C%20its%20position%20vector%20in%20the%20Cartesian%20coordinate%20system%2C%20and%20the%20versors%20(unit%20vectors)%20of%20the%20basis%20of%20this%20coordinate%20system.%0A%0A%20%20%20%20%3Cfigure%20aria-describedby%3D%22figure-caption-1%22%3E%3Ccenter%3E%3Cimg%20src%3D%22https%3A%2F%2Fgithub.com%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fimages%2Fvector3Dijk.png%3Fraw%3D1%22%20width%3D350%20alt%3D%22versors%20of%20the%20Cartesian%20basis%22%2F%3E%3C%2Fcenter%3E%3C%2Ffigure%3E%0A%0A%20%20%20%20%3Cspan%20id%3D%22figure-caption-1%22%20class%3D%22figure-caption%22%20style%3D%22display%3Ablock%3Btext-align%3Acenter%3Bfont-style%3Aitalic%22%3EFigure.%20Representation%20of%20a%20point%20%24%5Cmathbf%7BP%7D%24%20and%20its%20position%20vector%20%24%5Coverrightarrow%7B%5Cmathbf%7Ba%7D%7D%24%20in%20a%20Cartesian%20coordinate%20system.%20The%20versors%20%24%5Chat%7B%5Cmathbf%7Bi%7D%7D%2C%5C%2C%20%5Chat%7B%5Cmathbf%7Bj%7D%7D%2C%5C%2C%20%5Chat%7B%5Cmathbf%7Bk%7D%7D%24%20form%20a%20basis%20for%20this%20coordinate%20system%20and%20are%20usually%20drawn%20in%20the%20colour%20sequence%20RGB%20(red%2C%20green%2C%20blue)%20for%20easier%20visualization.%3C%2Fspan%3E%0A%0A%20%20%20%20In%20the%20Cartesian%20coordinate%20system%20itself%2C%20the%20versors%20of%20this%20basis%20have%20the%20coordinates%0A%0A%20%20%20%20%24%24%0A%20%20%20%20%5Chat%7B%5Cmathbf%7Bi%7D%7D%20%3D%20%5Cbegin%7Bbmatrix%7D1%5C%5C0%5C%5C0%20%5Cend%7Bbmatrix%7D%2C%20%5Cquad%20%5Chat%7B%5Cmathbf%7Bj%7D%7D%20%3D%20%5Cbegin%7Bbmatrix%7D0%5C%5C1%5C%5C0%20%5Cend%7Bbmatrix%7D%2C%20%5Cquad%20%5Chat%7B%5Cmathbf%7Bk%7D%7D%20%3D%20%5Cbegin%7Bbmatrix%7D%200%20%5C%5C%200%20%5C%5C%201%20%5Cend%7Bbmatrix%7D%0A%20%20%20%20%24%24%0A%0A%20%20%20%20and%2C%20in%20the%20notation%20of%20the%20figure%2C%20the%20position%20vector%20is%0A%0A%20%20%20%20%24%24%0A%20%20%20%20%5Coverrightarrow%7B%5Cmathbf%7Ba%7D%7D%20%3D%20a_x%5Chat%7B%5Cmathbf%7Bi%7D%7D%20%2B%20a_y%5Chat%7B%5Cmathbf%7Bj%7D%7D%20%2B%20a_z%5Chat%7B%5Cmathbf%7Bk%7D%7D%0A%20%20%20%20%24%24%0A%0A%20%20%20%20Every%20marker%20position%20a%20motion%20capture%20system%20reports%20is%20a%20vector%20like%20this%20one%2C%20written%20in%20the%20laboratory's%20basis.%20What%20we%20want%20is%20a%20second%20basis%2C%20attached%20to%20the%20segment.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Definition%20of%20a%20basis%0A%0A%20%20%20%20The%20mathematical%20problem%20of%20determining%20a%20coordinate%20system%20is%20to%20find%20a%20basis%20and%20an%20origin%20for%20it%3B%20a%20basis%20on%20its%20own%20is%20only%20a%20set%20of%20directions%2C%20with%20no%20origin.%20In%20three-dimensional%20space%2C%20given%20the%20coordinates%20of%20three%20noncollinear%20points%20(points%20that%20do%20not%20all%20lie%20on%20the%20same%20line)%2C%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1%2C%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_2%2C%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%24%2C%20which%20would%20be%20the%20positions%20of%20markers%20captured%20in%20a%20motion%20analysis%20session%2C%20a%20basis%20can%20be%20found%20in%20four%20steps%3A%0A%0A%20%20%20%201.%20First%20axis%2C%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%24%3A%20the%20vector%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_2-%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1%24%20(or%20any%20other%20difference%20between%20two%20of%20the%20points).%0A%20%20%20%202.%20Second%20axis%2C%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_2%24%3A%20the%20cross%20product%20between%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%24%20and%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3-%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1%24%20(or%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3-%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_2%24).%0A%20%20%20%203.%20Third%20axis%2C%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_3%24%3A%20the%20cross%20product%20between%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%24%20and%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_2%24.%0A%20%20%20%204.%20Make%20every%20axis%20a%20versor%20by%20dividing%20it%20by%20its%20norm.%0A%0A%20%20%20%20Each%20step%20uses%20the%20cross%20product%20for%20what%20it%20is%20good%20at%3A%20producing%20a%20vector%20perpendicular%20to%20two%20others.%20The%20second%20axis%20is%20perpendicular%20to%20the%20plane%20of%20the%20three%20points%2C%20and%20the%20third%20lies%20in%20that%20plane%2C%20perpendicular%20to%20the%20first.%0A%0A%20%20%20%20The%20positions%20of%20the%20points%20used%20to%20construct%20a%20coordinate%20system%20have%2C%20by%20definition%2C%20to%20be%20specified%20in%20relation%20to%20an%20already%20existing%20coordinate%20system.%20In%20motion%20analysis%2C%20this%20is%20the%20coordinate%20system%20of%20the%20motion%20capture%20system%2C%20established%20in%20the%20calibration%20phase.%20In%20that%20phase%2C%20the%20positions%20of%20markers%20placed%20on%20an%20object%20with%20perpendicular%20axes%20and%20known%20distances%20between%20the%20markers%20are%20captured%20and%20used%20as%20the%20reference%20(laboratory)%20coordinate%20system.%0A%0A%20%20%20%20For%20example%2C%20given%20the%20positions%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1%20%3D%20%5B1%2C2%2C0%5D%2C%5C%3B%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_2%20%3D%20%5B0%2C1%2C3%5D%2C%5C%3B%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%20%3D%20%5B1%2C0%2C1%5D%24%2C%20a%20basis%20can%20be%20found%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(np)%3A%0A%20%20%20%20m1%20%3D%20np.array(%5B1%2C%202%2C%200%5D)%0A%20%20%20%20m2%20%3D%20np.array(%5B0%2C%201%2C%203%5D)%0A%20%20%20%20m3%20%3D%20np.array(%5B1%2C%200%2C%201%5D)%0A%0A%20%20%20%20v1%20%3D%20m2%20-%20m1%20%20%23%20first%20axis%0A%20%20%20%20v2%20%3D%20np.cross(v1%2C%20m3%20-%20m1)%20%20%23%20second%20axis%0A%20%20%20%20v3%20%3D%20np.cross(v1%2C%20v2)%20%20%23%20third%20axis%0A%0A%20%20%20%20%23%20Vector%20normalization%0A%20%20%20%20e1%20%3D%20v1%20%2F%20np.linalg.norm(v1)%0A%20%20%20%20e2%20%3D%20v2%20%2F%20np.linalg.norm(v2)%0A%20%20%20%20e3%20%3D%20v3%20%2F%20np.linalg.norm(v3)%0A%0A%20%20%20%20print(%22Versors%3A%22%2C%20%22%5Cne1%20%3D%22%2C%20e1%2C%20%22%5Cne2%20%3D%22%2C%20e2%2C%20%22%5Cne3%20%3D%22%2C%20e3)%0A%20%20%20%20return%20e1%2C%20e2%2C%20e3%2C%20m1%2C%20m2%2C%20m3%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20**Exercise%201.**%20Without%20a%20computer%3A%20three%20markers%20are%20at%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1%20%3D%20%5B0%2C%200%2C%200%5D%24%2C%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_2%20%3D%20%5B2%2C%200%2C%200%5D%24%20and%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%20%3D%20%5B0%2C%203%2C%200%5D%24%20(in%20cm).%20Find%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_1%24%2C%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%24%20and%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_3%24%20with%20the%20four%20steps%20above.%20Does%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_3%24%20point%20towards%20the%20third%20marker%3F%0A%0A%20%20%20%20%2F%2F%2F%20details%20%7C%20Solution%20to%20Exercise%201%0A%0A%20%20%20%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%20%3D%20%5B2%2C%200%2C%200%5D%24%2C%20so%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_1%20%3D%20%5B1%2C%200%2C%200%5D%24.%20Then%0A%0A%20%20%20%20%24%24%0A%20%20%20%20%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_2%20%3D%20%5B2%2C%200%2C%200%5D%20%5Ctimes%20%5B0%2C%203%2C%200%5D%20%3D%20%5B0%2C%200%2C%206%5D%2C%20%5Cqquad%0A%20%20%20%20%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_3%20%3D%20%5B2%2C%200%2C%200%5D%20%5Ctimes%20%5B0%2C%200%2C%206%5D%20%3D%20%5B0%2C%20-12%2C%200%5D%0A%20%20%20%20%24%24%0A%0A%20%20%20%20so%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%20%3D%20%5B0%2C%200%2C%201%5D%24%20and%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_3%20%3D%20%5B0%2C%20-1%2C%200%5D%24.%0A%0A%20%20%20%20No%3A%20the%20third%20marker%20is%20on%20the%20positive%20%24y%24%20side%20of%20the%20first%20axis%2C%20and%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_3%24%20points%20to%20the%20negative%20side.%20That%20always%20happens%20with%20this%20recipe.%20With%20%24%5Coverrightarrow%7B%5Cmathbf%7Bw%7D%7D%20%3D%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%20-%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1%24%2C%20the%20vector%20triple%20product%20gives%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%20%5Ctimes%20(%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%20%5Ctimes%20%5Coverrightarrow%7B%5Cmathbf%7Bw%7D%7D)%20%3D%20-%5CVert%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%5CVert%5E2%5C%2C%5Coverrightarrow%7B%5Cmathbf%7Bw%7D%7D%5E%5Cbot%24%2C%20where%20%24%5Coverrightarrow%7B%5Cmathbf%7Bw%7D%7D%5E%5Cbot%24%20is%20the%20part%20of%20%24%5Coverrightarrow%7B%5Cmathbf%7Bw%7D%7D%24%20perpendicular%20to%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%24%3A%20the%20third%20axis%20points%20*away*%20from%20the%20third%20marker.%20If%20you%20want%20it%20to%20point%20towards%20the%20marker%2C%20compute%20the%20second%20axis%20as%20%24%5Coverrightarrow%7B%5Cmathbf%7Bw%7D%7D%20%5Ctimes%20%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%24%20instead.%20Then%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%20%3D%20%5B0%2C%200%2C%20-1%5D%24%20and%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_3%20%3D%20%5B0%2C%201%2C%200%5D%24%2C%20and%20the%20basis%20is%20still%20right-handed.%0A%0A%20%20%20%20%2F%2F%2F%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Checking%20the%20basis%0A%0A%20%20%20%20A%20coordinate%20system%20you%20have%20not%20tested%20is%20a%20guess.%20An%20orthonormal%2C%20right-handed%20basis%20has%20three%20properties%2C%20so%20there%20are%20three%20things%20to%20check%3A%0A%0A%20%20%20%20-%20**Every%20versor%20has%20norm%201.**%0A%20%20%20%20-%20**Every%20pair%20of%20versors%20is%20perpendicular.**%20Test%20it%20with%20the%20dot%20product%2C%20which%20should%20be%20zero.%20Testing%20it%20with%20the%20cross%20product%2C%20whose%20norm%20should%20be%20one%2C%20also%20works%20in%20principle%2C%20but%20for%20two%20versors%20%24%5CVert%5Chat%7B%5Cmathbf%7Ba%7D%7D%5Ctimes%5Chat%7B%5Cmathbf%7Bb%7D%7D%5CVert%20%3D%20%5Csin%5Ctheta%24%2C%20and%20the%20sine%20is%20flat%20near%2090%C2%B0%3A%20two%20versors%2089%C2%B0%20apart%20give%20a%20cross%20product%20of%20norm%200.99985%2C%20while%20their%20dot%20product%2C%20%24%5Ccos%5Ctheta%20%3D%200.017%24%2C%20gives%20the%20error%20away%20at%20once.%20%5BFrame%20of%20reference%5D(https%3A%2F%2Fmolab.marimo.io%2Fgithub%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fnotebooks%2Freference_frame.py)%20works%20through%20this%20example.%0A%20%20%20%20-%20**The%20basis%20is%20right-handed**%3A%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_1%20%5Ctimes%20%5Chat%7B%5Cmathbf%7Be%7D%7D_2%20%3D%20%5Chat%7B%5Cmathbf%7Be%7D%7D_3%24.%20Equivalently%2C%20the%20determinant%20of%20the%20matrix%20with%20the%20versors%20as%20its%20rows%20is%20%24%2B1%24%3B%20it%20is%20%24-1%24%20for%20a%20left-handed%20basis%2C%20which%20passes%20the%20first%20two%20tests%20just%20as%20well.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(e1%2C%20e2%2C%20e3%2C%20np)%3A%0A%20%20%20%20print(%0A%20%20%20%20%20%20%20%20%22Norm%20of%20each%20versor%3A%22%2C%0A%20%20%20%20%20%20%20%20%22%5Cn%7C%7Ce1%7C%7C%20%3D%22%2C%20np.linalg.norm(e1)%2C%0A%20%20%20%20%20%20%20%20%22%5Cn%7C%7Ce2%7C%7C%20%3D%22%2C%20np.linalg.norm(e2)%2C%0A%20%20%20%20%20%20%20%20%22%5Cn%7C%7Ce3%7C%7C%20%3D%22%2C%20np.linalg.norm(e3)%2C%0A%20%20%20%20)%0A%20%20%20%20print(%0A%20%20%20%20%20%20%20%20%22%5CnTest%20of%20orthogonality%20(dot%20product%20between%20versors)%3A%22%2C%0A%20%20%20%20%20%20%20%20f%22%5Cne1%20.%20e2%3A%20%7Bnp.dot(e1%2C%20e2)%3A.1e%7D%22%2C%0A%20%20%20%20%20%20%20%20f%22%5Cne1%20.%20e3%3A%20%7Bnp.dot(e1%2C%20e3)%3A.1e%7D%22%2C%0A%20%20%20%20%20%20%20%20f%22%5Cne2%20.%20e3%3A%20%7Bnp.dot(e2%2C%20e3)%3A.1e%7D%22%2C%0A%20%20%20%20)%0A%20%20%20%20print(%22%5CnTest%20of%20handedness%3A%22)%0A%20%20%20%20print(%22e1%20x%20e2%20-%20e3%20%3D%22%2C%20np.cross(e1%2C%20e2)%20-%20e3)%0A%20%20%20%20print(f%22det(%5Be1%2C%20e2%2C%20e3%5D)%20%3D%20%7Bnp.linalg.det(np.vstack((e1%2C%20e2%2C%20e3)))%3Ag%7D%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20norms%20are%201%20and%20the%20dot%20products%20are%20zero%2C%20to%20within%20about%20%2410%5E%7B-16%7D%24%3A%20that%20is%20floating-point%20rounding%2C%20not%20a%20flaw%20in%20the%20method.%20And%20the%20basis%20is%20right-handed%2C%20as%20the%20construction%20guarantees%2C%20because%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_3%20%3D%20%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%20%5Ctimes%20%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_2%24%20is%20by%20definition%20the%20third%20axis%20of%20a%20right-handed%20set.%0A%0A%20%20%20%20**Exercise%202.**%20The%20order%20of%20the%20markers%20matters.%20If%20you%20give%20the%20recipe%20the%20markers%20in%20the%20order%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_2%2C%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1%2C%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%24%20instead%20of%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1%2C%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_2%2C%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%24%2C%20what%20happens%20to%20each%20of%20the%20three%20versors%3F%20Is%20the%20new%20basis%20still%20right-handed%3F%20Work%20it%20out%20from%20the%20properties%20of%20the%20cross%20product%20before%20checking%20with%20the%20code.%0A%0A%20%20%20%20%2F%2F%2F%20details%20%7C%20Solution%20to%20Exercise%202%0A%0A%20%20%20%20The%20first%20axis%20becomes%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1%20-%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_2%20%3D%20-%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%24.%20For%20the%20second%2C%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%20-%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_2%20%3D%20(%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%20-%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1)%20-%20%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%24%2C%20and%20the%20cross%20product%20of%20a%20vector%20with%20itself%20is%20zero%2C%20so%0A%0A%20%20%20%20%24%24%0A%20%20%20%20(-%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1)%20%5Ctimes%20%5Cleft%5B(%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%20-%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1)%20-%20%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%5Cright%5D%20%3D%20-%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%20%5Ctimes%20(%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%20-%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1)%20%3D%20-%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_2%0A%20%20%20%20%24%24%0A%0A%20%20%20%20and%20the%20third%20is%20%24(-%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1)%20%5Ctimes%20(-%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_2)%20%3D%20%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_3%24.%20So%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_1%24%20and%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%24%20reverse%20and%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_3%24%20does%20not%20change%3A%20the%20coordinate%20system%20has%20turned%20by%20180%C2%B0%20about%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_3%24.%20Two%20reversed%20axes%20keep%20the%20determinant%20at%20%24%2B1%24%2C%20so%20the%20basis%20is%20still%20right-handed%2C%20and%20no%20test%20of%20the%20basis%20can%20tell%20that%20anything%20happened.%20With%20the%20markers%20of%20this%20notebook%20in%20the%20order%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_2%2C%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1%2C%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%24%2C%20the%20recipe%20gives%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_1%20%3D%20%5B0.3015%2C%200.3015%2C%20-0.9045%5D%24%20and%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%20%3D%20%5B-0.9129%2C%20-0.1826%2C%20-0.3651%5D%24%2C%20while%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_3%24%20stays%20%24%5B-0.2752%2C%200.9358%2C%200.2202%5D%24.%0A%0A%20%20%20%20%2F%2F%2F%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20The%20origin%0A%0A%20%20%20%20To%20define%20a%20coordinate%20system%20using%20the%20calculated%20basis%2C%20we%20also%20need%20to%20define%20an%20origin.%20In%20principle%2C%20any%20point%20could%20be%20the%20origin%2C%20but%20if%20the%20coordinate%20system%20should%20follow%20anatomical%20conventions%2C%20for%20example%20with%20its%20origin%20at%20a%20joint%20centre%2C%20the%20basis%20and%20the%20origin%20have%20to%20be%20calculated%20according%20to%20the%20standards%20used%20in%20motion%20analysis%20(see%20%5BFrame%20of%20reference%5D(https%3A%2F%2Fmolab.marimo.io%2Fgithub%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fnotebooks%2Freference_frame.py)).%0A%0A%20%20%20%20If%20the%20coordinate%20system%20is%20a%20technical%20one%20and%20not%20anatomically%20based%2C%20a%20common%20procedure%20in%20motion%20analysis%20is%20to%20place%20the%20origin%20at%20the%20centroid%20(average%20position)%20of%20the%20markers.%20Averaging%20the%20positions%20of%20several%20markers%20potentially%20reduces%20the%20effect%20of%20noise%2C%20for%20example%20from%20soft%20tissue%20artifacts%2C%20on%20the%20calculation.%0A%0A%20%20%20%20For%20the%20markers%20in%20the%20example%20above%2C%20the%20origin%20of%20the%20coordinate%20system%20is%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(m1%2C%20m2%2C%20m3%2C%20np)%3A%0A%20%20%20%20origin%20%3D%20np.mean((m1%2C%20m2%2C%20m3)%2C%20axis%3D0)%0A%0A%20%20%20%20print(%22Origin%3A%22%2C%20origin)%0A%20%20%20%20return%20(origin%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Visualization%0A%0A%20%20%20%20Let's%20plot%20the%20coordinate%20system%20with%20the%20function%20%60CCS%60%2C%20from%20the%20file%20%60CCS.py%60%20in%20the%20%60functions%60%20folder%20of%20this%20repository.%20The%20first%20cell%20adds%20that%20folder%20to%20Python's%20search%20path.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_()%3A%0A%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20from%20notebook_resources%20import%20repo_file%0A%20%20%20%20except%20ModuleNotFoundError%20as%20_error%3A%0A%20%20%20%20%20%20%20%20if%20_error.name%20!%3D%20%22notebook_resources%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%0A%20%20%20%20%20%20%20%20raise%20ModuleNotFoundError(%0A%20%20%20%20%20%20%20%20%20%20%20%20%22This%20notebook%20needs%20notebook_resources.py.%20Download%20it%20from%3A%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%22https%3A%2F%2Fraw.githubusercontent.com%2FBMClab%2FBMC%2Fmaster%2Ffunctions%2Fnotebook_resources.py%5Cn%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Save%20it%20in%20the%20same%20folder%20as%20this%20notebook%2C%20then%20rerun%20this%20cell.%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22notebook_resources%22%2C%0A%20%20%20%20%20%20%20%20)%20from%20None%0A%20%20%20%20return%20(repo_file%2C)%0A%0A%0A%40app.cell%0Adef%20_(repo_file)%3A%0A%20%20%20%20import%20sys%0A%0A%20%20%20%20sys.path.insert(0%2C%20str(repo_file(%22functions%2FCCS.py%22).parent))%20%20%23%20add%20to%20pythonpath%0A%20%20%20%20from%20CCS%20import%20CCS%0A%0A%20%20%20%20return%20(CCS%2C)%0A%0A%0A%40app.cell%0Adef%20_(e1%2C%20e2%2C%20e3%2C%20m1%2C%20m2%2C%20m3%2C%20np)%3A%0A%20%20%20%20markers%20%3D%20np.vstack((m1%2C%20m2%2C%20m3))%0A%20%20%20%20basis%20%3D%20np.vstack((e1%2C%20e2%2C%20e3))%0A%20%20%20%20return%20basis%2C%20markers%0A%0A%0A%40app.cell%0Adef%20_(CCS%2C%20basis%2C%20markers%2C%20origin)%3A%0A%20%20%20%20_ccs%20%3D%20CCS(xyz%3D%5B%5D%2C%20Oijk%3Dorigin%2C%20ijk%3Dbasis%2C%20point%3Dmarkers%2C%20vector%3DFalse)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20black%20arrows%20are%20the%20axes%20of%20the%20laboratory%2C%20the%20yellow%20dots%20are%20the%20three%20markers%2C%20and%20the%20red%2C%20green%20and%20blue%20arrows%2C%20which%20%60CCS%60%20labels%20i%2C%20j%20and%20k%2C%20are%20the%20versors%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_1%2C%20%5Chat%7B%5Cmathbf%7Be%7D%7D_2%2C%20%5Chat%7B%5Cmathbf%7Be%7D%7D_3%24%2C%20drawn%20from%20the%20centroid.%20The%20red%20versor%20points%20from%20the%20first%20marker%20towards%20the%20second%2C%20and%20the%20green%20one%20is%20perpendicular%20to%20the%20triangle%20the%20markers%20form.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20A%20coordinate%20system%20that%20travels%20with%20the%20segment%0A%0A%20%20%20%20Why%20go%20to%20all%20this%20trouble%3F%20Because%20the%20origin%20and%20the%20basis%20are%20built%20from%20the%20markers%20alone%2C%20and%20the%20markers%20are%20fixed%20to%20the%20segment%2C%20so%20the%20coordinate%20system%20goes%20wherever%20the%20segment%20goes.%0A%0A%20%20%20%20The%20way%20to%20see%20this%20is%20to%20describe%20each%20marker%20in%20the%20segment's%20own%20coordinate%20system.%20Subtract%20the%20origin%20from%20the%20marker's%20position%2C%20and%20project%20what%20is%20left%20on%20each%20versor%20with%20the%20dot%20product.%20That%20gives%20the%20marker's%20**local%20coordinates**%3A%0A%0A%20%20%20%20%24%24%0A%20%20%20%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_%7Blocal%7D%20%3D%20%5CBig%5B%5C%2C%20(%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D%20-%20%5Coverrightarrow%7B%5Cmathbf%7BO%7D%7D)%5Ccdot%5Chat%7B%5Cmathbf%7Be%7D%7D_1%2C%5C%3B%5C%3B%20(%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D%20-%20%5Coverrightarrow%7B%5Cmathbf%7BO%7D%7D)%5Ccdot%5Chat%7B%5Cmathbf%7Be%7D%7D_2%2C%5C%3B%5C%3B%20(%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D%20-%20%5Coverrightarrow%7B%5Cmathbf%7BO%7D%7D)%5Ccdot%5Chat%7B%5Cmathbf%7Be%7D%7D_3%20%5C%2C%5CBig%5D%0A%20%20%20%20%24%24%0A%0A%20%20%20%20Let's%20collect%20the%20recipe%20in%20two%20functions.%20The%20first%20returns%20the%20origin%20and%20the%20basis%2C%20with%20the%20versors%20as%20the%20rows%20of%20a%20matrix.%20It%20works%20for%20one%20frame%20or%2C%20as%20we%20will%20need%20later%2C%20for%20many%20frames%20at%20once%2C%20with%20one%20marker%20position%20per%20row.%20The%20second%20computes%20local%20coordinates.%20A%20third%20function%2C%20for%20comparing%20directions%2C%20measures%20the%20angle%20between%20two%20vectors%20from%20the%20dot%20product.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.function%0Adef%20segment_frame(m1%2C%20m2%2C%20m3)%3A%0A%20%20%20%20%22%22%22Origin%20and%20basis%20of%20the%20coordinate%20system%20defined%20by%20three%20markers.%0A%0A%20%20%20%20Each%20marker%20is%20a%20position%20%5Bx%2C%20y%2C%20z%5D%20or%20an%20array%20of%20positions%2C%20one%20frame%20per%0A%20%20%20%20row%20(the%20two%20can%20be%20mixed).%20Returns%20the%20origin%20(the%20centroid%20of%20the%20markers)%0A%20%20%20%20and%20the%20basis%2C%20with%20the%20versors%20e1%2C%20e2%2C%20e3%20as%20the%20rows%20of%20the%20last%20two%0A%20%20%20%20dimensions.%0A%20%20%20%20%22%22%22%0A%20%20%20%20import%20numpy%20as%20np%0A%0A%20%20%20%20m1%2C%20m2%2C%20m3%20%3D%20np.broadcast_arrays(*(np.asarray(m%2C%20dtype%3Dfloat)%20for%20m%20in%20(m1%2C%20m2%2C%20m3)))%0A%20%20%20%20v1%20%3D%20m2%20-%20m1%0A%20%20%20%20v2%20%3D%20np.cross(v1%2C%20m3%20-%20m1)%0A%20%20%20%20v3%20%3D%20np.cross(v1%2C%20v2)%0A%20%20%20%20basis%20%3D%20np.stack(%5Bv%20%2F%20np.linalg.norm(v%2C%20axis%3D-1%2C%20keepdims%3DTrue)%20for%20v%20in%20(v1%2C%20v2%2C%20v3)%5D%2C%20axis%3D-2)%0A%20%20%20%20origin%20%3D%20(m1%20%2B%20m2%20%2B%20m3)%20%2F%203%0A%20%20%20%20return%20origin%2C%20basis%0A%0A%0A%40app.function%0Adef%20local_coordinates(points%2C%20origin%2C%20basis)%3A%0A%20%20%20%20%22%22%22Coordinates%20of%20%60points%60%20(one%20per%20row)%20in%20the%20coordinate%20system%20with%20the%0A%20%20%20%20given%20%60origin%60%20and%20%60basis%60%20(versors%20as%20rows).%22%22%22%0A%20%20%20%20import%20numpy%20as%20np%0A%0A%20%20%20%20return%20(np.asarray(points%2C%20dtype%3Dfloat)%20-%20origin)%20%40%20np.asarray(basis).T%0A%0A%0A%40app.function%0Adef%20angle_between(u%2C%20w)%3A%0A%20%20%20%20%22%22%22Angle%20in%20degrees%20between%20the%20vectors%20%60u%60%20and%20%60w%60%20(along%20the%20last%20axis).%22%22%22%0A%20%20%20%20import%20numpy%20as%20np%0A%0A%20%20%20%20u%20%3D%20np.asarray(u%2C%20dtype%3Dfloat)%0A%20%20%20%20w%20%3D%20np.asarray(w%2C%20dtype%3Dfloat)%0A%20%20%20%20cos%20%3D%20np.sum(u%20*%20w%2C%20axis%3D-1)%20%2F%20(np.linalg.norm(u%2C%20axis%3D-1)%20*%20np.linalg.norm(w%2C%20axis%3D-1))%0A%20%20%20%20return%20np.degrees(np.arccos(np.clip(cos%2C%20-1%2C%201)))%0A%0A%0A%40app.cell%0Adef%20_(markers)%3A%0A%20%20%20%20_origin%2C%20_basis%20%3D%20segment_frame(*markers)%0A%0A%20%20%20%20print(%22Local%20coordinates%20of%20the%20markers%20(one%20marker%20per%20row)%3A%22)%0A%20%20%20%20print(local_coordinates(markers%2C%20_origin%2C%20_basis).round(4)%20%2B%200.0)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Now%20let's%20move%20the%20segment%3A%20turn%20it%20by%2030%C2%B0%20about%20the%20laboratory's%20Z%20axis%20and%20shift%20it%20by%20%24%5B0.5%2C%20-0.2%2C%200.1%5D%24.%20The%20rotation%20is%20done%20with%20a%20rotation%20matrix%3B%20how%20rotation%20matrices%20work%20is%20the%20subject%20of%20%5BRigid-body%20transformations%20in%203D%5D(https%3A%2F%2Fmolab.marimo.io%2Fgithub%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fnotebooks%2Ftransformation_3d.py)%2C%20but%20here%20it%20is%20simply%20a%20way%20to%20move%20the%20three%20markers%20as%20one%20rigid%20body.%0A%0A%20%20%20%20**Before%20you%20run%20the%20next%20cell**%2C%20predict%3A%20which%20numbers%20change%2C%20the%20laboratory%20coordinates%20of%20the%20markers%2C%20the%20versors%2C%20the%20local%20coordinates%3F%20And%20does%20every%20versor%20turn%20by%2030%C2%B0%3F%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(basis%2C%20markers%2C%20np)%3A%0A%20%20%20%20_theta%20%3D%20np.radians(30)%0A%20%20%20%20_Rz%20%3D%20np.array(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%5Bnp.cos(_theta)%2C%20-np.sin(_theta)%2C%200%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5Bnp.sin(_theta)%2C%20np.cos(_theta)%2C%200%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B0%2C%200%2C%201%5D%2C%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20)%0A%20%20%20%20_moved%20%3D%20markers%20%40%20_Rz.T%20%2B%20np.array(%5B0.5%2C%20-0.2%2C%200.1%5D)%0A%20%20%20%20_origin%2C%20_basis%20%3D%20segment_frame(*_moved)%0A%0A%20%20%20%20print(%22Laboratory%20coordinates%20of%20the%20moved%20markers%3A%22)%0A%20%20%20%20print(_moved.round(4))%0A%20%20%20%20print(%22%5CnLocal%20coordinates%20of%20the%20moved%20markers%3A%22)%0A%20%20%20%20print(local_coordinates(_moved%2C%20_origin%2C%20_basis).round(4)%20%2B%200.0)%0A%20%20%20%20print(%22%5CnHow%20much%20each%20versor%20turned%20%5Bdegrees%5D%3A%22%2C%20angle_between(_basis%2C%20basis).round(1))%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Every%20laboratory%20coordinate%20changed%2C%20and%20so%20did%20every%20versor%2C%20but%20the%20local%20coordinates%20are%20exactly%20the%20same%20as%20before.%20That%20is%20what%20a%20segment%20coordinate%20system%20buys%20you%3A%20numbers%20that%20describe%20the%20segment%20and%20are%20blind%20to%20where%20the%20segment%20happens%20to%20be.%20When%20the%20local%20coordinates%20of%20a%20marker%20change%20from%20one%20frame%20to%20the%20next%2C%20either%20the%20segment%20is%20not%20rigid%20or%20the%20marker%20has%20moved%20on%20it.%0A%0A%20%20%20%20Two%20details%20are%20worth%20a%20second%20look.%0A%0A%20%20%20%20-%20The%20second%20local%20coordinate%20of%20every%20marker%20is%20zero.%20That%20is%20no%20accident%3A%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%24%20is%20perpendicular%20to%20the%20plane%20of%20the%20markers%2C%20so%20no%20marker%20has%20any%20position%20along%20it.%0A%20%20%20%20-%20The%20segment%20turned%20by%2030%C2%B0%2C%20but%20its%20versors%20turned%20by%2012.7%C2%B0%2C%2027.9%C2%B0%20and%2029.2%C2%B0.%20Only%20a%20vector%20perpendicular%20to%20the%20axis%20of%20rotation%20turns%20by%20the%20full%20angle.%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_1%24%20is%20the%20versor%20closest%20to%20the%20Z%20axis%20(its%20Z%20component%20is%200.90)%2C%20so%20it%20turns%20the%20least.%0A%0A%20%20%20%20**Guiding%20questions%201.**%0A%0A%20%20%20%201.%20Which%20rotation%20of%20the%20segment%20would%20leave%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_1%24%20completely%20unchanged%3F%20Would%20the%20local%20coordinates%20change%3F%0A%20%20%20%202.%20A%20fourth%20marker%20on%20the%20same%20plate%20has%20a%20local%20coordinate%20of%202%20cm%20along%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%24%20in%20one%20frame%20and%202.6%20cm%20in%20the%20next.%20Give%20two%20possible%20explanations.%0A%20%20%20%203.%20Why%20must%20the%20origin%20be%20calculated%20from%20the%20markers%20too%2C%20and%20not%20taken%20as%20a%20fixed%20point%20of%20the%20laboratory%3F%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20**Exercise%203.**%20A%20segment's%20coordinate%20system%20has%20its%20origin%20at%20%24%5Coverrightarrow%7B%5Cmathbf%7BO%7D%7D%20%3D%20%5B1%2C%202%2C%200%5D%24%20and%20the%20versors%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_1%20%3D%20%5B0%2C%201%2C%200%5D%24%2C%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%20%3D%20%5B-1%2C%200%2C%200%5D%24%20and%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_3%20%3D%20%5B0%2C%200%2C%201%5D%24.%3Cbr%3E%0A%20%20%20%20a)%20What%20are%20the%20local%20coordinates%20of%20the%20point%20%24%5Coverrightarrow%7B%5Cmathbf%7BP%7D%7D%20%3D%20%5B1%2C%203%2C%200.5%5D%24%3F%3Cbr%3E%0A%20%20%20%20b)%20Which%20point%20of%20the%20laboratory%20has%20the%20local%20coordinates%20%24%5B2%2C%201%2C%200%5D%24%3F%0A%0A%20%20%20%20%2F%2F%2F%20details%20%7C%20Solution%20to%20Exercise%203%0A%0A%20%20%20%20a)%20%24%5Coverrightarrow%7B%5Cmathbf%7BP%7D%7D%20-%20%5Coverrightarrow%7B%5Cmathbf%7BO%7D%7D%20%3D%20%5B0%2C%201%2C%200.5%5D%24%2C%20and%20its%20dot%20products%20with%20the%20three%20versors%20are%20%241%24%2C%20%240%24%20and%20%240.5%24%3A%20the%20local%20coordinates%20are%20%24%5B1%2C%200%2C%200.5%5D%24.%0A%0A%20%20%20%20b)%20Local%20coordinates%20say%20how%20much%20of%20each%20versor%20to%20add%20to%20the%20origin%3A%0A%0A%20%20%20%20%24%24%0A%20%20%20%20%5Coverrightarrow%7B%5Cmathbf%7BO%7D%7D%20%2B%202%5C%2C%5Chat%7B%5Cmathbf%7Be%7D%7D_1%20%2B%201%5C%2C%5Chat%7B%5Cmathbf%7Be%7D%7D_2%20%2B%200%5C%2C%5Chat%7B%5Cmathbf%7Be%7D%7D_3%20%3D%20%5B1%2C%202%2C%200%5D%20%2B%20%5B0%2C%202%2C%200%5D%20%2B%20%5B-1%2C%200%2C%200%5D%20%3D%20%5B0%2C%204%2C%200%5D%0A%20%20%20%20%24%24%0A%0A%20%20%20%20Check%20it%20by%20going%20back%3A%20%24%5B0%2C%204%2C%200%5D%20-%20%5Coverrightarrow%7B%5Cmathbf%7BO%7D%7D%20%3D%20%5B-1%2C%202%2C%200%5D%24%2C%20whose%20dot%20products%20with%20the%20versors%20are%20%242%24%2C%20%241%24%20and%20%240%24.%0A%0A%20%20%20%20%2F%2F%2F%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Example%3A%20a%20landmark%20without%20a%20marker%0A%0A%20%20%20%20Local%20coordinates%20do%20more%20than%20prove%20that%20a%20segment%20is%20rigid%3A%20they%20let%20you%20follow%20points%20that%20carry%20no%20marker%20at%20all.%20Suppose%20you%20need%20the%20lateral%20malleolus%20to%20locate%20the%20ankle%20joint%20centre%2C%20but%20a%20marker%20on%20the%20malleolus%20would%20slide%20with%20the%20skin%2C%20or%20get%20knocked%20off%2C%20during%20walking.%20The%20usual%20solution%20is%20to%20locate%20the%20landmark%20once%2C%20in%20a%20standing%20calibration%20trial%2C%20while%20the%20shank%20cluster%20is%20recorded%2C%20and%20to%20store%20its%20local%20coordinates%20in%20the%20cluster's%20coordinate%20system.%20In%20every%20frame%20of%20the%20movement%2C%20the%20landmark%20is%20then%20rebuilt%20from%20the%20cluster%20alone%2C%20by%20running%20Exercise%203%20b)%20backwards%3A%0A%0A%20%20%20%20%24%24%0A%20%20%20%20%5Coverrightarrow%7B%5Cmathbf%7Bp%7D%7D%20%3D%20%5Coverrightarrow%7B%5Cmathbf%7BO%7D%7D%20%2B%20x_%7Blocal%7D%5C%2C%5Chat%7B%5Cmathbf%7Be%7D%7D_1%20%2B%20y_%7Blocal%7D%5C%2C%5Chat%7B%5Cmathbf%7Be%7D%7D_2%20%2B%20z_%7Blocal%7D%5C%2C%5Chat%7B%5Cmathbf%7Be%7D%7D_3%0A%20%20%20%20%24%24%0A%0A%20%20%20%20Here%20is%20that%20formula%20as%20a%20function%2C%20the%20inverse%20of%20%60local_coordinates%60%2C%20and%20a%20function%20that%20builds%20rotation%20matrices%2C%20which%20we%20need%20only%20to%20generate%20the%20movement%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.function%0Adef%20global_coordinates(local%2C%20origin%2C%20basis)%3A%0A%20%20%20%20%22%22%22Laboratory%20coordinates%20of%20the%20point(s)%20with%20the%20given%20%60local%60%20coordinates%0A%20%20%20%20in%20the%20coordinate%20system%20with%20%60origin%60%20and%20%60basis%60%20(versors%20as%20rows)%3B%20the%0A%20%20%20%20inverse%20of%20local_coordinates.%22%22%22%0A%20%20%20%20import%20numpy%20as%20np%0A%0A%20%20%20%20return%20origin%20%2B%20np.asarray(local%2C%20dtype%3Dfloat)%20%40%20np.asarray(basis)%0A%0A%0A%40app.function%0Adef%20rotation_matrix(axis%2C%20angle)%3A%0A%20%20%20%20%22%22%22Matrix%20that%20rotates%20vectors%20by%20%60angle%60%20degrees%20about%20the%20laboratory's%0A%20%20%20%20%22x%22%2C%20%22y%22%20or%20%22z%22%20axis%2C%20counterclockwise%20seen%20from%20the%20positive%20end%20of%20the%20axis.%22%22%22%0A%20%20%20%20import%20numpy%20as%20np%0A%0A%20%20%20%20c%2C%20s%20%3D%20np.cos(np.radians(angle))%2C%20np.sin(np.radians(angle))%0A%20%20%20%20if%20axis%20%3D%3D%20%22x%22%3A%0A%20%20%20%20%20%20%20%20return%20np.array(%5B%5B1%2C%200%2C%200%5D%2C%20%5B0%2C%20c%2C%20-s%5D%2C%20%5B0%2C%20s%2C%20c%5D%5D)%0A%20%20%20%20if%20axis%20%3D%3D%20%22y%22%3A%0A%20%20%20%20%20%20%20%20return%20np.array(%5B%5Bc%2C%200%2C%20s%5D%2C%20%5B0%2C%201%2C%200%5D%2C%20%5B-s%2C%200%2C%20c%5D%5D)%0A%20%20%20%20return%20np.array(%5B%5Bc%2C%20-s%2C%200%5D%2C%20%5Bs%2C%20c%2C%200%5D%2C%20%5B0%2C%200%2C%201%5D%5D)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20laboratory's%20axes%20point%20forward%20(%24X%24)%2C%20up%20(%24Y%24)%20and%20to%20the%20right%20(%24Z%24)%2C%20and%20we%20follow%20the%20right%20leg.%20In%20the%20calibration%20trial%2C%20the%20cluster's%20three%20markers%20sit%20on%20the%20lateral%20side%20of%20the%20shank%2C%20about%207%20to%2011%20cm%20apart%2C%20and%20the%20lateral%20malleolus%20is%20located%20with%20a%20pointer%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(np)%3A%0A%20%20%20%20%23%20Standing%20calibration%20trial%20%5Bm%5D%0A%20%20%20%20knee%20%3D%20np.array(%5B0.000%2C%200.500%2C%200.100%5D)%20%20%23%20knee%20joint%20centre%0A%20%20%20%20cluster_static%20%3D%20np.array(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B0.040%2C%200.330%2C%200.150%5D%2C%20%20%23%20c1%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B-0.030%2C%200.345%2C%200.148%5D%2C%20%20%23%20c2%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B0.005%2C%200.245%2C%200.156%5D%2C%20%20%23%20c3%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20)%0A%20%20%20%20lm_static%20%3D%20np.array(%5B-0.010%2C%200.070%2C%200.135%5D)%20%20%23%20lateral%20malleolus%2C%20located%20in%20this%20trial%20only%0A%0A%20%20%20%20_origin%2C%20_basis%20%3D%20segment_frame(*cluster_static)%0A%20%20%20%20lm_local%20%3D%20local_coordinates(lm_static%2C%20_origin%2C%20_basis)%0A%0A%20%20%20%20print(%22Lateral%20malleolus%20in%20the%20cluster's%20coordinate%20system%3A%22%2C%20lm_local.round(4)%2C%20%22m%22)%0A%20%20%20%20print(f%22Distance%20from%20the%20cluster's%20origin%3A%20%7Bnp.linalg.norm(lm_local)%3A.3f%7D%20m%22)%0A%20%20%20%20return%20cluster_static%2C%20knee%2C%20lm_local%2C%20lm_static%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20malleolus%20is%20about%2024%20cm%20from%20the%20cluster's%20origin.%20Now%20a%20frame%20of%20the%20movement%2C%20in%20which%20the%20motion%20capture%20system%20reports%20only%20the%20cluster%2C%20to%20the%20millimetre.%20To%20have%20a%20known%20answer%20to%20check%20against%2C%20the%20frame%20was%20generated%20from%20the%20calibration%3A%20the%20knee%20flexed%20by%2045%C2%B0%2C%20the%20shank%20turned%208%C2%B0%20outwards%20about%20its%20long%20axis%2C%20and%20the%20whole%20leg%20moved%200.75%20m%20forward.%20The%20same%20movement%20gives%20the%20true%20position%20of%20the%20malleolus%2C%20which%20we%20will%20now%20pretend%20not%20to%20know.%0A%0A%20%20%20%20**Before%20you%20run%20the%20next%20cells**%2C%20predict%3A%20how%20far%20from%20its%20true%20position%20will%20the%20rebuilt%20malleolus%20be%3F%20And%20how%20much%20will%20each%20versor%20of%20the%20cluster%20turn%3F%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(cluster_static%2C%20knee%2C%20lm_static%2C%20np)%3A%0A%20%20%20%20_R%20%3D%20rotation_matrix(%22z%22%2C%20-45)%20%40%20rotation_matrix(%22y%22%2C%20-8)%20%20%23%20flexion%20after%20the%20outward%20turn%0A%20%20%20%20_move%20%3D%20lambda%20p%3A%20knee%20%2B%20(p%20-%20knee)%20%40%20_R.T%20%2B%20np.array(%5B0.75%2C%200.02%2C%200.0%5D)%0A%0A%20%20%20%20cluster_mov%20%3D%20np.round(_move(cluster_static)%2C%203)%20%20%23%20reported%20to%20the%20millimetre%0A%20%20%20%20lm_true%20%3D%20_move(lm_static)%20%20%23%20the%20answer%20we%20pretend%20not%20to%20know%0A%0A%20%20%20%20print(%22Cluster%20markers%20in%20the%20movement%20trial%20%5Bm%5D%3A%5Cn%22%2C%20cluster_mov)%0A%20%20%20%20return%20cluster_mov%2C%20lm_true%0A%0A%0A%40app.cell%0Adef%20_(cluster_mov%2C%20cluster_static%2C%20lm_local%2C%20lm_true%2C%20np)%3A%0A%20%20%20%20_origin%2C%20_basis%20%3D%20segment_frame(*cluster_mov)%0A%20%20%20%20_%2C%20_basis_static%20%3D%20segment_frame(*cluster_static)%0A%20%20%20%20lm_rebuilt%20%3D%20global_coordinates(lm_local%2C%20_origin%2C%20_basis)%0A%0A%20%20%20%20print(%22Rebuilt%20lateral%20malleolus%3A%22%2C%20lm_rebuilt.round(4)%2C%20%22m%22)%0A%20%20%20%20print(f%22Distance%20from%20the%20true%20position%3A%20%7B1000%20*%20np.linalg.norm(lm_rebuilt%20-%20lm_true)%3A.2f%7D%20mm%22)%0A%20%20%20%20print(%22How%20much%20each%20versor%20of%20the%20cluster%20turned%20%5Bdegrees%5D%3A%22%2C%20angle_between(_basis%2C%20_basis_static).round(1))%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20malleolus%20is%20rebuilt%20less%20than%20half%20a%20millimetre%20from%20where%20it%20really%20is%2C%20and%20that%20small%20error%20comes%20entirely%20from%20rounding%20the%20cluster's%20markers%20to%20the%20millimetre%2C%20which%20shifts%20the%20cluster's%20origin%20and%20tilts%20its%20axes%20slightly.%0A%0A%20%20%20%20The%20versors%20turned%20by%2045.6%C2%B0%2C%205.7%C2%B0%20and%2045.4%C2%B0.%20The%20knee%20flexed%20by%2045%C2%B0%20about%20the%20laboratory's%20%24Z%24%20axis%2C%20and%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%24%2C%20perpendicular%20to%20a%20plate%20on%20the%20lateral%20side%20of%20the%20shank%2C%20points%20almost%20along%20%24Z%24.%20A%20versor%20parallel%20to%20the%20axis%20of%20rotation%20does%20not%20turn%20at%20all%2C%20which%20is%20Guiding%20question%201.1%20seen%20in%20real%20data%3B%20the%20few%20degrees%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%24%20did%20turn%20come%20from%20its%20not%20being%20exactly%20parallel%20to%20%24Z%24%20and%20from%20the%20outward%20turn%20of%20the%20shank.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20When%20the%20markers%20are%20real%0A%0A%20%20%20%20Everything%20so%20far%20used%20exact%20positions.%20Real%20markers%20are%20located%20with%20an%20error%3A%20around%20a%20millimetre%20for%20a%20good%20optoelectronic%20system%2C%20and%20much%20more%20if%20you%20count%20the%20movement%20of%20the%20skin%20over%20the%20bone.%20How%20does%20an%20error%20in%20the%20markers%20become%20an%20error%20in%20the%20axes%3F%0A%0A%20%20%20%20Let's%20simulate%20it.%20The%20function%20below%20places%20three%20markers%20at%20the%20corners%20of%20an%20equilateral%20triangle%20with%20a%20given%20side%2C%20like%20the%20markers%20of%20a%20cluster.%20We%20add%20random%20noise%20with%20a%20standard%20deviation%20of%201%20mm%20to%20every%20coordinate%20of%20every%20marker%2C%20compute%20the%20basis%2C%20and%20measure%20the%20angle%20between%20each%20noisy%20versor%20and%20the%20true%20one.%20Each%20run%20has%205000%20simulated%20frames.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.function%0Adef%20equilateral_cluster(side)%3A%0A%20%20%20%20%22%22%22Positions%20of%20three%20markers%20at%20the%20corners%20of%20an%20equilateral%20triangle%0A%20%20%20%20with%20the%20given%20side%2C%20in%20the%20plane%20z%20%3D%200%20and%20centred%20on%20the%20origin.%22%22%22%0A%20%20%20%20import%20numpy%20as%20np%0A%0A%20%20%20%20angles%20%3D%20np.radians(%5B90%2C%20210%2C%20330%5D)%0A%20%20%20%20radius%20%3D%20side%20%2F%20np.sqrt(3)%0A%20%20%20%20return%20%5Bnp.array(%5Bradius%20*%20np.cos(a)%2C%20radius%20*%20np.sin(a)%2C%200.0%5D)%20for%20a%20in%20angles%5D%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20**Before%20you%20run%20the%20next%20cell**%2C%20predict%20the%20typical%20angular%20error%20of%20the%20axes%20for%20a%20cluster%20with%20markers%2010%20cm%20apart%3A%200.1%C2%B0%2C%201%C2%B0%20or%2010%C2%B0%3F%20And%20for%20a%20cluster%20half%20that%20size%3F%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(np)%3A%0A%20%20%20%20_rng%20%3D%20np.random.default_rng(0)%0A%20%20%20%20_sigma%2C%20_n%20%3D%200.001%2C%205000%20%20%23%201%20mm%20of%20noise%2C%20number%20of%20simulated%20frames%0A%0A%20%20%20%20print(%22Side%20%20%20%20median%20error%20of%20e1%2C%20e2%2C%20e3%20%20%20%2095th%20percentile%22)%0A%20%20%20%20for%20_side%20in%20%5B0.05%2C%200.10%2C%200.15%5D%3A%0A%20%20%20%20%20%20%20%20_markers%20%3D%20equilateral_cluster(_side)%0A%20%20%20%20%20%20%20%20_%2C%20_basis%20%3D%20segment_frame(*_markers)%0A%20%20%20%20%20%20%20%20_%2C%20_noisy%20%3D%20segment_frame(*%5Bm%20%2B%20_rng.normal(0%2C%20_sigma%2C%20(_n%2C%203))%20for%20m%20in%20_markers%5D)%0A%20%20%20%20%20%20%20%20_err%20%3D%20angle_between(_noisy%2C%20_basis)%0A%20%20%20%20%20%20%20%20print(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%7B100%20*%20_side%3A3.0f%7D%20cm%20%20%20%7Bnp.median(_err%2C%20axis%3D0).round(2)%7D%20degrees%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%20%20%20%7Bnp.percentile(_err%2C%2095%2C%20axis%3D0).round(2)%7D%20degrees%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20With%20markers%2010%20cm%20apart%2C%20a%20millimetre%20of%20noise%20turns%20each%20axis%20by%20about%201%C2%B0%20in%20a%20typical%20frame%2C%20and%20by%202%C2%B0%20in%20one%20frame%20in%20twenty.%20Halve%20the%20size%20of%20the%20cluster%20and%20the%20errors%20double.%20Each%20axis%20is%20a%20direction%20computed%20from%20differences%20between%20markers%2C%20and%20an%20error%20of%201%20mm%20in%20a%20difference%20of%205%20cm%20tilts%20that%20difference%20twice%20as%20much%20as%20the%20same%20error%20in%20a%20difference%20of%2010%20cm.%0A%0A%20%20%20%20Let's%20check%20that%20the%20error%20really%20goes%20as%20the%20inverse%20of%20the%20spacing%2C%20for%20sides%20from%202%20to%2020%20cm.%20This%20time%20we%20record%2C%20in%20every%20frame%2C%20the%20worst%20of%20the%20three%20axes%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(np%2C%20plt)%3A%0A%20%20%20%20_rng%20%3D%20np.random.default_rng(1)%0A%20%20%20%20_sides%20%3D%20np.linspace(0.02%2C%200.20%2C%2010)%0A%20%20%20%20_worst%20%3D%20%5B%5D%0A%20%20%20%20for%20_side%20in%20_sides%3A%0A%20%20%20%20%20%20%20%20_markers%20%3D%20equilateral_cluster(_side)%0A%20%20%20%20%20%20%20%20_%2C%20_basis%20%3D%20segment_frame(*_markers)%0A%20%20%20%20%20%20%20%20_%2C%20_noisy%20%3D%20segment_frame(*%5Bm%20%2B%20_rng.normal(0%2C%200.001%2C%20(5000%2C%203))%20for%20m%20in%20_markers%5D)%0A%20%20%20%20%20%20%20%20_worst.append(np.median(angle_between(_noisy%2C%20_basis).max(axis%3D1)))%0A%20%20%20%20_worst%20%3D%20np.array(_worst)%0A%20%20%20%20_k%20%3D%20np.mean(_worst%20*%20_sides)%20%20%23%20error%20x%20side%2C%20constant%20if%20the%20error%20goes%20as%201%2Fside%0A%0A%20%20%20%20_d%20%3D%20np.linspace(0.018%2C%200.21%2C%20200)%0A%20%20%20%20_%2C%20_ax%20%3D%20plt.subplots(figsize%3D(8%2C%204.5))%0A%20%20%20%20_ax.plot(100%20*%20_d%2C%20_k%20%2F%20_d%2C%20color%3D%220.6%22%2C%20linewidth%3D2%2C%20label%3Df%22%7B_k%3A.3f%7D%20degree%C2%B7m%20%2F%20side%22)%0A%20%20%20%20_ax.plot(100%20*%20_sides%2C%20_worst%2C%20%22o%22%2C%20color%3D%22tab%3Ared%22%2C%20markersize%3D8%2C%20label%3D%22simulation%22)%0A%20%20%20%20_ax.set_xlabel(%22Side%20of%20the%20cluster%20%5Bcm%5D%22)%0A%20%20%20%20_ax.set_ylabel(%22Error%20of%20the%20worst%20axis%20%5Bdegrees%5D%22)%0A%20%20%20%20_ax.set_title(%22Median%20error%20of%20the%20worst%20axis%2C%201%20mm%20of%20noise%22)%0A%20%20%20%20_ax.legend()%0A%20%20%20%20plt.tight_layout()%0A%20%20%20%20plt.show()%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20simulation%20follows%20the%20inverse%20of%20the%20spacing%20closely%3A%20doubling%20the%20distance%20between%20the%20markers%20halves%20the%20error.%20Halving%20the%20noise%20would%20have%20the%20same%20effect%2C%20which%20gives%20you%20two%20levers%20for%20the%20same%20result%2C%20and%20spreading%20the%20markers%20is%20usually%20the%20cheaper%20one.%20Cappozzo%20et%20al.%20(1997)%20turned%20considerations%20like%20these%20into%20design%20criteria%20for%20marker%20clusters.%0A%0A%20%20%20%20Why%20not%2C%20then%2C%20make%20the%20cluster%20as%20large%20as%20possible%3F%20Because%20the%20cluster%20sits%20on%20skin.%20The%20larger%20it%20is%2C%20the%20more%20skin%20it%20spans%2C%20and%20skin%20moves%20relative%20to%20the%20bone%2C%20differently%20from%20place%20to%20place.%20The%20best%20cluster%20is%20a%20compromise%20between%20the%20two%20kinds%20of%20error.%0A%0A%20%20%20%20**Exercise%204.**%20A%20cluster%20has%20its%20markers%2010%20cm%20apart%2C%20each%20with%201%20mm%20of%20noise%2C%20and%20is%20used%20to%20rebuild%20a%20landmark%2030%20cm%20from%20the%20cluster's%20origin%2C%20in%20the%20plane%20of%20the%20markers%2C%20as%20in%20the%20example%20of%20the%20malleolus.%20Before%20simulating%2C%20estimate%20the%20typical%20error%20of%20(a)%20the%20origin%20and%20(b)%20the%20rebuilt%20landmark.%20Then%20check%20your%20estimates%20with%205000%20simulated%20frames%2C%20using%20%60equilateral_cluster%60%2C%20%60segment_frame%60%20and%20%60global_coordinates%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo%2C%20np)%3A%0A%20%20%20%20_rng%20%3D%20np.random.default_rng(3)%0A%20%20%20%20_markers%20%3D%20equilateral_cluster(0.10)%0A%20%20%20%20_origin%2C%20_basis%20%3D%20segment_frame(*_markers)%0A%20%20%20%20_p_local%20%3D%20np.array(%5B0%2C%200%2C%20-0.30%5D)%20%20%23%2030%20cm%20from%20the%20origin%2C%20in%20the%20plane%20of%20the%20markers%0A%20%20%20%20_p_true%20%3D%20global_coordinates(_p_local%2C%20_origin%2C%20_basis)%0A%0A%20%20%20%20_origin_n%2C%20_basis_n%20%3D%20segment_frame(*%5Bm%20%2B%20_rng.normal(0%2C%200.001%2C%20(5000%2C%203))%20for%20m%20in%20_markers%5D)%0A%20%20%20%20_p_n%20%3D%20global_coordinates(_p_local%2C%20_origin_n%2C%20_basis_n)%0A%20%20%20%20_err_origin%20%3D%201000%20*%20np.linalg.norm(_origin_n%20-%20_origin%2C%20axis%3D1)%0A%20%20%20%20_err_p%20%3D%201000%20*%20np.linalg.norm(_p_n%20-%20_p_true%2C%20axis%3D1)%0A%20%20%20%20_err_axis%20%3D%20np.median(angle_between(_basis_n%2C%20_basis))%0A%20%20%20%20_lever%20%3D%20300%20*%20np.radians(_err_axis)%0A%0A%20%20%20%20mo.accordion(%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Solution%20to%20Exercise%204%22%3A%20mo.md(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%22%22%22%0A%20%20%20%20**(a)%20The%20origin.**%20It%20is%20the%20average%20of%20three%20markers%2C%20so%20each%20of%20its%20coordinates%20has%20a%20standard%20deviation%20of%201%20mm%20divided%20by%20%E2%88%9A3%2C%20or%20%7B1%20%2F%20np.sqrt(3)%3A.2f%7D%20mm.%20In%20three%20dimensions%20that%20makes%20a%20typical%20error%20of%20%7Bnp.median(_err_origin)%3A.2f%7D%20mm%20(median%20of%20the%20simulation)%2C%20less%20than%20the%20error%20of%20any%20single%20marker.%0A%0A%20%20%20%20**(b)%20The%20landmark.**%20Its%20error%20is%20dominated%20by%20the%20tilt%20of%20the%20axes.%20The%20axes%20are%20off%20by%20%7B_err_axis%3A.2f%7D%C2%B0%20in%20a%20typical%20frame%20(as%20in%20the%20simulation%20above)%2C%20and%20a%20tilt%20of%20that%20size%20moves%20a%20point%2030%20cm%20away%20by%200.30%20m%20%C3%97%20%7Bnp.radians(_err_axis)%3A.4f%7D%20rad%20%3D%20%7B_lever%3A.1f%7D%20mm.%20The%20simulation%20gives%20a%20median%20error%20of%20%7Bnp.median(_err_p)%3A.1f%7D%20mm%2C%20and%20%7Bnp.percentile(_err_p%2C%2095)%3A.1f%7D%20mm%20in%20one%20frame%20in%20twenty.%0A%0A%20%20%20%20The%20landmark%20is%20rebuilt%20about%20%7Bnp.median(_err_p)%20%2F%20np.median(_err_origin)%3A.0f%7D%20times%20worse%20than%20the%20origin.%20Angular%20errors%20grow%20with%20distance%2C%20which%20is%20why%20a%20cluster%20should%20sit%20close%20to%20the%20landmarks%20it%20is%20used%20to%20rebuild%2C%20and%20why%20a%20joint%20centre%20far%20from%20a%20small%20cluster%20is%20the%20least%20reliable%20point%20of%20a%20model.%0A%20%20%20%20%22%22%22%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Nearly%20collinear%20markers%0A%0A%20%20%20%20The%20recipe%20requires%20noncollinear%20points%3A%20if%20the%20three%20markers%20lie%20on%20a%20line%2C%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%24%20and%20%24%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%20-%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1%24%20are%20parallel%2C%20their%20cross%20product%20is%20zero%2C%20and%20there%20is%20no%20basis%20at%20all.%20Real%20markers%20are%20never%20exactly%20in%20a%20line%2C%20but%20what%20happens%20when%20they%20almost%20are%3F%0A%0A%20%20%20%20Let's%20keep%20two%20markers%2010%20cm%20apart%20and%20move%20the%20third%20one%20towards%20the%20line%20between%20them%2C%20from%205%20cm%20away%20to%202%20cm%20and%20then%205%20mm.%0A%0A%20%20%20%20**Before%20you%20run%20the%20next%20cell**%2C%20predict%3A%20which%20of%20the%20three%20versors%20suffers%3F%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(np)%3A%0A%20%20%20%20_rng%20%3D%20np.random.default_rng(2)%0A%20%20%20%20_sigma%2C%20_n%20%3D%200.001%2C%205000%0A%0A%20%20%20%20print(%22Offset%20%20%20%20median%20error%20of%20e1%2C%20e2%2C%20e3%20%20%20%2095th%20percentile%22)%0A%20%20%20%20for%20_h%20in%20%5B0.05%2C%200.02%2C%200.005%5D%3A%0A%20%20%20%20%20%20%20%20_markers%20%3D%20%5Bnp.array(%5B-0.05%2C%200%2C%200%5D)%2C%20np.array(%5B0.05%2C%200%2C%200%5D)%2C%20np.array(%5B0%2C%20_h%2C%200%5D)%5D%0A%20%20%20%20%20%20%20%20_%2C%20_basis%20%3D%20segment_frame(*_markers)%0A%20%20%20%20%20%20%20%20_%2C%20_noisy%20%3D%20segment_frame(*%5Bm%20%2B%20_rng.normal(0%2C%20_sigma%2C%20(_n%2C%203))%20for%20m%20in%20_markers%5D)%0A%20%20%20%20%20%20%20%20_err%20%3D%20angle_between(_noisy%2C%20_basis)%0A%20%20%20%20%20%20%20%20print(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%7B1000%20*%20_h%3A3.0f%7D%20mm%20%20%20%7Bnp.median(_err%2C%20axis%3D0).round(2)%7D%20degrees%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%20%20%20%7Bnp.percentile(_err%2C%2095%2C%20axis%3D0).round(2)%7D%20degrees%22%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20first%20versor%20does%20not%20care%3A%20it%20depends%20only%20on%20the%20first%20two%20markers%2C%20which%20stay%2010%20cm%20apart%2C%20and%20its%20error%20stays%20at%20about%201%C2%B0.%20The%20other%20two%20degrade%20quickly.%20With%20the%20third%20marker%205%20mm%20from%20the%20line%2C%20their%20typical%20error%20is%20about%2010%C2%B0%2C%20and%20in%20one%20frame%20in%20twenty%20it%20is%20close%20to%2030%C2%B0%3A%20a%20coordinate%20system%20that%20wobbles%20by%20tens%20of%20degrees%20while%20the%20segment%20stands%20still.%0A%0A%20%20%20%20The%20reason%20is%20in%20step%202.%20The%20norm%20of%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%20%5Ctimes%20(%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_3%20-%20%5Coverrightarrow%7B%5Cmathbf%7Bm%7D%7D_1)%24%20is%20the%20length%20of%20%24%5Coverrightarrow%7B%5Cmathbf%7Bv%7D%7D_1%24%20times%20the%20distance%20of%20the%20third%20marker%20from%20the%20line.%20When%20that%20distance%20is%205%20mm%2C%20a%201%20mm%20error%20in%20the%20third%20marker%20is%20a%20large%20fraction%20of%20it%2C%20and%20the%20direction%20of%20the%20cross%20product%2C%20which%20defines%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%24%2C%20swings%20wildly.%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_3%24%20is%20computed%20from%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%24%20and%20inherits%20the%20error.%0A%0A%20%20%20%20**Challenge%201.**%20For%20the%205%20mm%20case%2C%20the%20first%20axis%20is%20built%20from%20the%20two%20markers%20that%20are%2010%20cm%20apart.%20Pass%20the%20same%20markers%20to%20%60segment_frame%60%20in%20a%20different%20order%2C%20so%20that%20the%20first%20axis%20is%20built%20from%20one%20of%20the%20outer%20markers%20and%20the%20marker%20near%20the%20middle%2C%20and%20compare%20the%20errors.%20What%20rule%20would%20you%20adopt%20for%20choosing%20which%20markers%20define%20the%20first%20axis%3F%0A%0A%20%20%20%20**Exercise%205.**%20In%20the%205%20mm%20case%2C%20all%20three%20markers%20are%20noisy.%20Which%20marker's%20noise%20does%20the%20most%20damage%20to%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%24%3F%20Add%20the%20noise%20to%20one%20marker%20at%20a%20time%20and%20find%20out.%20Then%20explain%20the%20result%20with%20the%20distance%20of%20each%20marker%20from%20the%20line%20through%20the%20other%20two.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo%2C%20np)%3A%0A%20%20%20%20_rng%20%3D%20np.random.default_rng(4)%0A%20%20%20%20_markers%20%3D%20%5Bnp.array(%5B-0.05%2C%200%2C%200%5D)%2C%20np.array(%5B0.05%2C%200%2C%200%5D)%2C%20np.array(%5B0%2C%200.005%2C%200%5D)%5D%0A%20%20%20%20_%2C%20_basis%20%3D%20segment_frame(*_markers)%0A%0A%20%20%20%20_rows%2C%20_e2%20%3D%20%5B%5D%2C%20%5B%5D%0A%20%20%20%20for%20_k%20in%20range(3)%3A%0A%20%20%20%20%20%20%20%20_noisy%20%3D%20%5Bm%20%2B%20_rng.normal(0%2C%200.001%2C%20(5000%2C%203))%20if%20_i%20%3D%3D%20_k%20else%20m%20for%20_i%2C%20m%20in%20enumerate(_markers)%5D%0A%20%20%20%20%20%20%20%20_%2C%20_basis_n%20%3D%20segment_frame(*_noisy)%0A%20%20%20%20%20%20%20%20_err%20%3D%20np.median(angle_between(_basis_n%2C%20_basis)%2C%20axis%3D0)%0A%20%20%20%20%20%20%20%20_e2.append(_err%5B1%5D)%0A%20%20%20%20%20%20%20%20_a%2C%20_b%20%3D%20%5B_markers%5B_i%5D%20for%20_i%20in%20range(3)%20if%20_i%20!%3D%20_k%5D%0A%20%20%20%20%20%20%20%20_dist%20%3D%20np.linalg.norm(np.cross(_b%20-%20_a%2C%20_markers%5B_k%5D%20-%20_a))%20%2F%20np.linalg.norm(_b%20-%20_a)%0A%20%20%20%20%20%20%20%20_rows.append(f%22%7C%20m%7B_k%20%2B%201%7D%20%7C%20%7B1000%20*%20_dist%3A.1f%7D%20mm%20%7C%20%7B_err%5B0%5D%3A.2f%7D%C2%B0%20%7C%20%7B_err%5B1%5D%3A.2f%7D%C2%B0%20%7C%20%7B_err%5B2%5D%3A.2f%7D%C2%B0%20%7C%22)%0A%0A%20%20%20%20_%2C%20_basis_all%20%3D%20segment_frame(*%5Bm%20%2B%20_rng.normal(0%2C%200.001%2C%20(5000%2C%203))%20for%20m%20in%20_markers%5D)%0A%20%20%20%20_e2_all%20%3D%20np.median(angle_between(_basis_all%2C%20_basis)%2C%20axis%3D0)%5B1%5D%0A%20%20%20%20_table%20%3D%20%22%5Cn%20%20%20%20%22.join(_rows)%20%20%23%20same%20indentation%20as%20the%20text%20around%20it%0A%0A%20%20%20%20mo.accordion(%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22Solution%20to%20Exercise%205%22%3A%20mo.md(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20f%22%22%22%0A%20%20%20%20Median%20error%20of%20each%20versor%20with%201%20mm%20of%20noise%20on%20one%20marker%20only%3A%0A%0A%20%20%20%20%7C%20noisy%20marker%20%7C%20distance%20from%20the%20line%20through%20the%20other%20two%20%7C%20e1%20%7C%20e2%20%7C%20e3%20%7C%0A%20%20%20%20%7C---%7C---%7C---%7C---%7C---%7C%0A%20%20%20%20%7B_table%7D%0A%0A%20%20%20%20The%20third%20marker%20does%20the%20most%20damage%2C%20but%20the%20outer%20ones%20are%20far%20from%20harmless%3A%20their%20noise%20turns%20%C3%AA2%20by%20about%20half%20as%20much.%20The%20plane%20of%20the%20markers%20tilts%20about%20the%20line%20through%20the%20other%20two%20markers%2C%20by%20roughly%20the%20noisy%20marker's%20error%20out%20of%20the%20plane%20divided%20by%20its%20distance%20from%20that%20line.%20That%20distance%20is%205%20mm%20for%20the%20third%20marker%20and%20only%20about%201%20cm%20for%20each%20of%20the%20outer%20ones%3A%20in%20a%20thin%20triangle%2C%20*every*%20marker%20is%20close%20to%20the%20line%20through%20the%20other%20two.%0A%0A%20%20%20%20The%20three%20contributions%20add%20roughly%20like%20the%20sides%20of%20a%20right%20triangle%3A%20%E2%88%9A(%7B_e2%5B0%5D%3A.1f%7D%C2%B2%20%2B%20%7B_e2%5B1%5D%3A.1f%7D%C2%B2%20%2B%20%7B_e2%5B2%5D%3A.1f%7D%C2%B2)%20%3D%20%7Bnp.sqrt(np.sum(np.square(_e2)))%3A.1f%7D%C2%B0%2C%20close%20to%20the%20%7B_e2_all%3A.1f%7D%C2%B0%20of%20a%20simulation%20with%20all%20three%20markers%20noisy.%0A%20%20%20%20%22%22%22%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Example%3A%20a%20label%20swap%20in%20a%20recording%0A%0A%20%20%20%20A%20motion%20capture%20system%20sees%20anonymous%20dots%3B%20software%20decides%20which%20dot%20is%20which%20marker%2C%20frame%20after%20frame.%20Now%20and%20then%20it%20gets%20two%20of%20them%20the%20wrong%20way%20round%20for%20a%20few%20frames%2C%20especially%20when%20markers%20are%20close%20or%20briefly%20hidden.%20Nothing%20in%20the%20coordinates%20looks%20wrong%2C%20but%20the%20segment's%20coordinate%20system%20does%20something%20dramatic.%0A%0A%20%20%20%20Let's%20simulate%20two%20seconds%20of%20the%20shank%20cluster%20from%20the%20malleolus%20example%2C%20recorded%20at%20100%20Hz%20with%201%20mm%20of%20noise%2C%20while%20the%20knee%20swings%20back%20and%20forth%20and%20the%20body%20walks%20forward%20at%201.2%20m%2Fs.%20Between%200.80%20and%200.95%20s%2C%20the%20labels%20of%20%24c_1%24%20and%20%24c_2%24%20are%20swapped.%20Pretend%20you%20do%20not%20know%20that%2C%20and%20look%20for%20it.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(cluster_static%2C%20knee%2C%20np)%3A%0A%20%20%20%20_rng%20%3D%20np.random.default_rng(5)%0A%20%20%20%20t_rec%20%3D%20np.arange(0%2C%202%2C%200.01)%20%20%23%202%20s%20at%20100%20Hz%0A%20%20%20%20_flexion%20%3D%20-15%20%2B%2025%20*%20np.sin(2%20*%20np.pi%20*%200.9%20*%20t_rec)%20%20%23%20degrees%20about%20Z%0A%20%20%20%20_true%20%3D%20np.array(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20knee%20%2B%20(cluster_static%20-%20knee)%20%40%20rotation_matrix(%22z%22%2C%20_a).T%20%2B%20%5B1.2%20*%20_t%2C%200%2C%200%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20_t%2C%20_a%20in%20zip(t_rec%2C%20_flexion)%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20)%0A%20%20%20%20_true%20%3D%20_true%20%2B%20_rng.normal(0%2C%200.001%2C%20_true.shape)%20%20%23%201%20mm%20of%20noise%0A%0A%20%20%20%20recording%20%3D%20_true.copy()%20%20%23%20frames%20x%20markers%20x%20coordinates%0A%20%20%20%20_swap%20%3D%20(t_rec%20%3E%3D%200.80)%20%26%20(t_rec%20%3C%200.95)%0A%20%20%20%20recording%5B_swap%2C%200%5D%2C%20recording%5B_swap%2C%201%5D%20%3D%20_true%5B_swap%2C%201%5D%2C%20_true%5B_swap%2C%200%5D%0A%20%20%20%20return%20recording%2C%20t_rec%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Two%20quantities%20are%20worth%20plotting%3A%20the%20distances%20between%20the%20markers%2C%20which%20a%20rigid%20plate%20keeps%20constant%20whatever%20the%20segment%20does%2C%20and%20how%20much%20each%20versor%20turns%20from%20one%20frame%20to%20the%20next.%0A%0A%20%20%20%20**Before%20you%20run%20the%20next%20cell**%2C%20predict%3A%20which%20of%20the%20three%20distances%20change%20during%20the%20swap%3F%20Which%20versors%20jump%2C%20and%20by%20how%20much%3F%20(If%20you%20solved%20Exercise%202%2C%20you%20already%20know.)%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(np%2C%20plt%2C%20recording%2C%20t_rec)%3A%0A%20%20%20%20_d12%20%3D%20np.linalg.norm(recording%5B%3A%2C%200%5D%20-%20recording%5B%3A%2C%201%5D%2C%20axis%3D1)%0A%20%20%20%20_d13%20%3D%20np.linalg.norm(recording%5B%3A%2C%200%5D%20-%20recording%5B%3A%2C%202%5D%2C%20axis%3D1)%0A%20%20%20%20_d23%20%3D%20np.linalg.norm(recording%5B%3A%2C%201%5D%20-%20recording%5B%3A%2C%202%5D%2C%20axis%3D1)%0A%20%20%20%20_%2C%20_basis%20%3D%20segment_frame(recording%5B%3A%2C%200%5D%2C%20recording%5B%3A%2C%201%5D%2C%20recording%5B%3A%2C%202%5D)%0A%20%20%20%20_turn%20%3D%20angle_between(_basis%5B1%3A%5D%2C%20_basis%5B%3A-1%5D)%20%20%23%20turn%20of%20each%20versor%20between%20consecutive%20frames%0A%0A%20%20%20%20_%2C%20_axs%20%3D%20plt.subplots(2%2C%201%2C%20figsize%3D(9%2C%206)%2C%20sharex%3DTrue)%0A%20%20%20%20for%20_d%2C%20_label%20in%20%5B(_d12%2C%20%22%24c_1%20-%20c_2%24%22)%2C%20(_d13%2C%20%22%24c_1%20-%20c_3%24%22)%2C%20(_d23%2C%20%22%24c_2%20-%20c_3%24%22)%5D%3A%0A%20%20%20%20%20%20%20%20_axs%5B0%5D.plot(t_rec%2C%20100%20*%20_d%2C%20linewidth%3D1.5%2C%20label%3D_label)%0A%20%20%20%20_axs%5B0%5D.set_ylabel(%22Distance%20%5Bcm%5D%22)%0A%20%20%20%20_axs%5B0%5D.set_title(%22Distances%20between%20the%20cluster's%20markers%22)%0A%20%20%20%20_axs%5B0%5D.set_ylim(6.5%2C%2011.5)%0A%20%20%20%20_axs%5B0%5D.legend(loc%3D%22center%22%2C%20bbox_to_anchor%3D(0.5%2C%200.34)%2C%20ncol%3D3)%20%20%23%20in%20the%20empty%20band%0A%20%20%20%20for%20_k%2C%20_color%20in%20enumerate(%5B%22tab%3Ared%22%2C%20%22tab%3Agreen%22%2C%20%22tab%3Ablue%22%5D)%3A%0A%20%20%20%20%20%20%20%20_axs%5B1%5D.plot(t_rec%5B1%3A%5D%2C%20_turn%5B%3A%2C%20_k%5D%2C%20color%3D_color%2C%20linewidth%3D1.5%2C%20label%3Drf%22%24%5Chat%7B%7B%5Cmathbf%7B%7Be%7D%7D%7D%7D_%7B_k%20%2B%201%7D%24%22)%0A%20%20%20%20_axs%5B1%5D.set_ylabel(%22Turn%20per%20frame%20%5Bdegrees%5D%22)%0A%20%20%20%20_axs%5B1%5D.set_xlabel(%22Time%20%5Bs%5D%22)%0A%20%20%20%20_axs%5B1%5D.set_title(%22Rotation%20of%20each%20versor%20from%20one%20frame%20to%20the%20next%22)%0A%20%20%20%20_axs%5B1%5D.legend(loc%3D%22center%20left%22)%0A%20%20%20%20plt.tight_layout()%0A%20%20%20%20plt.show()%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20distance%20between%20%24c_1%24%20and%20%24c_2%24%20does%20not%20notice%20the%20swap%3A%20it%20is%20the%20same%20distance%20whichever%20marker%20is%20called%20which.%20The%20other%20two%20exchange%20values%2C%20because%20during%20the%20swap%20%22%24c_1%24%22%20is%20really%20%24c_2%24.%20A%20cluster%20whose%20triangle%20is%20equilateral%20would%20hide%20even%20that%2C%20which%20is%20one%20reason%20clusters%20are%20deliberately%20built%20asymmetric.%0A%0A%20%20%20%20The%20versors%20are%20more%20dramatic.%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_1%24%20and%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%24%20turn%20by%20nearly%20180%C2%B0%20in%20a%20single%20frame%2C%20at%20the%20start%20and%20again%20at%20the%20end%20of%20the%20swap%2C%20while%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_3%24%20hardly%20moves%3A%20the%20180%C2%B0%20turn%20about%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_3%24%20of%20Exercise%202.%20Any%20joint%20angle%20computed%20from%20this%20shank%20would%20jump%20by%20the%20same%20amount%2C%20and%20a%20low-pass%20filter%20would%20smear%20the%20jump%20into%20something%20that%20looks%20like%20a%20real%20movement.%0A%0A%20%20%20%20Detection%20and%20repair%20take%20a%20few%20lines.%20During%20the%20swap%2C%20the%20distances%20%24c_1%20-%20c_3%24%20and%20%24c_2%20-%20c_3%24%20move%20away%20from%20their%20typical%20values%20by%20about%201.4%20cm%2C%20the%20difference%20between%20them%2C%20while%20the%20noise%20moves%20them%20by%20only%20a%20few%20millimetres.%20A%20threshold%20halfway%20between%2C%207%20mm%2C%20separates%20the%20two%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(np%2C%20recording%2C%20t_rec)%3A%0A%20%20%20%20_d13%20%3D%20np.linalg.norm(recording%5B%3A%2C%200%5D%20-%20recording%5B%3A%2C%202%5D%2C%20axis%3D1)%0A%20%20%20%20_d23%20%3D%20np.linalg.norm(recording%5B%3A%2C%201%5D%20-%20recording%5B%3A%2C%202%5D%2C%20axis%3D1)%0A%20%20%20%20_swapped%20%3D%20(np.abs(_d13%20-%20np.median(_d13))%20%3E%200.007)%20%7C%20(np.abs(_d23%20-%20np.median(_d23))%20%3E%200.007)%0A%0A%20%20%20%20_fixed%20%3D%20recording.copy()%0A%20%20%20%20_fixed%5B_swapped%2C%200%5D%2C%20_fixed%5B_swapped%2C%201%5D%20%3D%20recording%5B_swapped%2C%201%5D%2C%20recording%5B_swapped%2C%200%5D%0A%20%20%20%20_%2C%20_basis_before%20%3D%20segment_frame(*recording.transpose(1%2C%200%2C%202))%0A%20%20%20%20_%2C%20_basis_after%20%3D%20segment_frame(*_fixed.transpose(1%2C%200%2C%202))%0A%0A%20%20%20%20print(%0A%20%20%20%20%20%20%20%20f%22Frames%20flagged%3A%20%7B_swapped.sum()%7D%2C%20from%20t%20%3D%20%7Bt_rec%5B_swapped%5D%5B0%5D%3A.2f%7D%20s%20to%20t%20%3D%20%7Bt_rec%5B_swapped%5D%5B-1%5D%3A.2f%7D%20s%22%0A%20%20%20%20)%0A%20%20%20%20print(%0A%20%20%20%20%20%20%20%20%22Largest%20turn%20per%20frame%20of%20e1%2C%20e2%2C%20e3%20before%20the%20repair%3A%22%2C%0A%20%20%20%20%20%20%20%20angle_between(_basis_before%5B1%3A%5D%2C%20_basis_before%5B%3A-1%5D).max(axis%3D0).round(1)%2C%0A%20%20%20%20%20%20%20%20%22degrees%22%2C%0A%20%20%20%20)%0A%20%20%20%20print(%0A%20%20%20%20%20%20%20%20%22Largest%20turn%20per%20frame%20of%20e1%2C%20e2%2C%20e3%20after%20the%20repair%3A%20%22%2C%0A%20%20%20%20%20%20%20%20angle_between(_basis_after%5B1%3A%5D%2C%20_basis_after%5B%3A-1%5D).max(axis%3D0).round(1)%2C%0A%20%20%20%20%20%20%20%20%22degrees%22%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20threshold%20finds%20exactly%20the%2015%20swapped%20frames%2C%20and%20after%20swapping%20the%20labels%20back%2C%20no%20versor%20turns%20by%20more%20than%20about%206%C2%B0%20between%20frames.%20That%20remaining%20turn%20is%20mostly%20noise%3A%20with%201%20mm%20of%20error%20on%20markers%207%20to%2011%20cm%20apart%2C%20the%20versors%20jitter%20by%20a%20degree%20or%20two%20from%20frame%20to%20frame%2C%20more%20than%20the%20true%20rotation%20of%20the%20shank%2C%20which%20never%20exceeds%2025%C2%B0%20%C3%97%202%CF%80%20%C3%97%200.9%20Hz%20%E2%89%88%20141%C2%B0%2Fs%2C%20or%201.4%C2%B0%20per%20frame.%0A%0A%20%20%20%20A%20real%20pipeline%20would%20use%20more%20robust%20statistics%20than%20the%20median%20of%20the%20whole%20trial%2C%20and%20would%20check%20every%20pair%20of%20markers%2C%20but%20the%20idea%20is%20the%20one%20shown%20here%3A%20a%20rigid%20cluster%20carries%20its%20own%20test%20of%20whether%20its%20labels%20can%20be%20trusted.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Other%20ways%20to%20calculate%20a%20basis%0A%0A%20%20%20%20An%20equivalent%20way%20to%20calculate%20a%20basis%20from%20vectors%20is%20the%20%5BGram%E2%80%93Schmidt%20process%5D(https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FGram%25E2%2580%2593Schmidt_process)%2C%20built%20on%20the%20dot%20product.%20It%20is%20implemented%20in%20%5BScalar%20and%20vector%5D(https%3A%2F%2Fmolab.marimo.io%2Fgithub%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fnotebooks%2Fscalar_vector.py)%2C%20and%20%5BFrame%20of%20reference%5D(https%3A%2F%2Fmolab.marimo.io%2Fgithub%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fnotebooks%2Freference_frame.py)%20shows%20a%20trap%20to%20avoid%20when%20using%20it%20for%20a%20coordinate%20system%3A%20it%20must%20be%20fed%20differences%20between%20markers%2C%20not%20their%20positions.%20For%20an%20anatomical%20coordinate%20system%2C%20we%20have%20to%20handle%20the%20vectors%20explicitly%20in%20either%20method%2C%20to%20obtain%20the%20axes%20in%20the%20directions%20the%20convention%20prescribes.%0A%0A%20%20%20%20For%20a%20technical%20coordinate%20system%2C%20it%20is%20common%20to%20use%20more%20than%20three%20markers%20on%20the%20segment%2C%20to%20reduce%20the%20effect%20of%20noise%20and%20soft%20tissue%20artifacts.%20With%20more%20markers%20than%20the%20minimum%2C%20the%20basis%20is%20found%20by%20least%20squares%2C%20with%20yet%20another%20procedure%3A%20the%20%5Bsingular%20value%20decomposition%20(SVD)%20algorithm%5D(https%3A%2F%2Fmolab.marimo.io%2Fgithub%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fnotebooks%2Fsvd_algorithm.py).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Checkpoint%20questions%0A%0A%20%20%20%20Pause%20here%20before%20the%20problems.%0A%0A%20%20%20%201.%20Go%20back%20to%20the%20three%20markers%20you%20placed%20on%20your%20forearm%20in%20Challenge%200.%20Would%20you%20move%20them%20now%3F%20How%20far%20apart%20would%20you%20try%20to%20put%20them%2C%20and%20what%20stops%20you%20from%20spreading%20them%20further%3F%0A%20%20%20%202.%20A%20colleague%20tests%20the%20orthogonality%20of%20a%20basis%20by%20checking%20that%20%24%5CVert%5Chat%7B%5Cmathbf%7Be%7D%7D_1%20%5Ctimes%20%5Chat%7B%5Cmathbf%7Be%7D%7D_2%5CVert%20%3D%201%24%2C%20and%20reports%20success.%20What%20could%20they%20have%20missed%2C%20and%20what%20would%20you%20check%20instead%3F%0A%20%20%20%203.%20A%20basis%20passes%20every%20test%20of%20length%20and%20orthogonality%2C%20but%20its%20determinant%20is%20%24-1%24.%20What%20went%20wrong%20in%20its%20construction%2C%20and%20why%20does%20it%20matter%3F%0A%20%20%20%204.%20Why%20is%20the%20centroid%20of%20the%20markers%20a%20better%20origin%20for%20a%20technical%20coordinate%20system%20than%20one%20of%20the%20markers%3F%0A%20%20%20%205.%20You%20can%20either%20buy%20cameras%20that%20halve%20the%20noise%20of%20every%20marker%2C%20or%20double%20the%20spacing%20of%20your%20markers.%20Which%20improves%20the%20axes%20more%3F%0A%20%20%20%206.%20In%20the%20simulation%20with%20moved%20markers%2C%20the%20local%20coordinates%20were%20exactly%20the%20same%20before%20and%20after.%20In%20real%20data%20they%20never%20are.%20List%20two%20reasons.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Problems%0A%0A%20%20%20%201.%20Given%20the%20points%20in%203D%20space%20m1%20%3D%20%5B2%2C%202%2C%200%5D%2C%20m2%20%3D%20%5B0%2C%201%2C%201%5D%2C%20m3%20%3D%20%5B1%2C%202%2C%200%5D%2C%20find%20an%20orthonormal%20basis.%0A%0A%20%20%20%202.%20Determine%20whether%20the%20following%20points%20form%20a%20basis%20in%203D%20space%3A%20m1%20%3D%20%5B2%2C%202%2C%200%5D%2C%20m2%20%3D%20%5B1%2C%201%2C%201%5D%2C%20m3%20%3D%20%5B1%2C%201%2C%200%5D.%0A%0A%20%20%20%203.%20For%20the%20points%20of%20problem%201%2C%20calculate%20the%20origin%20of%20the%20coordinate%20system%20and%20the%20local%20coordinates%20of%20the%20three%20points.%20Which%20local%20coordinate%20is%20zero%20for%20all%20three%2C%20and%20why%3F%0A%0A%20%20%20%204.%20Using%20%60segment_frame%60%20and%20the%20simulation%20in%20this%20notebook%2C%20find%20the%20smallest%20side%20of%20an%20equilateral%20cluster%20for%20which%20the%20median%20error%20of%20every%20axis%20is%20below%200.5%C2%B0%20when%20the%20markers%20have%200.5%20mm%20of%20noise.%20Before%20simulating%2C%20predict%20the%20answer%20from%20the%20results%20for%201%20mm.%0A%0A%20%20%20%205.%20Write%20a%20function%20that%20receives%20the%20positions%20of%20three%20markers%20and%20returns%20a%20number%20between%200%20and%201%20that%20measures%20how%20far%20they%20are%20from%20lying%20on%20a%20line%2C%20with%200%20for%20collinear%20markers.%20(Hint%3A%20the%20norm%20of%20a%20cross%20product%2C%20divided%20by%20something.)%20Apply%20it%20to%20the%20points%20of%20problems%201%20and%202%20and%20to%20the%20clusters%20of%20this%20notebook.%0A%0A%20%20%20%206.%20In%20the%20calibration%20trial%20of%20the%20example%20%22A%20landmark%20without%20a%20marker%22%2C%20the%20medial%20malleolus%20was%20at%20%5B%E2%88%920.005%2C%200.075%2C%200.065%5D%20m.%20Rebuild%20it%20in%20the%20movement%20trial%2C%20calculate%20the%20ankle%20joint%20centre%20as%20the%20midpoint%20between%20the%20two%20malleoli%20in%20both%20trials%2C%20and%20check%20that%20the%20distance%20between%20the%20malleoli%20is%20the%20same%20in%20both%20trials.%0A%0A%20%20%20%207.%20Repeat%20the%20example%20%22A%20label%20swap%20in%20a%20recording%22%20with%20the%20labels%20of%20%24c_1%24%20and%20%24c_3%24%20swapped%20instead%20of%20%24c_1%24%20and%20%24c_2%24.%20Before%20running%20the%20code%2C%20predict%20which%20distances%20change%20and%20what%20happens%20to%20each%20versor%3B%20then%20check%20your%20predictions.%0A%0A%20%20%20%208.%20A%20cluster's%20markers%20are%20at%20the%20corners%20of%20a%20triangle%20with%20sides%20of%208%2C%2010%20and%2012%20cm.%20Calculate%20the%20distance%20of%20each%20marker%20from%20the%20line%20through%20the%20other%20two%20and%2C%20using%20the%20reasoning%20of%20Exercise%205%2C%20rank%20the%20markers%20by%20how%20much%20their%20noise%20disturbs%20%24%5Chat%7B%5Cmathbf%7Be%7D%7D_2%24.%20Check%20the%20ranking%20with%20a%20simulation.%0A%0A%20%20%20%209.%20The%20whole%20plate%20of%20a%20marker%20cluster%20slides%201%20cm%20down%20the%20shank%20during%20a%20run.%20Explain%20why%20neither%20the%20distances%20between%20its%20markers%20nor%20their%20local%20coordinates%20can%20reveal%20it%2C%20and%20propose%20a%20measurement%20that%20could.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%2F%2F%2F%20details%20%7C%20Final%20answers%20to%20the%20problems%0A%0A%20%20%20%201.%20Rows%20%24%5B(-2%2C-1%2C1)%2F%5Csqrt6%2C(0%2C-1%2C-1)%2F%5Csqrt2%2C(1%2C-1%2C1)%2F%5Csqrt3%5D%24%20form%20one%20valid%20right-handed%20orthonormal%20basis.%0A%20%20%20%202.%20As%20vectors%20from%20the%20laboratory%20origin%2C%20no%3A%20%24m_1%3D2m_3%24.%20As%20three%20noncollinear%20marker%20locations%2C%20yes%3A%20their%20differences%20can%20define%20a%20frame.%0A%20%20%20%203.%20Origin%20%24(1%2C5%2F3%2C1%2F3)%24.%20Local%20coordinates%20are%20approximately%20%24(-1.088662%2C0%2C0.192450)%24%2C%20%24(1.360828%2C0%2C0.192450)%24%20and%20%24(-0.272166%2C0%2C-0.384900)%24.%20All%20second%20coordinates%20vanish%20because%20%24e_2%24%20is%20normal%20to%20the%20marker%20plane.%0A%20%20%20%204.%20About%209.6%20cm%20for%20the%20population%20median%20under%20independent%20isotropic%20Gaussian%20coordinate%20noise%3B%20simulation%20estimates%20depend%20on%20sample%20count%20and%20seed.%20With%20seed%200%20and%2050%2C000%20frames%2C%20a%209.5%20cm%20cluster%20gives%20median%20errors%20about%20(0.503%2C%200.502%2C%200.502)%C2%B0%2C%20and%2010%20cm%20gives%20(0.478%2C%200.477%2C%200.477)%C2%B0.%0A%20%20%20%205.%20One%20answer%20is%20%24q%3D%5C%7C(m_2-m_1)%5Ctimes(m_3-m_1)%5C%7C%2F(%5C%7Cm_2-m_1%5C%7C%5C%7Cm_3-m_1%5C%7C)%24%2C%20with%20coincident%20markers%20assigned%20zero.%20It%20is%20the%20sine%20of%20the%20included%20angle%2C%20lies%20in%20%5B0%2C1%5D%2C%20and%20equals%20approximately%200.577350%20and%200.577350%20for%20the%20two%20given%20triples%3B%20an%20equilateral%20cluster%20gives%20%24%5Csqrt3%2F2%24.%0A%20%20%20%206.%20Medial%20malleolus%20in%20the%20moving%20trial%20%24(0.450021%2C0.219212%2C0.064731)%24%20m.%20Ankle%20centres%3A%20static%20%24(-0.0075%2C0.0725%2C0.1000)%24%20m%3B%20moving%20%24(0.442968%2C0.222779%2C0.099010)%24%20m.%20Both%20malleolar%20separations%20are%200.070356%20m.%0A%20%20%20%207.%20The%20%24c_1%24%E2%80%93%24c_3%24%20distance%20is%20unchanged%20and%20the%20other%20two%20exchange%20labels.%20The%20normal%20%24e_2%24%20reverses%3B%20the%20original%20cluster%E2%80%99s%20%24e_1%24%20and%20%24e_3%24%20turn%20by%20about%2058.59%C2%B0%20and%20121.41%C2%B0%2C%20respectively.%20A%20rigid%20rotation%20of%20the%20whole%20cluster%20does%20not%20change%20those%20angles%3B%20measurement%20noise%20adds%20variation.%0A%20%20%20%208.%20Altitudes%20opposite%20the%208%2C%2010%20and%2012%20cm%20sides%20are%209.9216%2C%207.9373%20and%206.6144%20cm.%20Noise%20at%20the%20vertex%20opposite%20the%2012%20cm%20side%20has%20the%20largest%20effect%20on%20the%20normal%2C%20then%20that%20opposite%2010%20cm%2C%20then%208%20cm.%0A%20%20%20%209.%20A%20rigid%20slide%20preserves%20internal%20geometry.%20A%20separately%20measured%20anatomical%20landmark%2C%20bone%20reference%2C%20or%20repeated%20anatomical%20calibration%20can%20detect%20it%3B%20cluster%20distances%20alone%20cannot.%0A%0A%20%20%20%20%2F%2F%2F%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%0A%20%20%20%20return%20(mo%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Go%20deeper%0A%0A%20%20%20%20-%20%5BFrame%20of%20reference%5D(https%3A%2F%2Fmolab.marimo.io%2Fgithub%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fnotebooks%2Freference_frame.py)%3A%20frames%20of%20reference%2C%20inertial%20frames%2C%20the%20Gram%E2%80%93Schmidt%20process%2C%20and%20an%20anatomical%20coordinate%20system%20for%20the%20femur.%0A%20%20%20%20-%20%5BScalar%20and%20vector%5D(https%3A%2F%2Fmolab.marimo.io%2Fgithub%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fnotebooks%2Fscalar_vector.py)%3A%20the%20norm%2C%20the%20dot%20product%20and%20the%20cross%20product%20used%20here.%0A%20%20%20%20-%20%5BSVD%20algorithm%5D(https%3A%2F%2Fmolab.marimo.io%2Fgithub%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fnotebooks%2Fsvd_algorithm.py)%3A%20the%20best-fitting%20basis%20for%20more%20than%20three%20markers.%0A%20%20%20%20-%20%5BRigid-body%20transformations%20in%203D%5D(https%3A%2F%2Fmolab.marimo.io%2Fgithub%2FBMClab%2FBMC%2Fblob%2Fmaster%2Fnotebooks%2Ftransformation_3d.py)%3A%20rotation%20matrices%2C%20which%20take%20coordinates%20from%20one%20coordinate%20system%20to%20another.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Video%20lectures%20on%20the%20Internet%0A%0A%20%20%20%20%5BLynch%20and%20Park%3A%20rotation%20matrices%5D(https%3A%2F%2Fmodernrobotics.northwestern.edu%2Fnu-gm-book-resource%2F3-2-1-rotation-matrices-part-1-of-2%2F)%20explains%20orthogonality%2C%20right-handedness%20and%20what%20each%20matrix%20column%20represents.%20Relate%20the%20displayed%20axes%20to%20the%20marker-derived%20basis%20in%20this%20guide.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20References%0A%0A%20%20%20%20-%20Cappozzo%20A%2C%20Cappello%20A%2C%20Della%20Croce%20U%2C%20Pensalfini%20F%20(1997)%20%5BSurface-marker%20cluster%20design%20criteria%20for%203-D%20bone%20movement%20reconstruction%5D(https%3A%2F%2Fdoi.org%2F10.1109%2F10.649988).%20IEEE%20Transactions%20on%20Biomedical%20Engineering%2C%2044(12)%2C%201165%E2%80%931174.%0A%0A%20%20%20%20-%20%5BStandards%20-%20International%20Society%20of%20Biomechanics%5D(https%3A%2F%2Fisbweb.org%2Factivities%2Fstandards).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
9f6281c80666cfd652bf4380e6c6cf60