From d40fc9e0cffb90140c8e037266b11159169dcbd8 Mon Sep 17 00:00:00 2001 From: John Pearson Date: Mon, 15 Jan 2024 14:47:22 -0500 Subject: [PATCH 01/27] make sure pypi only runs on a tag of main (release) --- .github/workflows/pypi.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/pypi.yaml b/.github/workflows/pypi.yaml index 19b92eaf..70a200f3 100644 --- a/.github/workflows/pypi.yaml +++ b/.github/workflows/pypi.yaml @@ -2,11 +2,9 @@ name: Publish Python 🐍 distribution 📦 to PyPI on: - push: + release: tags: - 'v[0-9]+.[0-9]+.[0-9]+' - branches: - - 'main' jobs: build: From 035f1ab1554ec1563d85a77d403d17af4cba4848 Mon Sep 17 00:00:00 2001 From: John Pearson Date: Mon, 15 Jan 2024 15:45:47 -0500 Subject: [PATCH 02/27] reverting to pypi on tag push --- .github/workflows/pypi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi.yaml b/.github/workflows/pypi.yaml index 70a200f3..e1114677 100644 --- a/.github/workflows/pypi.yaml +++ b/.github/workflows/pypi.yaml @@ -2,7 +2,7 @@ name: Publish Python 🐍 distribution 📦 to PyPI on: - release: + push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' From 2cdcaa1cdf638b7bab72de9e45225bb5e790f3fc Mon Sep 17 00:00:00 2001 From: John Pearson Date: Thu, 25 Jan 2024 13:12:13 -0500 Subject: [PATCH 03/27] small docs and readme updates (#175) Closes #173 --- .github/workflows/CI.yaml | 4 ++-- .github/workflows/docs.yaml | 2 +- .github/workflows/pypi.yaml | 2 +- .github/workflows/test-pypi.yaml | 2 +- README.md | 8 ++++---- docs/installation.md | 30 +++++------------------------- pyproject.toml | 2 -- 7 files changed, 14 insertions(+), 36 deletions(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index de16e138..1f3002a8 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -25,7 +25,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -56,7 +56,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 94ba3e74..1d72c671 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -21,7 +21,7 @@ jobs: # Install dependencies - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/pypi.yaml b/.github/workflows/pypi.yaml index e1114677..12296a57 100644 --- a/.github/workflows/pypi.yaml +++ b/.github/workflows/pypi.yaml @@ -16,7 +16,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install pypa/build diff --git a/.github/workflows/test-pypi.yaml b/.github/workflows/test-pypi.yaml index e13ab432..0121a1dd 100644 --- a/.github/workflows/test-pypi.yaml +++ b/.github/workflows/test-pypi.yaml @@ -18,7 +18,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install pypa/build diff --git a/README.md b/README.md index 716d2d06..0770e6fe 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI](https://img.shields.io/pypi/v/improv?style=flat-square?style=flat-square)](https://pypi.org/project/improv) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/improv?style=flat-square)](https://pypi.org/project/improv) [![docs](https://github.com/project-improv/improv/actions/workflows/docs.yaml/badge.svg?style=flat-square)](https://project-improv.github.io/) -[![tests](https://github.com/project-improv/improv/actions/workflows/CI.yaml/badge.svg?style=flat-square)](https://project-improv.github.io/) +[![tests](https://github.com/project-improv/improv/actions/workflows/CI.yaml/badge.svg?style=flat-square)](https://github.com/project-improv/improv/actions/workflows/CI.yaml) [![Coverage Status](https://coveralls.io/repos/github/project-improv/improv/badge.svg?branch=main)](https://coveralls.io/github/project-improv/improv?branch=main) [![PyPI - License](https://img.shields.io/pypi/l/improv?style=flat-square)](https://opensource.org/licenses/MIT) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square)](https://github.com/psf/black) @@ -11,7 +11,7 @@ A flexible software platform for real-time and adaptive neuroscience experiments _improv_ is a streaming software platform designed to enable adaptive experiments. By analyzing data, such as 2-photon calcium images, as it comes in, we can obtain information about the current brain state in real time and use it to adaptively modify an experiment as data collection is ongoing. -![](https://dibs-web01.vm.duke.edu/pearson/assets/improv/improvGif.gif) +![](https://dibs-files.cloud.duke.edu/sites/default/files/improvGif.gif) This video shows raw 2-photon calcium imaging data in zebrafish, with cells detected in real time by [CaImAn](https://github.com/flatironinstitute/CaImAn), and directional tuning curves (shown as colored neurons) and functional connectivity (lines) estimated online, during a live experiment. Here only a few minutes of data have been acquired, and neurons are colored by their strongest response to visual simuli shown so far. We also provide up-to-the-moment estimates of the functional connectivity by fitting linear-nonlinear-Poisson models online, as each new piece of data is acquired. Simple visualizations offer real-time insights, allowing for adaptive experiments that change in response to the current state of the brain. @@ -19,13 +19,13 @@ We also provide up-to-the-moment estimates of the functional connectivity by fit ### How improv works - + improv allows users to flexibly specify and manage adaptive experiments to integrate data collection, preprocessing, visualization, and user-defined analytics. All kinds of behavioral, neural, or modeling data can be incorporated, and input and output data streams are managed independently and asynchronously. With this design, streaming analyses and real-time interventions can be easily integrated into various experimental setups. improv manages the backend engineering of data flow and task execution for all steps in an experimental pipeline in real time, without requiring user oversight. Users need only define their particular processing pipeline with simple text files and are free to define their own streaming analyses via Python classes, allowing for rapid prototyping of adaptive experiments.

- + _improv_'s design is based on a streamlined version of the actor model for concurrent computation. Each component of the system (experimental pipeline) is considered an 'actor' and has a unique role. They interact via message passing, without the need for a central broker. Actors are implemented as user-defined classes that inherit from _improv_'s `Actor` class, which supplies all queues for message passing and orchestrates process execution and error handling. Messages between actors are composed of keys that correspond to items in a shared, in-memory data store. This both minimizes communication overhead and data copying between processes. diff --git a/docs/installation.md b/docs/installation.md index 0d388295..dc9eeb42 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -7,11 +7,11 @@ The simplest way to install _improv_ is with pip: pip install improv ``` ````{warning} -Due to [this pyzmq issue](https://github.com/zeromq/libzmq/issues/3313), if you're running on Ubuntu, you need to specify +Due to [this pyzmq issue](https://github.com/zeromq/libzmq/issues/3313), if you're running on Ubuntu, you _may_ need to specify ``` pip install improv --no-binary pyzmq ``` -to build `pyzmq` from source. +to build `pyzmq` from source if you're running into ZMQ errors. ```` ## Optional dependencies @@ -48,13 +48,10 @@ Currently, we build using [Setuptools](https://setuptools.pypa.io/en/latest/inde For now, the package can be built by running ``` -python -m build -``` -from within the project directory. After that -``` pip install --editable . ``` -will install the package in editable mode, which means that changes in the project directory will affect the code that is run (i.e., the installation will not copy over the code to `site-packages` but simply link the project directory). +from within the project directory. +this will install the package in editable mode, which means that changes in the project directory will affect the code that is run (i.e., the installation will not copy over the code to `site-packages` but simply link the project directory). When uninstalling, be sure to do so _from outside the project directory_, since otherwise, `pip` only appears to find the command line script, not the full package. @@ -74,21 +71,4 @@ Then simply run ``` jupyter-book build docs ``` -and open `docs/_build/html/index.html` in your browser. - -## Getting around certificate issues - -On some systems, building (or installing from `pip`) can run into [this error](https://stackoverflow.com/questions/25981703/pip-install-fails-with-connection-error-ssl-certificate-verify-failed-certi) related to SSL certificates. For `pip`, the solution is given in the linked StackOverflow question (add `--trusted-host` to the command line), but for `build`, we run into the issue that the `trusted-host` flag will not be passed through to `pip`, and `pip` will build inside an isolated venv, meaning it won't read a file-based configuration option like the one given in the answer, either. - -The (inelegant) solution that will work is to set the `pip.conf` file with -``` -[global] -trusted-host = pypi.python.org - pypi.org - files.pythonhosted.org -``` -and then run -``` -python -m build --no-isolation -``` -which will allow `pip` to correctly read the configuration. \ No newline at end of file +and open `docs/_build/html/index.html` in your browser. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2cc99c28..044829f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,8 +35,6 @@ classifiers = ['Development Status :: 3 - Alpha', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython' ] dynamic = ["version"] From 5682eeb82b9a8acd3cd5a9960e5cef308efc7f3e Mon Sep 17 00:00:00 2001 From: John Pearson Date: Sat, 17 Feb 2024 10:25:21 -0500 Subject: [PATCH 04/27] adding improv logo (#176) --- README.md | 12 ++++++++---- docs/_config.yml | 2 +- docs/improv_logo_horizontal.svg | 21 +++++++++++++++++++++ docs/improv_logo_vertical.svg | 21 +++++++++++++++++++++ docs/logo.png | Bin 9854 -> 0 bytes 5 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 docs/improv_logo_horizontal.svg create mode 100644 docs/improv_logo_vertical.svg delete mode 100644 docs/logo.png diff --git a/README.md b/README.md index 0770e6fe..702425c2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# improv + + + +Adaptive experiments for neuroscience +--------- + [![PyPI](https://img.shields.io/pypi/v/improv?style=flat-square?style=flat-square)](https://pypi.org/project/improv) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/improv?style=flat-square)](https://pypi.org/project/improv) [![docs](https://github.com/project-improv/improv/actions/workflows/docs.yaml/badge.svg?style=flat-square)](https://project-improv.github.io/) @@ -7,9 +12,8 @@ [![PyPI - License](https://img.shields.io/pypi/l/improv?style=flat-square)](https://opensource.org/licenses/MIT) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square)](https://github.com/psf/black) -A flexible software platform for real-time and adaptive neuroscience experiments. -_improv_ is a streaming software platform designed to enable adaptive experiments. By analyzing data, such as 2-photon calcium images, as it comes in, we can obtain information about the current brain state in real time and use it to adaptively modify an experiment as data collection is ongoing. +_improv_ is a streaming software platform designed to enable adaptive experiments. By analyzing data as they arrive, we can obtain information about the current brain state in real time and use it to adaptively modify an experiment as data collection is ongoing. ![](https://dibs-files.cloud.duke.edu/sites/default/files/improvGif.gif) @@ -33,7 +37,7 @@ _improv_'s design is based on a streamlined version of the actor model for concu ## Installation -For installation instructions, please consult the [docs](https://project-improv.github.io/improv/installation.html) on our github. +For installation instructions, please consult the [documentation](https://project-improv.github.io/improv/installation.html). ### Contact To get in touch, feel free to reach out on Twitter @annedraelos or @jmxpearson. diff --git a/docs/_config.yml b/docs/_config.yml index e3881a7a..6398470a 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -8,7 +8,7 @@ title : improv documentation # The title of the book. Will be placed in the left navbar. author : improv team # The author of the book copyright : "2024" # Copyright year to be placed in the footer -logo : logo.png # A path to the book logo +logo : improv_logo_vertical.svg # A path to the book logo # Force re-execution of notebooks on each build. # See https://jupyterbook.org/content/execute.html diff --git a/docs/improv_logo_horizontal.svg b/docs/improv_logo_horizontal.svg new file mode 100644 index 00000000..57f190b2 --- /dev/null +++ b/docs/improv_logo_horizontal.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/improv_logo_vertical.svg b/docs/improv_logo_vertical.svg new file mode 100644 index 00000000..9d17a96a --- /dev/null +++ b/docs/improv_logo_vertical.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/logo.png b/docs/logo.png deleted file mode 100644 index 06d56f40c838b64eb048a63e036125964a069a3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9854 zcmcJ#_ghoX7cGn*K?4W`P^xr9dX-2=s)7_L0g)Pd2}GoYu8}Goq=uqY=^(vJ3q7D9 zgx&ncihTY58>yQhyHVAq6+lGO~MVagOauq5m9v<`6Yyea8LU7g^33d5#6JIpIaLG z-1|gCJhU3BN``QY-K@=)`@JW9M^t~b7uLWQYei|mDOJQ5UUg0~vIqbGt~C8wn@$P% z5pl~zRjG%ihlB(HjNnDEe-dDz2JixSk(`6?==a`q;3sz5kB=vYkB^Usv)VI9k21rD zJiTz9qguh+nKE8m#+pE4C16PIb7Iqf7uN3q_3Quydk+ycREf|Kaf=g!AT$7Pt5%T^ z8aVDmSdkMNlHc+OU`GfMo(G6M`@b>}|7lC2WNZAX;dG{O$?=D%iOq{^-7HrB zcK+ZB)!$jy15VseoVKDTyWDkjAxOOZ*QX8d#=@20sP;i@Xow95<_tP$?zwjiu96e z>w=n(W1oxV>vfZL+?;M$rHrbr-j~Q4Z0#f{{?B_kL)V~b;Ypj(r`bl+t51=jl6us% zisP0c%+gd*@NjHx-9P?#e$1%)t<{43ZZ7psNeO=)Y*C@keuU`+V-r`LF5ytZC}IBu zbG$h|;({qNsYw+4y*-`g9}w-)-1o;4J-XQ1@Hi(x-*u)|Ne#p@^B%N%Ah2Q;LCG(ZHBQFL?Li-e*gAW=zAB)SnqFmSqA*R7HO(vfNpkV`XWrI=Kemp{ z`XTgmXPPHd1fbknj1MsBI};8fOdfpI;lh4^A@)#qeVu zJb2)IeR*!gAy`Wti~X5b#3bXH#-tDsvNcs{`2~3O>45+@w=lsB@yx}N+7A*AT1iWo zCLk(xWbfP7ppKMjUV$FTMNv+WKG*ZuS~AGj-P2i^ah`gNkqv6jA-Y4>M+bYJ1ouAM zhio_#B1U3u6!)N$?mJ2ZbANVV>Xl|5+3DVVOU$d89?>$dF>ix#X2Zv>SuCqqWS!S> zomY&g)au`g*ER&}`dKnw-|KyL(Xv>>BAvCz#~c7<2z4i2S3Ea{s$tl4;Fmfrw5uQ6 zdZdFouB9Zn$Ox^;m&3&jBs=B z%AGu-+-3>0hu*7*Go%ig0F*a+}bQ z8Cc)R_4Xa}T)gvvu4H@GAS#AA)o|_JsNW8zdTY`Y2EMw$8M6iKf6zLo2}vX5#E`E8 zLfTXySbqo;)cm*vz`Y<&q8-QUpyBxlw(wEG~e8ar+}fF-S+sb& zDz})rHK~=qsT-W;2Plhi{U0Y!6S$rmj%Lf#_6Q{}o9ON=pa2rAPpn&9&e(qYe-t*V z@vGCn-F!I$@0)jPw(#1-v_r^JT~5YZW{`r1+085#GB%6IJC?RRv%5q~F>%c&OxynZ z%xYjt7MVY0BPNAf>4{^p{XbrcwEclTApV;6FC515Ns#UfLZ z2YrA=|5>ly8F0Bp+l*6!<>1heHsIR01D`C08YNKz!~*JpVLU<@0QpHnTUW{;>sYp= z#eU02VX*}f3vp`~7iJXDzx9yXd^Up*0-yHrbarsvW*UH%P49|4$4@)tJgR$?6o6f5 z(}}v|BxI|mgea@2>qg^b#ozLf17HU@5Fa-Fraz@QN%7lZ5lq)Vn7Q=hZ-RdZ+wFlD zJdw!7J1*GND#_)ys*=%^U*Zr0;^&s<^_q%ds6d3-IC~_amnNV&0xM^1;`=@1xFn zORQ(tnI35sf7lD%W&%N9E6Y~6qoNr#BAw2aiDiR!7CS8KoW|9)GoJAAS##ZIrQTUl zBW}q?kb$Tk4JP=7j@W0(Ea^R`$D>gU%nfa^3Dwub5~JS+2Q@c7Z9!yGJ7`P^5kIj$ zg3O{je@?Kt&v;;R&~$K48WR=L6GcxNIc4yw)BgJ8Bb7oLJ2X_3X0F)>>o%A^0m7n(dq+!+P%cBi)cl|I6CzcZF{kC1jgSv|j(+zAw~tn#IxO6lUd zj3V;e_C=~at8H=>ZGE#9<8QfP>BH9b3(Dp1zuXnN-uc&csJ#%8Q4@!2to4XneWRff zIaA{h=OO9fyAt`B20gSGZE0+5EGtCJ!AS6zFb)b4RvV0{cMY(`Y<6f+_ign{;I9w2 z?`CxPvQXRE34SVHT0>{c&%(Dx6)wu&)H)_KU+lGLizO9h`wd3$Z?JRA2VVyyEvYkH zj67X5JX#+y_;{BJ&ZZ+qCX?k*~w*V_4;9w2D`5E~7T0 zi3~~~jxu(te?CA<_w_{5#uzKO&O9+lj}F{x+F-4r%K9((FwF&kFVse6mP!vDt_>x9 zUx>VaMt_HzSdkN>rs`F|pR*{TM8rR(unZk0EXqrLLqyw#%|A#KhSQVT6e&4@$gbX?Lg3SMXEj8wDG)D;FDQ8q8%^qqz=<=X1rcVpN?A{w?-*WMkRlJWw z3+-+`iUdC0c&rucqhLSGU;|L-v$MoCUgN^3b8#YnlqTL^wOuSldYIkFOhc9*s!|7C zZCfJ4{p-Vci9_o*vV1IliLv_qg!ONlaCFU*O(&by>`lq|I z4hpOFuCt)IyVtJs&2^hgfhWI>P3B?isG8c+o4E?=CTZWp{P7tyGpzM%&=GR+HEzQq z;QD++XUMPpY$fV5j+c40`CQ?TIK@slTaYNrn;_-|+;Pj|71}f4JSG4)@AI{Y``0;x zLIAw$!n>UCW{q*q4}sT5IX7vGytw4;e6H4@D|~;@%gt~92mAUO5uvgxOB5{Ep(ELz z2y^2geQ@Ae;wJm&>(%ce!yCWuih%7rn$vb`hZwIICxbe)vwUsJ`2COHc=-h!)ol1J zae_fdeqQ#!4Z#;G@7#18om~t^Dkw@;k|8CYnl4`WYjT=O>;V$I*8CVeKahu}oThzI zby8mM|V!o$r$h~2x@YYgecvv)44 zVEmn@-71;kO#&Fe!dj}OTkMCigz^2|hDFfuhsUY!IpqW^Rup!q8D*X-)f`dZYB%V( z+J*fl9B5WrGvpO-E^E$NZT%lAFfaIUD6e?hS2V7Wc__#^DX?+UE*yzRce_CIV*Ig- z{@Au>EMGnM(+{WpNRX7+Z+dydzM}QZc1KP7jO|yav-WKD37#31CiIdmppsvasoZjJ zhjMmpSbHGVq~5PpJY6V>>3^|%q!?r@6j>j<0Q>N?Q0ng{%+Hv@V2buU<19&o4fYJ3 zRGPrfikVAIeWWMdn<`28#Px;wwVB2fJsK(!YG{yS2zy&s*m4tR82lID$;!4LN{)!y zpw)6_si`@A8LBejn^g}GjhqlZDAg{@exDRYNd-JHnKP1SG{R>+AL4dGabfD5bgVHmK*ej73ye~XLd@pb%2zq%8KX$Hu7^Z0-YJ;>P` zMz#ko5|<$pp_sI$-oYj5Rh=9)S^tdB2NesZ#!D?}dqn52D&U`j+g9hxWVkkYBdn4% zc1N30W|e88l8+P(9tA)ET&$81!y6FlDYdS0di~KK=i%a0-3?AToyPe^X?C+|Opi&` zbYC5`m0#x7y;R^{@3?t`@KHC#yOZy27qg$X^7DX*k*Y3=r*l?48H^0m@Zwspa2w!= z8Rzo~D@*^~I(w;37S?CAu65^aOXttu1t0tLL~jVQR1W5BCjS95x7_>(Zn95tLXtC* zAm8pA%*Ozxz$w46`Mo9f*vB&x+b$=u+Rs;z6TfMxU!%gWE+ByCzxygTL4BDhyv1d} zD{w^)?0bgm#ph9M!q4%-kFW6k$paVLINgXgd}#yNe44b#J%%(m=NxxGP{<*vw)MiW z6(l~^rYnHK%O&5WXN!98Ny<2ab6T^H9CrHXik+$sMot2o2Lo_hnsKuJwz^8h{%eED zr2qYWAmxXK|7vS?)YGY#yL&+^&tb?CV%7@vu~e0v#UWvx>Telu)*eDs26wvKAAXce ztkMG*S4qdZc!ua^$*k4(E6U{?$oIskaZkqURK+y3u8q`gKrVl;*Kr~!{`;%OR=SeB ztg)-z=sVw9%gUM?9nbAWb9|%m;w8yNvf{LmJDY1OcB@=q+=4Avtsm1NlJkLj{9Zl{ zRC#RkkoY@`MSlwW&pUEARg2XK0BFF<0#Y;GEnlJE-CR#m7hqrV%3DU|i@%R^k^PBt zL9=sbeO)J@Xjbkq>qG>iL5LcW_dHMcNq-6n&mRKy6!O?ZPQK4yWR5ho z{xPlMGn^?DBvWZmb@A?AY_C}N(gWxoy$S=QS5eTZZNYtHt5=3oKWhj+ zaI1UQC{CL^LFo3hB0Yv-r9m2lrMlI5bVANzvQRAEKf+Mm4kO*IX;k1Odq94dXD2Rs zWX}=%8D2#S>f=WSng80ZNRQ|oV9VtCLzT;8yEMCSo9+)@Kf$MS9rA)R#TWwx9jD+W zi=Qw0Y3I9G%tn(aT>or~nGrp+uA%epd$M7pH7C*qpS6v-koOaxCl@1mMC(q!bC(s) zUgY#LBuJW)rT0r8sRU(ABiG>{p%6yPkp?S?iJpV=r}PnKq7z9&)n=Xca+&dPn@b(& ze@Ry7r&SX0G7$e$1tfQ_eZVwx$s~3PWZ`c=&{$USD7g>1-9MIsOBgfi&|QFmfGN66 z9#c7bCn;+>Nxde;IuKZxgr+*ZjS~=78Slptsx0B zC(yjsMZl}YK{pqR$m-cI5O-qwWr{63uC0&=YTvT9y zqo$r(5f9TobpUqSOOL1pntof&8#PdLxxmJ+JLjGv#)W(sdt|m&Pg~Ei>X{9WRM-FL z1ug=$A>CFfx;j-KXvS4L_(V+6QyE%^N?!-xBP2BBQ&_ebDIcw^RMMR1W|7&hYIg>2|Km|AZ``=`r~-Lr_^!%2k1B)uvrP6qZ4d`6mPBN>!xZ zJk**bYPy$w%2j60-W`={AU!!oHcBpiucFvTp~*34H)rMJxvaCFizQ$>@9ORE9?hrY z_T-JG%a{$#O{{Y(Q@I#^@Irxli=@R7a-z_}8Dgl&lu4==oQh=QPkJP(*KtS8U5075dF7 zk<6-UO^#Ci_8qvBD}L=^EXWWqC7Ki;}V;^B#BGlT;7cAwRaC& zbWyAQj{@gNy2Gix);R!v_O^)R%4Ip-S@)aIy3x`iPB(2_!mn0a%vs>k4@ENe8|dXK zHIjH9)!DsyQ_armEk(s_CL(LDUW*)7qrAO%P<3Cqijj$(X$*6}#_C8^v1VmCWJ3)T z|NZ4>M2bedT9pKY4Q7bvkLx|;@_%6ztsBvH1rl^a`}A}Jw($PS)0VjqRNGVbvSsj1 zeq5c?zFG;a$eXVSb{-Qhrt$Ln4+G5@1M_i1Fd>I!(Z%Ryk{|<_Z0^nWo_yDc#qZRN zW*Uzoe6ISr;?i&$?@WdN7*w?_e&Bt%7FO_$#Pp-o%+Bmb?YO52TFJ_X=Y` zB79{;AGE8w(H+`M-ReL&@+8qpOiubk(5AfNWE$Iqg?mQ0-sS zlV5}N6=QWM-DmG5T$?m-d0zL9tvkD61+==-ZvvE}&!_HEa);T%|5iUNQgr??Arj2v zYeVDHiT2)^j`CqWEdiHi8rN_or|xDgsM^V2=a8S%L1RY)zkF1Bo+rlV-5C~88P38p z>}G^G6k1xQ5BXO3n!~$(MW8-5Y&VdjIRXZ``{U*C+40wek?4&Psi$FSNhg8N zU>DZ?ITJ2d!p5txmKpAB-_hjqgY3%%Myhw3#rRoHotWKDxD&LqP=@Yh^miCTC#uU( z=E!Jmu<%zp1u}I+JV)@$hXbDq!nQdddw1iD+p{!H2R#miIqW_TAeZvSG>?CwQDl<= zq&r!EMjYv>v=zr(%WfQ4B|0sk7UEOk!}O@D@vX9{>t{X+!7w~tYw!I{;N8C%TN>!M z>7xX?(GH%vZg|!Xp4X8E(FQ-T=0g3Wlt`Tf|0;>yF&gVyM`s}om7?7plxL!q;@a!V z{l4{qolEEroMw2oJNmp@)G2ljpK|T#-8cW*{bS2K$Q!%h%5Qx>Yp}*|3=`1IrGYA_ z#3pFJc%b*?vw(G9JA{OJs6Iu?03Z#!9|dMV4WKd?L9VWXkJ|UY=bg3AH;sIrwQD;I zc&6=zrtXy=AOQHjS}Aa=9}Kkvkysnn7oOmLzpHuu&^6N3?IQXpetcqqXIvVbh9q;e zZ*y5xQ0j@_UR5}&q#}Q}_z?g~1NZ0~DoWtg{a2c3{5#i|(VB{zs7lh{ns-0}39+eZ zmuodiGS}9p!Cll;j;+GMld@E%{}vT(vQ^7~sZyUydd{}xs*Gvp`d6JIiVu(*_EN9q z$Qn5T>zL^jWs2J*dS)WbaTymtJNWt7SCtZNBxs!#HlJZ0Xj4IzF#0FmKaa9WFj*t^ z4$IrEQwQer_l3e3LFZ0uR?w~Qj8tBUW5aL8_8yvFiQH_QgmCjr9apD6&DIQX(SNWv zb|F@%eNq*cn1*MdhziznN^XqbD54Rd`f42e z%dkRxE0pw|k;g*Kxz=~~Q^d%iQS|mdZfV%PFuTgKOoQQ2B*Db7CQ{U+%(vqjr2@+)eLf{cZscW-ddJS@qnyU6i*!6DF%fms`AZv@>Z`K>M^jl7)Jy^-; z(0WW!4OGD=qRpx%OsLesm*9)M|LH&G?IjJgE9N?vI~25Tc)^B;`$p7s5P+z)rqsu8 z#LN*-*k4E7rlzJtJp0n5I7ds<0+d9DE{E_46|Ejr244-$k>h0krj6YhP4oX0jS7JghDO3@cFKC#AZ`8L30t0U8)M^2*m&M6}$Qp~Q_wmx(G zn(!n3Y)O0=4MK=5P0>QQfvJ@cAL_pnWzfF4g)K|wu=I~FYX(3W-2 zf|@^YU!Ut&*_K+D;p+qF5BVv9?k(CLxvwrM?*$1Y8Q1rO%po-&x{`*9F<>gKc8C(qtBR&?*4F>(;9=xmQap z#=6YaIOw962LhIK=$)s(7ibVg-6j|qt}Gf?spXuC?|60jPfUv_x01+;B7RU=)jPnT zh|?|SxQAbf65$EmPTvdZzt8N+PABxnwrkm)Y?Tga)nYIMgoc7w4XzRV2$`%ex6z9bNU zTv2t^8?QF3hskl_jm7(RNCWicsx?yw57HN%DNW%~m{)QN=KZ8m6{!i#T2h!Xg3@O2 zaAK4htobm}2YP9pB2afR<%OFoY%oDA4Bs?^mtDV=I(pY}zRp|(4qBFfrFG}vZP7x$ zMB$pC$#-tpQDWYIddI0^+71N$Q1U1_4^iys=?2}s!KPO2!JcD*HVg#F{oEWIe*nU-%yV<;YJz}09_`Dz?AWLlKA$!=5B7tkp z9_Ihe&3$NL+wtF@TpDvLR)ihayRpLvH0}o-Zvte|uU@(+0lWT*aU3ZK?GKTLYcJCO zQ~U7QT6{r3c?3TzU|gX^V}%M%XI;xd_qK-&M9KdV1Sos|8}%17JACDbM!iDforMt* z7Auxhgv1PQq~6FDWuVifhd=4`Vl2Xr#vI%}S{cQRAwGNOF9zF0RTH&w_q#Z%t4 zGx*92|7e3Cd$W~Y2_l4SU!I)$Ol%$mL(dkfgnhfk3#n<-t!kX(JFLhS_|%>=Fst7u zheXTT)Vo^bsf*`klEo@*>S0f;%3gz^+m_^rcv(QLan(?cKx9RG{g^Ez;QtBl6Ph+saou0`c!| zI8XcO^OnR(X{|3YvOxJr%@#4@tTR!0O7_qzZS`-=iZ3mwL3@LwASi z(QvV}`KN5>8$=N+@p9IR8VfQdvSZ+Lf{Fv;$%v%_0s%+RBoafg; zB^upVY;ewq1QLHeD4y<6Oa4d6hgbOmM;(hw8Y(3@UM)XV_nC91+I{svghGcwL9DQC zyM&5PhT=%Y7C{j)JyC3slsF1h)J!2ju6cNc?HhXJLHl25entk$v!XYOzK=(rP~Rh! z*p(T?yl4h)l~Mkkoa1>4%y{DERdSd$UE=vGXLs>#A3q)Cuz$F)ey2S&b!HYf=Mm@i z$vBfjX|diF=}~}Se`0d%u`^s!Jiz*S>KK$b5mKnTyL{tReI0e>|9%tuAFB^Xu1EqI z2*~6xoM8t-esZMcNE3x1OqyN-Lp+FtX23bZdIhv1I_L3poeEE12w+x`r3BtK?UgS_ zgjv%6!;CN9dDzM}v3-DxU~SIgW9;-IvqR%OnBm4Ejqg0G}YnQC~*J|RZ=pB5-~vu$W~ z)Un>6^zlydKLzhIZ?b;=zuG68MC1PzKM`{<{lBe>Vh5EBTCLDuB`X-584ml0{G L>8MsHTOs~GC;Fmw From 01e08740cfe86645c7e40768623a1cf57f2d4948 Mon Sep 17 00:00:00 2001 From: John Pearson Date: Tue, 13 Aug 2024 14:58:47 -0400 Subject: [PATCH 05/27] fixing broken image links --- README.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 702425c2..53ab0500 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,17 @@ - - - -Adaptive experiments for neuroscience ---------- - +# improv [![PyPI](https://img.shields.io/pypi/v/improv?style=flat-square?style=flat-square)](https://pypi.org/project/improv) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/improv?style=flat-square)](https://pypi.org/project/improv) [![docs](https://github.com/project-improv/improv/actions/workflows/docs.yaml/badge.svg?style=flat-square)](https://project-improv.github.io/) -[![tests](https://github.com/project-improv/improv/actions/workflows/CI.yaml/badge.svg?style=flat-square)](https://github.com/project-improv/improv/actions/workflows/CI.yaml) +[![tests](https://github.com/project-improv/improv/actions/workflows/CI.yaml/badge.svg?style=flat-square)](https://project-improv.github.io/) [![Coverage Status](https://coveralls.io/repos/github/project-improv/improv/badge.svg?branch=main)](https://coveralls.io/github/project-improv/improv?branch=main) [![PyPI - License](https://img.shields.io/pypi/l/improv?style=flat-square)](https://opensource.org/licenses/MIT) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square)](https://github.com/psf/black) +A flexible software platform for real-time and adaptive neuroscience experiments. _improv_ is a streaming software platform designed to enable adaptive experiments. By analyzing data as they arrive, we can obtain information about the current brain state in real time and use it to adaptively modify an experiment as data collection is ongoing. -![](https://dibs-files.cloud.duke.edu/sites/default/files/improvGif.gif) +![](https://dibs.duke.edu/sites/default/files/improvGif.gif) This video shows raw 2-photon calcium imaging data in zebrafish, with cells detected in real time by [CaImAn](https://github.com/flatironinstitute/CaImAn), and directional tuning curves (shown as colored neurons) and functional connectivity (lines) estimated online, during a live experiment. Here only a few minutes of data have been acquired, and neurons are colored by their strongest response to visual simuli shown so far. We also provide up-to-the-moment estimates of the functional connectivity by fitting linear-nonlinear-Poisson models online, as each new piece of data is acquired. Simple visualizations offer real-time insights, allowing for adaptive experiments that change in response to the current state of the brain. @@ -23,13 +19,13 @@ We also provide up-to-the-moment estimates of the functional connectivity by fit ### How improv works - + improv allows users to flexibly specify and manage adaptive experiments to integrate data collection, preprocessing, visualization, and user-defined analytics. All kinds of behavioral, neural, or modeling data can be incorporated, and input and output data streams are managed independently and asynchronously. With this design, streaming analyses and real-time interventions can be easily integrated into various experimental setups. improv manages the backend engineering of data flow and task execution for all steps in an experimental pipeline in real time, without requiring user oversight. Users need only define their particular processing pipeline with simple text files and are free to define their own streaming analyses via Python classes, allowing for rapid prototyping of adaptive experiments.

- + _improv_'s design is based on a streamlined version of the actor model for concurrent computation. Each component of the system (experimental pipeline) is considered an 'actor' and has a unique role. They interact via message passing, without the need for a central broker. Actors are implemented as user-defined classes that inherit from _improv_'s `Actor` class, which supplies all queues for message passing and orchestrates process execution and error handling. Messages between actors are composed of keys that correspond to items in a shared, in-memory data store. This both minimizes communication overhead and data copying between processes. @@ -37,7 +33,7 @@ _improv_'s design is based on a streamlined version of the actor model for concu ## Installation -For installation instructions, please consult the [documentation](https://project-improv.github.io/improv/installation.html). +For installation instructions, please consult the [docs](https://project-improv.github.io/improv/installation.html). ### Contact To get in touch, feel free to reach out on Twitter @annedraelos or @jmxpearson. From 79efa4700df93b9b346829327f64de8a0781f0a7 Mon Sep 17 00:00:00 2001 From: John Pearson Date: Tue, 13 Aug 2024 15:18:43 -0400 Subject: [PATCH 06/27] fixing test formatting --- .github/workflows/CI.yaml | 2 +- test/conftest.py | 2 +- test/test_actor.py | 8 ++++---- test/test_cli.py | 6 +++--- test/test_config.py | 2 +- test/test_demos.py | 4 ++-- test/test_link.py | 22 +++++++++++----------- test/test_nexus.py | 8 ++++---- test/test_store_with_errors.py | 4 ++-- test/test_tui.py | 6 +++--- 10 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 1f3002a8..032710d2 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10"] os: [ubuntu-latest, macos-latest] # [ubuntu-latest, macos-latest, windows-latest] steps: diff --git a/test/conftest.py b/test/conftest.py index 5bbdb552..e314b5ee 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,6 +5,6 @@ store_loc = str(os.path.join("/tmp/", str(uuid.uuid4()))) -@pytest.fixture() +@pytest.fixture def set_store_loc(): return store_loc diff --git a/test/test_actor.py b/test/test_actor.py index 0745aa69..4c7b3987 100644 --- a/test/test_actor.py +++ b/test/test_actor.py @@ -13,7 +13,7 @@ pytest.example_links = {} -@pytest.fixture() +@pytest.fixture def setup_store(set_store_loc, scope="module"): """Fixture to set up the store subprocess with 10 mb.""" p = subprocess.Popen( @@ -26,7 +26,7 @@ def setup_store(set_store_loc, scope="module"): p.wait() -@pytest.fixture() +@pytest.fixture def init_actor(set_store_loc): """Fixture to initialize and teardown an instance of actor.""" @@ -35,7 +35,7 @@ def init_actor(set_store_loc): act = None -@pytest.fixture() +@pytest.fixture def example_string_links(): """Fixture to provide a commonly used test input.""" @@ -43,7 +43,7 @@ def example_string_links(): return pytest.example_string_links -@pytest.fixture() +@pytest.fixture def example_links(setup_store, set_store_loc): """Fixture to provide link objects as test input and setup store.""" StoreInterface(store_loc=set_store_loc) diff --git a/test/test_cli.py b/test/test_cli.py index 7e18363f..c4b34bff 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -13,7 +13,7 @@ SERVER_TIMEOUT = 16 -@pytest.fixture() +@pytest.fixture def setdir(): prev = os.getcwd() os.chdir(os.path.dirname(__file__)) @@ -21,7 +21,7 @@ def setdir(): os.chdir(prev) -@pytest.fixture() +@pytest.fixture async def server(setdir, ports): """ Sets up a server using minimal.yaml in the configs folder. @@ -61,7 +61,7 @@ async def server(setdir, ports): pass -@pytest.fixture() +@pytest.fixture async def cli_args(setdir, ports): logfile = "tmp.log" control_port, output_port, logging_port = ports diff --git a/test/test_config.py b/test/test_config.py index f9d11201..12cc79bf 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -16,7 +16,7 @@ # set global variables -@pytest.fixture() +@pytest.fixture def set_configdir(): """Sets the current working directory to the configs file.""" prev = os.getcwd() diff --git a/test/test_demos.py b/test/test_demos.py index 61098c67..10ebff68 100644 --- a/test/test_demos.py +++ b/test/test_demos.py @@ -16,7 +16,7 @@ SERVER_WARMUP = 10 -@pytest.fixture() +@pytest.fixture def setdir(): prev = os.getcwd() os.chdir(os.path.dirname(__file__)) @@ -25,7 +25,7 @@ def setdir(): os.chdir(prev) -@pytest.fixture() +@pytest.fixture def ip(): """Fixture to provide an IP test input.""" diff --git a/test/test_link.py b/test/test_link.py index 41ffbcfb..4401556c 100644 --- a/test/test_link.py +++ b/test/test_link.py @@ -11,7 +11,7 @@ from improv.link import Link -@pytest.fixture() +@pytest.fixture def setup_store(): """Fixture to set up the store subprocess with 10 mb. @@ -50,7 +50,7 @@ def init_actors(n=1): return [Actor("test " + str(i), "/tmp/store", links={}) for i in range(n)] -@pytest.fixture() +@pytest.fixture def example_link(setup_store): """Fixture to provide a commonly used Link object.""" setup_store @@ -60,7 +60,7 @@ def example_link(setup_store): lnk = None -@pytest.fixture() +@pytest.fixture def example_actor_system(setup_store): """Fixture to provide a list of 4 connected actors.""" @@ -88,7 +88,7 @@ def example_actor_system(setup_store): acts = None -@pytest.fixture() +@pytest.fixture def _kill_pytest_processes(): """Kills all processes with "pytest" in their name. @@ -242,7 +242,7 @@ def test_put_nowait(example_link): assert t_net < 0.005 # 5 ms -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_put_async_success(example_link): """Tests if put_async returns None. @@ -256,7 +256,7 @@ async def test_put_async_success(example_link): assert res is None -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_put_async_multiple(example_link): """Tests if async putting multiple objects preserves their order.""" @@ -273,7 +273,7 @@ async def test_put_async_multiple(example_link): assert messages_out == messages -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_put_and_get_async(example_link): """Tests if async get preserves order after async put.""" @@ -397,7 +397,7 @@ def test_get_nowait_empty(example_link): pytest.fail("the queue is not empty") -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_get_async_success(example_link): """Tests if async_get gets the correct element from the queue.""" @@ -408,7 +408,7 @@ async def test_get_async_success(example_link): assert res == "message" -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_get_async_empty(example_link): """Tests if get_async times out given an empty queue. @@ -442,7 +442,7 @@ def test_cancel_join_thread(example_link): @pytest.mark.skip(reason="unfinished") -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_join_thread(example_link): """Tests join_thread. This test is unfinished @@ -456,7 +456,7 @@ async def test_join_thread(example_link): assert True -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_multi_actor_system(example_actor_system, setup_store): """Tests if async puts/gets with many actors have good messages.""" diff --git a/test/test_nexus.py b/test/test_nexus.py index 84f3759f..6faef130 100644 --- a/test/test_nexus.py +++ b/test/test_nexus.py @@ -16,7 +16,7 @@ SERVER_COUNTER = 0 -@pytest.fixture() +@pytest.fixture def ports(): global SERVER_COUNTER CONTROL_PORT = 5555 @@ -30,7 +30,7 @@ def ports(): SERVER_COUNTER += 3 -@pytest.fixture() +@pytest.fixture def setdir(): prev = os.getcwd() os.chdir(os.path.dirname(__file__) + "/configs") @@ -38,7 +38,7 @@ def setdir(): os.chdir(prev) -@pytest.fixture() +@pytest.fixture def sample_nex(setdir, ports): nex = Nexus("test") nex.createNexus( @@ -271,7 +271,7 @@ def test_queue_message(setdir, sample_nex): assert True -@pytest.mark.asyncio() +@pytest.mark.asyncio @pytest.mark.skip(reason="This test is unfinished.") async def test_queue_readin(sample_nex, caplog): nex = sample_nex diff --git a/test/test_store_with_errors.py b/test/test_store_with_errors.py index d2951398..b381ddf1 100644 --- a/test/test_store_with_errors.py +++ b/test/test_store_with_errors.py @@ -35,7 +35,7 @@ # store_loc = '/dev/shm' -@pytest.fixture() +@pytest.fixture # TODO: put in conftest.py def setup_store(set_store_loc): """Start the server""" @@ -161,7 +161,7 @@ def test_is_csc_matrix_and_put(setup_store, set_store_loc): # class StoreInterfaceGetListandAll(StoreInterfaceDependentTestCase): -@pytest.mark.skip() +@pytest.mark.skip def test_get_list_and_all(setup_store, set_store_loc): store = StoreInterface(store_loc=set_store_loc) # id = store.put(1, "one") diff --git a/test/test_tui.py b/test/test_tui.py index 50ad6d54..d45bccfe 100644 --- a/test/test_tui.py +++ b/test/test_tui.py @@ -9,7 +9,7 @@ from test_nexus import ports -@pytest.fixture() +@pytest.fixture def logger(ports): logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -19,7 +19,7 @@ def logger(ports): logger.removeHandler(zmq_log_handler) -@pytest.fixture() +@pytest.fixture async def sockets(ports): with zmq.Context() as context: ctrl_socket = context.socket(REP) @@ -29,7 +29,7 @@ async def sockets(ports): yield (ctrl_socket, out_socket) -@pytest.fixture() +@pytest.fixture async def app(ports): mock = tui.TUI(*ports) yield mock From e267cc05030c7ac5b470a4cd1d53e3d505f9ec1e Mon Sep 17 00:00:00 2001 From: Anne Draelos <424969+draelos@users.noreply.github.com> Date: Thu, 5 Sep 2024 20:20:01 -0400 Subject: [PATCH 07/27] Docs (#189) Co-authored-by: John Pearson Co-authored-by: rwschonberg <47501218+rwschonberg@users.noreply.github.com> Co-authored-by: Richard Schonberg --- README.md | 11 +++++-- docs/_toc.yml | 1 + docs/configuration.md | 72 +++++++++++++++++++++++++++++++++++++++++++ docs/design.md | 2 +- docs/installation.md | 29 +++++++++++++++-- pyproject.toml | 2 +- 6 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 docs/configuration.md diff --git a/README.md b/README.md index 53ab0500..e5a25164 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# improv + + + +Adaptive experiments for neuroscience +--------- + [![PyPI](https://img.shields.io/pypi/v/improv?style=flat-square?style=flat-square)](https://pypi.org/project/improv) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/improv?style=flat-square)](https://pypi.org/project/improv) [![docs](https://github.com/project-improv/improv/actions/workflows/docs.yaml/badge.svg?style=flat-square)](https://project-improv.github.io/) @@ -7,7 +12,6 @@ [![PyPI - License](https://img.shields.io/pypi/l/improv?style=flat-square)](https://opensource.org/licenses/MIT) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square)](https://github.com/psf/black) -A flexible software platform for real-time and adaptive neuroscience experiments. _improv_ is a streaming software platform designed to enable adaptive experiments. By analyzing data as they arrive, we can obtain information about the current brain state in real time and use it to adaptively modify an experiment as data collection is ongoing. @@ -33,7 +37,8 @@ _improv_'s design is based on a streamlined version of the actor model for concu ## Installation -For installation instructions, please consult the [docs](https://project-improv.github.io/improv/installation.html). +For installation instructions, please consult the [documentation](https://project-improv.github.io/improv/installation.html). + ### Contact To get in touch, feel free to reach out on Twitter @annedraelos or @jmxpearson. diff --git a/docs/_toc.yml b/docs/_toc.yml index 426f566d..400c2b81 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -5,6 +5,7 @@ format: jb-book root: intro chapters: - file: installation +- file: configuration - file: running - file: demos - file: design diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 00000000..eab0f073 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,72 @@ +(page:configuration)= +# Configuring _improv_ + +## Redis Configuration Options + +Although _improv_ works with Redis out of the box, there are a handful of optional configuration options that can be used to change the behavior of _improv_'s data store. +Redis configuration is specified in the configuration file under the `redis_config` key. + +(#persistence)= +### Persistence +_improv_ can configure the data store to write its contents to a set of append-only log files on disk. +Append-only files, or AOFs, can be reused between instances of _improv_, which will allow successive runs of _improv_ to access previously stored data. +By default, this capability is disabled, but is controlled by the following settings. + +#### enable_saving +This field enables saving of data store contents to AOFs on disk. The default mode of operation uses the default Redis AOF directory, `appendonlydir`, which will be automatically created if it does not exist. +- Type: boolean (True/False) +- Example: True + +#### aof_dirname +This setting controls the name of the directory in which the store saves the append-only logs. +Persistence happens automatically at scheduled intervals throughout the operation of the data store, as well as during shutdown of the data store. + +- Type: string +- Example: `custom_aof_directory` + +#### generate_ephemeral_aof_dirname +This field indicates that _improv_ should generate a unique AOF directory name to use each time it is run. +This will set each run of _improv_ to start from a clean data store without needing to set a unique directory name manually before each run. + +- Type: boolean (True/False) +- Example: True + +#### fsync_frequency +Although the append-only log files, when enabled, are updated every time the state of the data store is changed, the contents of the file are not always immediately flushed to the system's disk. +This field controls how often the data store should request that the operating system flush the contents of the append-only log files to the hard disk. + +- Type: string +- Values: + - `every_write` - sync the AOF to disk on every data store modification. + - Highest fault tolerance + - Highest background disk and data store load. The data store can spend a lot of time doing processing unrelated to running the experiment. + - `every_second` - sync the AOF to disk every second. + - Moderate fault tolerance - can only lose up to roughly 1 second of data in a disaster. + - Better performance. + - `no_schedule` - sync the AOF according the default system policy. + - Lowest fault tolerance - can go from 1 to 30 seconds, occasionally more, between syncs. + - Highest performance. +- Default: `no_schedule` +- Example: every_second + +### Connectivity +#### port +This field controls the port on which the data store runs. If left unspecified, _improv_ will attempt to start the data store on the default port number, +looking for alternatives if tried port numbers are busy. If specified, then _improv_ will only try to start the data store on the specified port number. + +- Type: integer +- Default: 6379 +- Example: 16300 + +### Example +An _improv_ configuration file which sets the data store port to 6385, enables persistence to disk, and uses a unique directory for each run, would look like: +``` +actors: ... + +connections: ... + +redis_config: + enable_saving: True + port: 6385 + generate_ephemeral_aof_dirname: True +``` \ No newline at end of file diff --git a/docs/design.md b/docs/design.md index 9bd6b188..c67b3d3a 100644 --- a/docs/design.md +++ b/docs/design.md @@ -98,4 +98,4 @@ For examples and further documentation, see [](page:actors). ## Logging and persistence Finally, _improv_ handles centralized logging via the [`logging`](https://docs.python.org/3/library/logging.html) module, which listens for messages on a global logging port. These messages are written to the experimental log file. -Data from the server are persisted to disk using [LMDB](http://www.lmdb.tech/doc/) (if `settings: use_hdd` is set to `true` in the configuration file). \ No newline at end of file +Data from the server are persisted to disk using [Redis Append-Only Log Files](https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/) (if `redis_config: enable_saving` is set to `True` in the configuration file). See the [persistence section](configuration.md#persistence) of the [configuration guide](configuration.md) for more information. \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md index dc9eeb42..318390d1 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,8 +1,11 @@ (page:installation)= # Installation and building +## Installation Time +All installation processes described here should take less than 5 minutes to complete on a standard workstation. + ## Simple installation -The simplest way to install _improv_ is with pip: +Once you have the [required dependencies](#required-dependencies), the simplest way to install _improv_ is with pip: ``` pip install improv ``` @@ -13,6 +16,28 @@ pip install improv --no-binary pyzmq ``` to build `pyzmq` from source if you're running into ZMQ errors. ```` +(#required-dependencies)= +## Required dependencies + +### Redis + +_improv_ uses Redis, an in-memory datastore, to hold data to be communicated between actors. +_improv_ works automatically with Redis, but additional options for controlling the behavior of the data store are listed in +the [configuration guide](configuration.md). + +_improv_ has been tested with Redis server version 7.2.4. Please refer to the instructions below for your operating system: + +#### macOS +A compatible version of Redis can be installed via [Homebrew](https://brew.sh): +``` +brew install redis +``` + +#### Linux +A compatible version of Redis can be installed for most standard Linux distributions (e.g. Ubuntu) by following Redis' short [Linux installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/). + +#### Windows (WSL2) +Redis can also be installed on Windows in WSL2. The [WSL2 installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-windows/) details the process for both the Windows and Linux portions of WSL2. ## Optional dependencies In addition to the basic _improv_ installation, users who want to, e.g., run tests locally and build docs should do @@ -71,4 +96,4 @@ Then simply run ``` jupyter-book build docs ``` -and open `docs/_build/html/index.html` in your browser. \ No newline at end of file +and open `docs/_build/html/index.html` in your browser. diff --git a/pyproject.toml b/pyproject.toml index 044829f5..6eab88a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ readme = "README.md" requires-python = ">=3.6" keywords = ["neuroscience", "adaptive", "closed loop"] dependencies = [ - "numpy", + "numpy<=1.26", "scipy", "matplotlib", "pyarrow==9.0.0", From c673319b42d8b46e91afaf2231de586edd3008a4 Mon Sep 17 00:00:00 2001 From: Edwin Ma Date: Mon, 23 Dec 2024 16:26:25 -0500 Subject: [PATCH 08/27] Added working cosine/sine wave generator based on frame index. Add working lorenz_generator/processor that plots the final lorenz_graphic, but need help on removing graphic and replotting updated graphic in updating_graphic.ipynb. --- demos/minimal/actors/fastplotlib.ipynb | 368 ++++++++++++++++++++ demos/minimal/actors/lorenz.ipynb | 228 ++++++++++++ demos/minimal/actors/lorenz_generator.py | 57 +++ demos/minimal/actors/lorenz_processor.py | 177 ++++++++++ demos/minimal/actors/sample_generator.py | 20 +- demos/minimal/actors/sample_processor.py | 139 ++++++-- demos/minimal/actors/updating_graphic.ipynb | 91 +++++ 7 files changed, 1037 insertions(+), 43 deletions(-) create mode 100644 demos/minimal/actors/fastplotlib.ipynb create mode 100644 demos/minimal/actors/lorenz.ipynb create mode 100644 demos/minimal/actors/lorenz_generator.py create mode 100644 demos/minimal/actors/lorenz_processor.py create mode 100644 demos/minimal/actors/updating_graphic.ipynb diff --git a/demos/minimal/actors/fastplotlib.ipynb b/demos/minimal/actors/fastplotlib.ipynb new file mode 100644 index 00000000..fe3c0e3e --- /dev/null +++ b/demos/minimal/actors/fastplotlib.ipynb @@ -0,0 +1,368 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "83566503-c31b-4d83-b6f0-173bc9b2102d", + "metadata": {}, + "source": [ + "## `fastplotlib` demo" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "initial_id", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bb427bf789ec409c8d5a0c5a82fcfea8", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "No windowing system present. Using surfaceless platform\n", + "No config found!\n", + "No config found!\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Available devices:\n", + "✅ (default) | NVIDIA GeForce GTX 1080 Ti | DiscreteGPU | Vulkan | 555.42.06\n", + "❗ | llvmpipe (LLVM 15.0.7, 256 bits) | CPU | Vulkan | Mesa 23.2.1-1ubuntu3.1~22.04.2 (LLVM 15.0.7)\n", + "❗ | NVIDIA GeForce GTX 1080 Ti/PCIe/SSE2 | Unknown | OpenGL | \n" + ] + } + ], + "source": [ + "import numpy as np\n", + "import zmq\n", + "import fastplotlib as fpl" + ] + }, + { + "cell_type": "markdown", + "id": "74e6f9a5-e4f3-4033-9529-20fcf5068703", + "metadata": {}, + "source": [ + "### Check if GPU or rendering with CPU via lavapipe is available" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "093c8fd4-bbf8-4374-a661-494a61a73cca", + "metadata": {}, + "outputs": [], + "source": [ + "if len(fpl.utils.gpu.enumerate_adapters()) < 1:\n", + " raise IndexError(\"WGPU could not enumerate any adapters, fastplotlib will not work.\")" + ] + }, + { + "cell_type": "markdown", + "id": "24f4a955-c7c5-4b9d-a653-d5daecb27d04", + "metadata": {}, + "source": [ + "### Setup zmq subscriber client" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b4182903-b4fe-462a-9f92-71d254bb5e24", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "context = zmq.Context()\n", + "sub = context.socket(zmq.SUB)\n", + "sub.setsockopt(zmq.SUBSCRIBE, b\"\")\n", + "\n", + "# keep only the most recent message\n", + "sub.setsockopt(zmq.CONFLATE, 1)\n", + "\n", + "# address must match publisher in Processor actor\n", + "sub.connect(\"tcp://127.0.0.1:5555\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b06167bf-5220-46f4-bd13-3506d848bf35", + "metadata": {}, + "outputs": [], + "source": [ + "def get_buffer():\n", + " \"\"\"\n", + " Gets the buffer from the publisher\n", + " \"\"\"\n", + " try:\n", + " b = sub.recv(zmq.NOBLOCK)\n", + " except zmq.Again:\n", + " pass\n", + " else:\n", + " return b\n", + " \n", + " return None" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8107b2e2-f855-4455-ab45-65351b3cd1ab", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6e7519d1af5d452daf7c04c8ea0ac54f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/edwin/edwin_improv/fpl/lib/python3.10/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32\n", + " warn(f\"casting {array.dtype} array to float32\")\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5efef3202fb24294ae765a4ae0bed5bc", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "# Create the figure\n", + "figure = fpl.Figure()\n", + "\n", + "# Initialize sine and cosine wave data\n", + "xs = np.linspace(-10, 10, 100)\n", + "cos_ys = np.cos(xs)\n", + "sin_ys = np.sin(xs)\n", + "\n", + "# Add a line plot placeholder\n", + "figure[0, 0].add_line(data=np.column_stack((xs, cos_ys)), name=\"wave\")\n", + "\n", + "def update_frame(p):\n", + " # Receive memory with buffer\n", + " buff = get_buffer() # Replace with a valid data source\n", + " \n", + " if buff is not None:\n", + " # Convert the buffer to a numpy array\n", + " a = np.frombuffer(buff, dtype=np.float64)\n", + " ix = a[-1]\n", + " \n", + " # Update wave plot based on parity of ix\n", + " if ix % 2 == 0:\n", + " # Display cosine wave\n", + " new_ys = np.cos(xs)\n", + " p[\"wave\"].data = np.column_stack((xs, new_ys, np.zeros_like(xs)))\n", + " p[\"wave\"].color = \"blue\" # Change color for cosine\n", + " else:\n", + " # Display sine wave\n", + " new_ys = np.sin(xs)\n", + " p[\"wave\"].data = np.column_stack((xs, new_ys, np.zeros_like(xs)))\n", + " p[\"wave\"].color = \"red\" # Change color for sine\n", + " \n", + " # Update the plot name to reflect the current frame\n", + " p.name = f\"frame: {int(ix)}\"\n", + "\n", + "# Add the animation update function\n", + "figure[0, 0].add_animations(update_frame)\n", + "\n", + "# Show the figure\n", + "figure.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "9a791d42-9ba1-4533-a4f2-98f191ba4ac9", + "metadata": {}, + "source": [ + "## `fastplotlib` is non-blocking!" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d7138f60-7812-49ca-9655-a6ecdecad5e4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'sent_frames': 123,\n", + " 'confirmed_frames': 121,\n", + " 'roundtrip': 0.05958017041860533,\n", + " 'delivery': -0.5480798748899097,\n", + " 'img_encoding': 0.007126040003315463,\n", + " 'fps': 31.430251931495025}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "figure.canvas.get_stats()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "8cd5298f-f5bc-480e-9956-64efd3d6bb5b", + "metadata": {}, + "outputs": [], + "source": [ + "buff = get_buffer()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "c37a3f8d-092f-4d11-a352-fdc4fc7279b5", + "metadata": {}, + "outputs": [], + "source": [ + "a= np.frombuffer(buff, dtype=np.float64)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "3c0d3d83-f8af-4965-8984-778dedd1090f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(262145,)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "d5ab93f1-297d-48bc-9a26-cdedc26f9669", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "229.0" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a[-1]" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "7d3474c2-9dc5-4549-9b24-7294d6d14a21", + "metadata": {}, + "outputs": [], + "source": [ + "figure[0,0][\"img\"].data = a[:-1].reshape(512, 512)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "b28a2346-c4cf-4de7-b203-f5a8b40bfdd3", + "metadata": {}, + "outputs": [], + "source": [ + "figure[0,0].name = \"bah\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e8b04e6-3fe2-4b66-81bc-777fb0267350", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demos/minimal/actors/lorenz.ipynb b/demos/minimal/actors/lorenz.ipynb new file mode 100644 index 00000000..a040c1d7 --- /dev/null +++ b/demos/minimal/actors/lorenz.ipynb @@ -0,0 +1,228 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "98c54909-f50a-42f6-a49c-8f249a279e19", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6037601cdd0d473ba4bfb0a9c639e841", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "No windowing system present. Using surfaceless platform\n", + "No config found!\n", + "No config found!\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Available devices:\n", + "✅ (default) | NVIDIA GeForce GTX 1080 Ti | DiscreteGPU | Vulkan | 555.42.06\n", + "❗ | llvmpipe (LLVM 15.0.7, 256 bits) | CPU | Vulkan | Mesa 23.2.1-1ubuntu3.1~22.04.2 (LLVM 15.0.7)\n", + "❗ | NVIDIA GeForce GTX 1080 Ti/PCIe/SSE2 | Unknown | OpenGL | \n", + "Listening for updated Lorenz coordinates...\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "261fac1ee2ee4218a9f20e74340982ce", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/edwin/edwin_improv/fpl/lib/python3.10/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32\n", + " warn(f\"casting {array.dtype} array to float32\")\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Automatically created module for IPython interactive environment\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "665ab12b09854dc4acb1d1fe23593340", + "version_major": 2, + "version_minor": 0 + }, + "text/html": [ + "
snapshot
" + ], + "text/plain": [ + "JupyterWgpuCanvas(css_height='560px', css_width='700px')" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import zmq\n", + "import numpy as np\n", + "import fastplotlib as fpl\n", + "import time # Import time module\n", + "\n", + "# Set up ZMQ subscriber\n", + "context = zmq.Context()\n", + "socket = context.socket(zmq.SUB)\n", + "socket.connect(\"tcp://127.0.0.1:5555\")\n", + "socket.setsockopt_string(zmq.SUBSCRIBE, \"\")\n", + "\n", + "print(\"Listening for updated Lorenz coordinates...\")\n", + "\n", + "try:\n", + " # Example num_steps (adjust based on your use case)\n", + " num_steps = 100 # This must match the expected size of xyzs\n", + " figure = fpl.Figure(\n", + " cameras=\"3d\",\n", + " controller_types=\"fly\",\n", + " size=(700, 560)\n", + " )\n", + " # Initialize coordinates array with zeros\n", + " lorenz_data = np.zeros((1, num_steps + 200, 3), dtype=np.float64)\n", + " # Keep track of the current frame index being filled\n", + " current_index = 0\n", + " # line_graphic = figure[0, 0].add_line(data=coordinates_array, name=\"line\")\n", + " while current_index < num_steps + 1:\n", + " # Receive raw message\n", + " msg = socket.recv()\n", + "\n", + " # Convert the message back to a numpy array\n", + " data = np.frombuffer(msg, dtype=np.float64)\n", + "\n", + " # Separate coordinates and frame index\n", + " coordinates = data[:-1] # All except last element\n", + " frame_index = int(data[-1]) # Last element is the frame index\n", + "\n", + " # Fill the corresponding row in the coordinates array\n", + " lorenz_data[0][current_index] = coordinates\n", + " \n", + " # Increment the current index\n", + " current_index += 1\n", + " \n", + " lorenz_line = figure[0, 0].add_line_collection(data=lorenz_data, thickness=.1, cmap=\"tab10\")\n", + " figure.show()\n", + "\n", + " # Wait for 2 seconds before showing the updated figure\n", + " time.sleep(2)\n", + "\n", + " figure.clear()\n", + "\n", + " while current_index < num_steps + 100:\n", + " # Receive raw message\n", + " msg = socket.recv()\n", + "\n", + " # Convert the message back to a numpy array\n", + " data = np.frombuffer(msg, dtype=np.float64)\n", + "\n", + " # Separate coordinates and frame index\n", + " coordinates = data[:-1] # All except last element\n", + " frame_index = int(data[-1]) # Last element is the frame index\n", + "\n", + " # Fill the corresponding row in the coordinates array\n", + " lorenz_data[0][current_index] = coordinates\n", + " \n", + " # Increment the current index\n", + " current_index += 1\n", + "\n", + " new_lorenz_line = figure[0, 0].add_line_collection(data=lorenz_data, thickness=.1, cmap=\"tab10\")\n", + "\n", + " figure.show()\n", + " \n", + " # Set initial camera position to make animation in gallery render better\n", + " figure[0, 0].camera.world.z = 80\n", + " \n", + " # NOTE: `if __name__ == \"__main__\"` is NOT how to use fastplotlib interactively\n", + " # please see our docs for using fastplotlib interactively in ipython and jupyter\n", + " if __name__ == \"__main__\":\n", + " print(__doc__)\n", + " fpl.run()\n", + " # Print the size of coordinates_array\n", + " # print(\"Coordinates collection complete:\")\n", + " # print(\"Size of coordinates_array:\", coordinates_array.shape)\n", + "\n", + "except KeyboardInterrupt:\n", + " print(\"Stopped listening.\")\n", + "finally:\n", + " socket.close()\n", + " context.term()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1df0f598-31d0-4a79-828b-f3ee2a43cc3a", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0dc63fe3-b01c-4176-aac2-5bbc2518dfe0", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "acc364fa-01e8-4c50-94ab-e4ee638bb2ae", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demos/minimal/actors/lorenz_generator.py b/demos/minimal/actors/lorenz_generator.py new file mode 100644 index 00000000..0a86fbc0 --- /dev/null +++ b/demos/minimal/actors/lorenz_generator.py @@ -0,0 +1,57 @@ +from improv.actor import Actor, RunManager +from datetime import date # used for saving +import numpy as np +import logging +import time # Importing time module for the delay + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class Generator(Actor): + """ + Generates the initial coordinates for the Lorenz system. + Intended to provide initial data for the LorenzProcessor. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.data = None + self.name = "lorenz_generator" + self.frame_num = 0 + + def __str__(self): + return f"Name: {self.name}, Data: {self.data}" + + def setup(self): + """ + Initializes the Lorenz system with the starting coordinates. + """ + logger.info("Beginning setup for LorenzGenerator") + self.data = np.array([[1.0, 1.0, 1.0]]) # Initial coordinates (x, y, z) + logger.info(f"Initialized Lorenz system with coordinates: {self.data}") + + def stop(self): + """ + Save the current Lorenz coordinates to a file for persistence. + """ + logger.info("LorenzGenerator stopping") + np.save("lorenz_initial_data.npy", self.data) + return 0 + + def runStep(self): + """ + Sends the initial Lorenz system coordinates. + """ + time.sleep(0.5) # Delay for half a second + + if self.frame_num < np.shape(self.data)[0]: + data_id = self.client.put( + self.data[self.frame_num], str(f"Lorenz_Initial: {self.frame_num}") + ) + try: + self.q_out.put([[data_id, str(self.frame_num)]]) + logger.info(f"Sent initial Lorenz coordinate: {self.data[self.frame_num]}") + self.frame_num += 1 + except Exception as e: + logger.error(f"LorenzGenerator Exception: {e}") diff --git a/demos/minimal/actors/lorenz_processor.py b/demos/minimal/actors/lorenz_processor.py new file mode 100644 index 00000000..8b3553df --- /dev/null +++ b/demos/minimal/actors/lorenz_processor.py @@ -0,0 +1,177 @@ +from improv.actor import Actor +from queue import Empty +import logging +import zmq +import numpy as np + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +def lorenz(xyz, s=10, r=28, b=2.667): + """ + Parameters + ---------- + xyz : array-like, shape (3,) + Point of interest in three-dimensional space. + s, r, b : float + Parameters defining the Lorenz attractor. + + Returns + ------- + xyz_dot : array, shape (3,) + Values of the Lorenz attractor's partial derivatives at *xyz*. + """ + x, y, z = xyz + x_dot = s * (y - x) + y_dot = r * x - y - x * z + z_dot = x * y - b * z + return np.array([x_dot, y_dot, z_dot]) + + +class Processor(Actor): + """ + Process data, calculate updated Lorenz coordinates, and send them via ZMQ. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def setup(self): + """ + Creates and binds the socket for ZMQ and initializes the Lorenz system. + """ + self.name = "lorenz_processor" + + # Set up ZMQ PUB socket + context = zmq.Context() + self.socket = context.socket(zmq.PUB) + self.socket.bind("tcp://127.0.0.1:5555") + + self.frame_num = 1 + self.current_coords = None # Will be initialized with generator data + self.dt = 0.01 # Small time step for Euler's method + + logger.info("Completed setup for Processor") + + def stop(self): + logger.info("Processor stopping") + self.socket.close() + return 0 + + def runStep(self): + """ + Receives initial coordinates from the Generator, calculates updated Lorenz coordinates, + and sends them via ZMQ. + """ + if self.current_coords is None: + # Get initial coordinates from the queue + try: + frame = self.q_in.get(timeout=0.05) + data_id = frame[0][0] # Data ID in the store + # Fetch data and make a writable copy + self.current_coords = np.array(self.client.getID(data_id), copy=True) + + logger.info(f"Initialized Lorenz coordinates: {self.current_coords}") + + except Empty: + logger.info("Waiting for initial Lorenz coordinates...") + return + except Exception as e: + logger.error(f"Failed to initialize Lorenz coordinates: {e}") + return + + try: + # Calculate the Lorenz derivatives + derivatives = lorenz(self.current_coords) + + # Update coordinates using Euler's method + self.current_coords += derivatives * self.dt + frame_ix = self.frame_num + + logger.info(f"Frame {frame_ix}: Updated Lorenz coordinates: {self.current_coords}") + + # Combine the updated coordinates and frame index for sending + out = np.concatenate([self.current_coords, [frame_ix]], dtype=np.float64) + + # Send the data via ZMQ + self.socket.send(out.tobytes()) + logger.info(f"Sent frame {frame_ix}: {self.current_coords}") + + self.frame_num += 1 + + except Exception as e: + logger.error(f"Error during processing: {e}") + + +# from improv.actor import Actor +# from queue import Empty +# import logging +# import zmq +# import numpy as np + +# logger = logging.getLogger(__name__) +# logger.setLevel(logging.INFO) + + +# class Processor(Actor): +# """ +# Process data and send it through zmq to be visualized in Jupyter Notebook. +# """ + +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) + +# def setup(self): +# """ +# Creates and binds the socket for zmq. +# """ +# self.name = "lorenz_processor" + +# # Set up ZMQ PUB socket +# context = zmq.Context() +# self.socket = context.socket(zmq.PUB) +# self.socket.bind("tcp://127.0.0.1:5555") + +# self.frame_num = 1 + +# logger.info("Completed setup for Processor") + +# def stop(self): +# logger.info("Processor stopping") +# self.socket.close() +# return 0 + +# def runStep(self): +# """ +# Fetches data from the queue, processes it, and sends it via ZMQ. +# """ +# frame = None +# try: +# frame = self.q_in.get(timeout=0.05) +# except Empty: +# logger.info("Queue is empty; no frame to process.") +# except Exception as e: +# logger.error(f"Could not get frame! Exception: {e}") + +# if frame is not None: +# try: +# # Fetch the data from the store +# self.frame = self.client.getID(frame[0][0]) +# coordinates = self.frame.ravel() +# frame_ix = self.frame_num + +# logger.info(f"Retrieved coordinates: {coordinates}") +# logger.info(f"Sending frame {frame_ix}") + +# # Combine the coordinates and frame index for sending +# out = np.concatenate([coordinates, [frame_ix]], dtype=np.float64) + +# # Send the data +# self.socket.send(out.tobytes()) # Send as bytes +# logger.info(f"Sent frame {frame_ix}: {coordinates}") + +# self.frame_num += 1 + +# except Exception as e: +# logger.error(f"Error during processing: {e}") diff --git a/demos/minimal/actors/sample_generator.py b/demos/minimal/actors/sample_generator.py index df5191ec..8a057d61 100644 --- a/demos/minimal/actors/sample_generator.py +++ b/demos/minimal/actors/sample_generator.py @@ -2,6 +2,7 @@ from datetime import date # used for saving import numpy as np import logging +import time # Importing time module for the delay logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) @@ -30,7 +31,7 @@ def setup(self): """ logger.info("Beginning setup for Generator") - self.data = np.asmatrix(np.random.randint(100, size=(100, 5))) + self.data = np.random.randint(10, size=(1, 5)) # Initialize data logger.info("Completed setup for Generator") def stop(self): @@ -43,26 +44,21 @@ def stop(self): def runStep(self): """Generates additional data after initial setup data is exhausted. - Data is of a different form as the setup data in that although it is - the same size (5x1 vector), it is uniformly distributed in [1, 10] - instead of in [1, 100]. Therefore, the average over time should - converge to 5.5. + Data is a 5x1 vector uniformly distributed in [1, 10]. """ + time.sleep(0.5) # Delay for half a second + if self.frame_num < np.shape(self.data)[0]: data_id = self.client.put( self.data[self.frame_num], str(f"Gen_raw: {self.frame_num}") ) - # logger.info('Put data in store') try: self.q_out.put([[data_id, str(self.frame_num)]]) logger.info("Sent message on") self.frame_num += 1 except Exception as e: - logger.error( - f"--------------------------------Generator Exception: {e}" - ) + logger.error(f"--------------------------------Generator Exception: {e}") else: - self.data = np.concatenate( - (self.data, np.asmatrix(np.random.randint(10, size=(1, 5)))), axis=0 - ) + new_data = np.random.randint(10, size=(1, 5)) + self.data = np.concatenate((self.data, new_data), axis=0) diff --git a/demos/minimal/actors/sample_processor.py b/demos/minimal/actors/sample_processor.py index 24f87880..65306d60 100644 --- a/demos/minimal/actors/sample_processor.py +++ b/demos/minimal/actors/sample_processor.py @@ -1,63 +1,140 @@ +# from improv.actor import Actor +# import numpy as np +# import logging + +# logger = logging.getLogger(__name__) +# logger.setLevel(logging.INFO) + + +# class Processor(Actor): +# """Sample processor used to calculate the average of an array of integers. + +# Intended for use with sample_generator.py. +# """ + +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) + +# def setup(self): +# """Initializes all class variables. + +# self.name (string): name of the actor. +# self.frame (ObjectID): StoreInterface object id referencing data from the store. +# self.avg_list (list): list that contains averages of individual vectors. +# self.frame_num (int): index of current frame. +# """ + +# self.name = "Processor" +# self.frame = None +# self.avg_list = [] +# self.frame_num = 1 +# logger.info("Completed setup for Processor") + +# def stop(self): +# """Trivial stop function for testing purposes.""" + +# logger.info("Processor stopping") + +# def runStep(self): +# """Gets from the input queue and calculates the average. + +# Receives an ObjectID, references data in the store using that +# ObjectID, calculates the average of that data, and finally prints +# to stdout. +# """ + +# frame = None +# try: +# frame = self.q_in.get(timeout=0.001) + +# except: +# logger.error("Could not get frame!") +# pass + +# if frame is not None and self.frame_num is not None: +# self.done = False +# self.frame = self.client.getID(frame[0][0]) +# avg = np.mean(self.frame[0]) + +# # print(f"Average: {avg}") +# self.avg_list.append(avg) +# # print(f"Overall Average: {np.mean(self.avg_list)}") +# # print(f"Frame number: {self.frame_num}") +# self.frame_num += 1 + + from improv.actor import Actor -import numpy as np -import logging +from queue import Empty +import logging; logger = logging.getLogger(__name__) +import zmq + logger.setLevel(logging.INFO) +import numpy as np class Processor(Actor): - """Sample processor used to calculate the average of an array of integers. - - Intended for use with sample_generator.py. + """ + Process data and send it through zmq to be visualized. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def setup(self): - """Initializes all class variables. - - self.name (string): name of the actor. - self.frame (ObjectID): StoreInterface object id referencing data from the store. - self.avg_list (list): list that contains averages of individual vectors. - self.frame_num (int): index of current frame. + """ + Creates and binds the socket for zmq. """ self.name = "Processor" - self.frame = None - self.avg_list = [] + + context = zmq.Context() + self.socket = context.socket(zmq.PUB) + self.socket.bind("tcp://127.0.0.1:5555") + self.frame_num = 1 - logger.info("Completed setup for Processor") - def stop(self): - """Trivial stop function for testing purposes.""" + logger.info('Completed setup for Processor') + def stop(self): logger.info("Processor stopping") + self.socket.close() + return 0 def runStep(self): - """Gets from the input queue and calculates the average. - - Receives an ObjectID, references data in the store using that - ObjectID, calculates the average of that data, and finally prints - to stdout. + """ + Gets the data_id to the store from the queue, fetches the frame from the data store, + take the mean, sends a memoryview so the zmq subscriber can get the buffer to update + the plot. """ frame = None - try: - frame = self.q_in.get(timeout=0.001) + try: + frame = self.q_in.get(timeout=0.05) + except Empty: + pass except: logger.error("Could not get frame!") - pass - if frame is not None and self.frame_num is not None: - self.done = False + if frame is not None: + # get frame from data store self.frame = self.client.getID(frame[0][0]) - avg = np.mean(self.frame[0]) - # print(f"Average: {avg}") - self.avg_list.append(avg) - # print(f"Overall Average: {np.mean(self.avg_list)}") - # print(f"Frame number: {self.frame_num}") + # do some processing + self.frame.mean() + + frame_ix = self.frame_num % 10 + + logger.info(self.frame.shape) + + # # send the buffer data and frame number as an array + out = np.concatenate( + [self.frame.ravel(), np.array([frame_ix])], # Convert frame_ix to a 1D array + dtype=np.float64 + ) + self.frame_num += 1 + self.socket.send(out) + logger.info(f"Processed and sent frame number {frame_ix}") \ No newline at end of file diff --git a/demos/minimal/actors/updating_graphic.ipynb b/demos/minimal/actors/updating_graphic.ipynb new file mode 100644 index 00000000..483513d7 --- /dev/null +++ b/demos/minimal/actors/updating_graphic.ipynb @@ -0,0 +1,91 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "id": "3e3d7881-bf2c-40f1-943e-988c15f427da", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9f2c5dec0cea4b8e8c4eb5d7b86a3f89", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "092e0f456911455aa7d34084541d26ed", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "import fastplotlib as fpl\n", + "import time\n", + "\n", + "# Create the figure\n", + "figure = fpl.Figure()\n", + "\n", + "# Add a line plot placeholder\n", + "line_data = np.array([[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 3, 0]], dtype=np.float32)\n", + "new_line_data = np.array([[0, 1, 0], [1, 0, 0], [0, 1, 0], [1, 0, 0]], dtype=np.float32)\n", + "\n", + "line_graphic = figure[0, 0].add_line(data=line_data, name=\"line\")\n", + "figure.show()\n", + "\n", + "time.sleep(2)\n", + "figure[0,0].remove_graphic(line_graphic)\n", + "\n", + "new_line_graphic = figure[0, 0].add_line(data=new_line_data, name=\"new_line\")\n", + "figure.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "108675b8-3f99-4a92-adda-507116ea810c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 41a2d90023c5285b87cc20cdc5e2f976e3748124 Mon Sep 17 00:00:00 2001 From: Edwin Ma Date: Mon, 13 Jan 2025 17:40:47 -0500 Subject: [PATCH 09/27] updated sine/cosine generation in sample_generator intead of jupyter lab --- demos/minimal/actors/fastplotlib.ipynb | 268 ++++++----------------- demos/minimal/actors/sample_generator.py | 64 +++--- demos/minimal/actors/sample_processor.py | 121 ++-------- 3 files changed, 122 insertions(+), 331 deletions(-) diff --git a/demos/minimal/actors/fastplotlib.ipynb b/demos/minimal/actors/fastplotlib.ipynb index fe3c0e3e..a2520b87 100644 --- a/demos/minimal/actors/fastplotlib.ipynb +++ b/demos/minimal/actors/fastplotlib.ipynb @@ -11,13 +11,13 @@ { "cell_type": "code", "execution_count": 1, - "id": "initial_id", + "id": "282342a1-3257-4b07-a318-81b819f8a46a", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "bb427bf789ec409c8d5a0c5a82fcfea8", + "model_id": "c0f7d443919245cda4f5e18171f3f4d8", "version_major": 2, "version_minor": 0 }, @@ -49,98 +49,42 @@ } ], "source": [ - "import numpy as np\n", "import zmq\n", + "import numpy as np\n", "import fastplotlib as fpl" ] }, - { - "cell_type": "markdown", - "id": "74e6f9a5-e4f3-4033-9529-20fcf5068703", - "metadata": {}, - "source": [ - "### Check if GPU or rendering with CPU via lavapipe is available" - ] - }, { "cell_type": "code", "execution_count": 2, - "id": "093c8fd4-bbf8-4374-a661-494a61a73cca", + "id": "3e98d5f9-4c5e-4b9e-a63b-bd61dbf88e8d", "metadata": {}, "outputs": [], "source": [ - "if len(fpl.utils.gpu.enumerate_adapters()) < 1:\n", - " raise IndexError(\"WGPU could not enumerate any adapters, fastplotlib will not work.\")" - ] - }, - { - "cell_type": "markdown", - "id": "24f4a955-c7c5-4b9d-a653-d5daecb27d04", - "metadata": {}, - "source": [ - "### Setup zmq subscriber client" + "# ZMQ context and socket setup\n", + "context = zmq.Context()\n", + "socket = context.socket(zmq.SUB)\n", + "socket.connect(\"tcp://127.0.0.1:5555\")\n", + "socket.setsockopt_string(zmq.SUBSCRIBE, \"\")" ] }, { "cell_type": "code", "execution_count": 3, - "id": "b4182903-b4fe-462a-9f92-71d254bb5e24", + "id": "c92fbe02-fe8f-4fd0-94d3-da999b51d51e", "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "context = zmq.Context()\n", - "sub = context.socket(zmq.SUB)\n", - "sub.setsockopt(zmq.SUBSCRIBE, b\"\")\n", - "\n", - "# keep only the most recent message\n", - "sub.setsockopt(zmq.CONFLATE, 1)\n", - "\n", - "# address must match publisher in Processor actor\n", - "sub.connect(\"tcp://127.0.0.1:5555\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "b06167bf-5220-46f4-bd13-3506d848bf35", - "metadata": {}, - "outputs": [], - "source": [ - "def get_buffer():\n", - " \"\"\"\n", - " Gets the buffer from the publisher\n", - " \"\"\"\n", - " try:\n", - " b = sub.recv(zmq.NOBLOCK)\n", - " except zmq.Again:\n", - " pass\n", - " else:\n", - " return b\n", - " \n", - " return None" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "8107b2e2-f855-4455-ab45-65351b3cd1ab", - "metadata": {}, - "outputs": [ + "name": "stdout", + "output_type": "stream", + "text": [ + "Listening...\n" + ] + }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "6e7519d1af5d452daf7c04c8ea0ac54f", + "model_id": "fbbae92c1e344a9d867bcf5366acf7b5", "version_major": 2, "version_minor": 0 }, @@ -161,184 +105,102 @@ }, { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5efef3202fb24294ae765a4ae0bed5bc", - "version_major": 2, - "version_minor": 0 - }, "text/plain": [ - "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" + "" ] }, - "execution_count": 5, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "\n", + "print(\"Listening...\")\n", "# Create the figure\n", "figure = fpl.Figure()\n", "\n", - "# Initialize sine and cosine wave data\n", + "# Add a line plot placeholder\n", "xs = np.linspace(-10, 10, 100)\n", "cos_ys = np.cos(xs)\n", - "sin_ys = np.sin(xs)\n", - "\n", - "# Add a line plot placeholder\n", - "figure[0, 0].add_line(data=np.column_stack((xs, cos_ys)), name=\"wave\")\n", - "\n", - "def update_frame(p):\n", - " # Receive memory with buffer\n", - " buff = get_buffer() # Replace with a valid data source\n", - " \n", - " if buff is not None:\n", - " # Convert the buffer to a numpy array\n", - " a = np.frombuffer(buff, dtype=np.float64)\n", - " ix = a[-1]\n", - " \n", - " # Update wave plot based on parity of ix\n", - " if ix % 2 == 0:\n", - " # Display cosine wave\n", - " new_ys = np.cos(xs)\n", - " p[\"wave\"].data = np.column_stack((xs, new_ys, np.zeros_like(xs)))\n", - " p[\"wave\"].color = \"blue\" # Change color for cosine\n", - " else:\n", - " # Display sine wave\n", - " new_ys = np.sin(xs)\n", - " p[\"wave\"].data = np.column_stack((xs, new_ys, np.zeros_like(xs)))\n", - " p[\"wave\"].color = \"red\" # Change color for sine\n", - " \n", - " # Update the plot name to reflect the current frame\n", - " p.name = f\"frame: {int(ix)}\"\n", - "\n", - "# Add the animation update function\n", - "figure[0, 0].add_animations(update_frame)\n", - "\n", - "# Show the figure\n", - "figure.show()\n" - ] - }, - { - "cell_type": "markdown", - "id": "9a791d42-9ba1-4533-a4f2-98f191ba4ac9", - "metadata": {}, - "source": [ - "## `fastplotlib` is non-blocking!" + "figure[0, 0].add_line(data=np.column_stack((xs, cos_ys)), name=\"wave\")" ] }, { "cell_type": "code", - "execution_count": 6, - "id": "d7138f60-7812-49ca-9655-a6ecdecad5e4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'sent_frames': 123,\n", - " 'confirmed_frames': 121,\n", - " 'roundtrip': 0.05958017041860533,\n", - " 'delivery': -0.5480798748899097,\n", - " 'img_encoding': 0.007126040003315463,\n", - " 'fps': 31.430251931495025}" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "figure.canvas.get_stats()" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "8cd5298f-f5bc-480e-9956-64efd3d6bb5b", + "execution_count": 4, + "id": "38a63a8f-8726-42a9-87f9-10fd3c0e6013", "metadata": {}, "outputs": [], "source": [ - "buff = get_buffer()" + "def get_buffer():\n", + " \"\"\"\n", + " Retrieve the buffer from the socket.\n", + " \"\"\"\n", + " try:\n", + " b = socket.recv(zmq.NOBLOCK) # Non-blocking receive\n", + " return b\n", + " except zmq.Again:\n", + " return None" ] }, { "cell_type": "code", - "execution_count": 12, - "id": "c37a3f8d-092f-4d11-a352-fdc4fc7279b5", + "execution_count": 5, + "id": "19e7a8c8-e18d-4bb0-b9dc-a475d05c8138", "metadata": {}, "outputs": [], "source": [ - "a= np.frombuffer(buff, dtype=np.float64)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "3c0d3d83-f8af-4965-8984-778dedd1090f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(262145,)" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "a.shape" + "def update_frame(p):\n", + " \"\"\"\n", + " Update the frame using 2D data received from the socket and add z-coordinates.\n", + " \"\"\"\n", + " buff = get_buffer()\n", + " if buff is not None:\n", + " # Deserialize the buffer into a NumPy array\n", + " data = np.frombuffer(buff, dtype=np.float64)\n", + "\n", + " # Extract the frame number and 2D values\n", + " frame_num = int(data[0]) # First element is the frame number\n", + " values = data[1:].reshape(-1, 2) # Reshape the remaining data into 2D array [x, y]\n", + "\n", + " # Update the line plot\n", + " p[\"wave\"].data[:, :2] = values\n", + " p.name = f\"frame: {frame_num}\"" ] }, { "cell_type": "code", - "execution_count": 14, - "id": "d5ab93f1-297d-48bc-9a26-cdedc26f9669", + "execution_count": 6, + "id": "6dc5b86b-660f-4eb1-aadc-0133c5302938", "metadata": {}, "outputs": [ { "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9ce7685657a34e82a9635f93aed4c2c9", + "version_major": 2, + "version_minor": 0 + }, "text/plain": [ - "229.0" + "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" ] }, - "execution_count": 14, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "a[-1]" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "7d3474c2-9dc5-4549-9b24-7294d6d14a21", - "metadata": {}, - "outputs": [], - "source": [ - "figure[0,0][\"img\"].data = a[:-1].reshape(512, 512)" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "b28a2346-c4cf-4de7-b203-f5a8b40bfdd3", - "metadata": {}, - "outputs": [], - "source": [ - "figure[0,0].name = \"bah\"" + "# Add the animation update function\n", + "figure[0, 0].add_animations(update_frame)\n", + "\n", + "figure.show()" ] }, { "cell_type": "code", "execution_count": null, - "id": "2e8b04e6-3fe2-4b66-81bc-777fb0267350", + "id": "efe0d7c0-f69b-4842-9567-bb8d549140a4", "metadata": {}, "outputs": [], "source": [] diff --git a/demos/minimal/actors/sample_generator.py b/demos/minimal/actors/sample_generator.py index 8a057d61..74611707 100644 --- a/demos/minimal/actors/sample_generator.py +++ b/demos/minimal/actors/sample_generator.py @@ -1,5 +1,4 @@ from improv.actor import Actor, RunManager -from datetime import date # used for saving import numpy as np import logging import time # Importing time module for the delay @@ -16,49 +15,52 @@ class Generator(Actor): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.data = None self.name = "Generator" - self.frame_num = 0 + self.frame_num = 0 # Initialize frame counter + # Generate xs (x-coordinates) + self.xs = np.linspace(-10, 10, 100) def __str__(self): - return f"Name: {self.name}, Data: {self.data}" + return f"Name: {self.name}" def setup(self): - """Generates an array that serves as an initial source of data. - - Initial array is a 100 row, 5 column numpy matrix that contains - integers from 1-99, inclusive. - """ - + """Initial setup for Generator.""" logger.info("Beginning setup for Generator") - self.data = np.random.randint(10, size=(1, 5)) # Initialize data logger.info("Completed setup for Generator") def stop(self): - """Save current randint vector to a file.""" - + """Actions to perform on stopping.""" logger.info("Generator stopping") - np.save("sample_generator_data.npy", self.data) return 0 def runStep(self): - """Generates additional data after initial setup data is exhausted. - - Data is a 5x1 vector uniformly distributed in [1, 10]. - """ - + """Sends a dictionary containing frame number and 2D array with x and y coordinates.""" time.sleep(0.5) # Delay for half a second - if self.frame_num < np.shape(self.data)[0]: - data_id = self.client.put( - self.data[self.frame_num], str(f"Gen_raw: {self.frame_num}") - ) - try: - self.q_out.put([[data_id, str(self.frame_num)]]) - logger.info("Sent message on") - self.frame_num += 1 - except Exception as e: - logger.error(f"--------------------------------Generator Exception: {e}") + # Generate sine or cosine values based on frame number + if self.frame_num % 2 == 0: + # Even frame: Generate sine wave + ys = np.sin(self.xs) else: - new_data = np.random.randint(10, size=(1, 5)) - self.data = np.concatenate((self.data, new_data), axis=0) + # Odd frame: Generate cosine wave + ys = np.cos(self.xs) + + # Combine x and y into a 2D array + values = np.column_stack((self.xs, ys)) # Shape (100, 2) + + # Prepare the data to send + data_to_send = { + "frame_num": self.frame_num, + "values": values, # 2D array with x and y coordinates + } + + # Send the dictionary + try: + data_id = self.client.put(data_to_send, f"Frame: {self.frame_num}") + self.q_out.put([[data_id, f"Frame: {self.frame_num}"]]) + logger.info(f"Sent frame {self.frame_num} with x and y coordinates") + except Exception as e: + logger.error(f"Generator Exception: {e}") + + # Increment frame number + self.frame_num += 1 diff --git a/demos/minimal/actors/sample_processor.py b/demos/minimal/actors/sample_processor.py index 65306d60..0819ca21 100644 --- a/demos/minimal/actors/sample_processor.py +++ b/demos/minimal/actors/sample_processor.py @@ -1,77 +1,11 @@ -# from improv.actor import Actor -# import numpy as np -# import logging - -# logger = logging.getLogger(__name__) -# logger.setLevel(logging.INFO) - - -# class Processor(Actor): -# """Sample processor used to calculate the average of an array of integers. - -# Intended for use with sample_generator.py. -# """ - -# def __init__(self, *args, **kwargs): -# super().__init__(*args, **kwargs) - -# def setup(self): -# """Initializes all class variables. - -# self.name (string): name of the actor. -# self.frame (ObjectID): StoreInterface object id referencing data from the store. -# self.avg_list (list): list that contains averages of individual vectors. -# self.frame_num (int): index of current frame. -# """ - -# self.name = "Processor" -# self.frame = None -# self.avg_list = [] -# self.frame_num = 1 -# logger.info("Completed setup for Processor") - -# def stop(self): -# """Trivial stop function for testing purposes.""" - -# logger.info("Processor stopping") - -# def runStep(self): -# """Gets from the input queue and calculates the average. - -# Receives an ObjectID, references data in the store using that -# ObjectID, calculates the average of that data, and finally prints -# to stdout. -# """ - -# frame = None -# try: -# frame = self.q_in.get(timeout=0.001) - -# except: -# logger.error("Could not get frame!") -# pass - -# if frame is not None and self.frame_num is not None: -# self.done = False -# self.frame = self.client.getID(frame[0][0]) -# avg = np.mean(self.frame[0]) - -# # print(f"Average: {avg}") -# self.avg_list.append(avg) -# # print(f"Overall Average: {np.mean(self.avg_list)}") -# # print(f"Frame number: {self.frame_num}") -# self.frame_num += 1 - - from improv.actor import Actor from queue import Empty -import logging; - -logger = logging.getLogger(__name__) +import logging import zmq +import numpy as np +logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -import numpy as np class Processor(Actor): @@ -86,16 +20,13 @@ def setup(self): """ Creates and binds the socket for zmq. """ - self.name = "Processor" context = zmq.Context() self.socket = context.socket(zmq.PUB) self.socket.bind("tcp://127.0.0.1:5555") - self.frame_num = 1 - - logger.info('Completed setup for Processor') + logger.info("Completed setup for Processor") def stop(self): logger.info("Processor stopping") @@ -104,37 +35,33 @@ def stop(self): def runStep(self): """ - Gets the data_id to the store from the queue, fetches the frame from the data store, - take the mean, sends a memoryview so the zmq subscriber can get the buffer to update - the plot. + Receives data from the queue, prepares 2D data with x and y coordinates, + and sends it through the socket along with the frame number. """ - - frame = None - try: + # Retrieve data from the queue frame = self.q_in.get(timeout=0.05) except Empty: - pass - except: - logger.error("Could not get frame!") + return # No data received, skip this step + except Exception as e: + logger.error(f"Error retrieving frame: {e}") + return if frame is not None: - # get frame from data store - self.frame = self.client.getID(frame[0][0]) - - # do some processing - self.frame.mean() + try: + # Fetch the dictionary from the data store + data_dict = self.client.getID(frame[0][0]) - frame_ix = self.frame_num % 10 + # Extract frame number and values (2D array with x and y) + frame_num = data_dict["frame_num"] + values = data_dict["values"] # Shape (N, 2), with columns [x, y] - logger.info(self.frame.shape) + # Combine frame number and flattened data into a single array + flat_data = np.concatenate(([frame_num], values.ravel())).astype(np.float64) - # # send the buffer data and frame number as an array - out = np.concatenate( - [self.frame.ravel(), np.array([frame_ix])], # Convert frame_ix to a 1D array - dtype=np.float64 - ) + # Send the serialized buffer through the socket + self.socket.send(flat_data.tobytes()) + logger.info(f"Frame {frame_num}: Sent {values.shape[0]} points") - self.frame_num += 1 - self.socket.send(out) - logger.info(f"Processed and sent frame number {frame_ix}") \ No newline at end of file + except Exception as e: + logger.error(f"Error processing frame: {e}") From 84b5c61dd32b791761d608ac3939843dfad79cf0 Mon Sep 17 00:00:00 2001 From: Edwin Ma Date: Fri, 17 Jan 2025 00:15:06 -0500 Subject: [PATCH 10/27] updated changes, removed dictionary and reformatted output, cleaned up code --- demos/minimal/actors/fastplotlib.ipynb | 157 ++++++----------------- demos/minimal/actors/sample_generator.py | 51 +++++--- demos/minimal/actors/sample_processor.py | 46 ++++--- 3 files changed, 96 insertions(+), 158 deletions(-) diff --git a/demos/minimal/actors/fastplotlib.ipynb b/demos/minimal/actors/fastplotlib.ipynb index a2520b87..f9b83b22 100644 --- a/demos/minimal/actors/fastplotlib.ipynb +++ b/demos/minimal/actors/fastplotlib.ipynb @@ -10,44 +10,10 @@ }, { "cell_type": "code", - "execution_count": 1, - "id": "282342a1-3257-4b07-a318-81b819f8a46a", + "execution_count": null, + "id": "093c8fd4-bbf8-4374-a661-494a61a73cca", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c0f7d443919245cda4f5e18171f3f4d8", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "No windowing system present. Using surfaceless platform\n", - "No config found!\n", - "No config found!\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Available devices:\n", - "✅ (default) | NVIDIA GeForce GTX 1080 Ti | DiscreteGPU | Vulkan | 555.42.06\n", - "❗ | llvmpipe (LLVM 15.0.7, 256 bits) | CPU | Vulkan | Mesa 23.2.1-1ubuntu3.1~22.04.2 (LLVM 15.0.7)\n", - "❗ | NVIDIA GeForce GTX 1080 Ti/PCIe/SSE2 | Unknown | OpenGL | \n" - ] - } - ], + "outputs": [], "source": [ "import zmq\n", "import numpy as np\n", @@ -56,8 +22,8 @@ }, { "cell_type": "code", - "execution_count": 2, - "id": "3e98d5f9-4c5e-4b9e-a63b-bd61dbf88e8d", + "execution_count": null, + "id": "24d73c48-c24d-440b-8dfc-9c4a2f476233", "metadata": {}, "outputs": [], "source": [ @@ -65,70 +31,33 @@ "context = zmq.Context()\n", "socket = context.socket(zmq.SUB)\n", "socket.connect(\"tcp://127.0.0.1:5555\")\n", - "socket.setsockopt_string(zmq.SUBSCRIBE, \"\")" + "socket.setsockopt_string(zmq.SUBSCRIBE, \"\")\n", + "print(\"Listening...\")" ] }, { "cell_type": "code", - "execution_count": 3, - "id": "c92fbe02-fe8f-4fd0-94d3-da999b51d51e", + "execution_count": null, + "id": "fc5d4c34-3f4e-4526-b245-1db3fcd84bf5", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Listening...\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "fbbae92c1e344a9d867bcf5366acf7b5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/edwin/edwin_improv/fpl/lib/python3.10/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32\n", - " warn(f\"casting {array.dtype} array to float32\")\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "print(\"Listening...\")\n", "# Create the figure\n", "figure = fpl.Figure()\n", "\n", "# Add a line plot placeholder\n", "xs = np.linspace(-10, 10, 100)\n", "cos_ys = np.cos(xs)\n", - "figure[0, 0].add_line(data=np.column_stack((xs, cos_ys)), name=\"wave\")" + "figure[0, 0].add_line(data=np.column_stack((xs, cos_ys)), name=\"wave\")\n", + "\n", + "# Set the dimension for reshaping\n", + "dimension = 2 # Default to 2 for [x, y] data; adjust as needed" ] }, { "cell_type": "code", - "execution_count": 4, - "id": "38a63a8f-8726-42a9-87f9-10fd3c0e6013", + "execution_count": null, + "id": "8f637c0c-3854-49ce-9d19-c692d2af61e8", "metadata": {}, "outputs": [], "source": [ @@ -145,65 +74,51 @@ }, { "cell_type": "code", - "execution_count": 5, - "id": "19e7a8c8-e18d-4bb0-b9dc-a475d05c8138", + "execution_count": null, + "id": "d5240ab4-3bb4-4840-8a30-b0a60abe4aa1", "metadata": {}, "outputs": [], "source": [ "def update_frame(p):\n", " \"\"\"\n", - " Update the frame using 2D data received from the socket and add z-coordinates.\n", + " Update the frame using data received from the socket and reshape it based on the specified dimension.\n", " \"\"\"\n", " buff = get_buffer()\n", " if buff is not None:\n", " # Deserialize the buffer into a NumPy array\n", " data = np.frombuffer(buff, dtype=np.float64)\n", "\n", - " # Extract the frame number and 2D values\n", - " frame_num = int(data[0]) # First element is the frame number\n", - " values = data[1:].reshape(-1, 2) # Reshape the remaining data into 2D array [x, y]\n", + " # Extract the frame number from the last index\n", + " frame_num = int(data[-1]) # Last element is the frame number\n", + "\n", + " # Reshape the remaining data based on the specified dimension\n", + " if dimension > 0:\n", + " values = data[:-1].reshape(-1, dimension) # Reshape all but the last element\n", + " else:\n", + " values = data[:-1] # No reshaping if dimension <= 0\n", "\n", " # Update the line plot\n", - " p[\"wave\"].data[:, :2] = values\n", + " if dimension >= 2: # Ensure at least [x, y] data is available for plotting\n", + " p[\"wave\"].data[:, :dimension] = values\n", + " else:\n", + " print(f\"Received frame {frame_num}, but dimension {dimension} is insufficient for plotting.\")\n", + "\n", + " # Update the plot title with the frame number\n", " p.name = f\"frame: {frame_num}\"" ] }, { "cell_type": "code", - "execution_count": 6, - "id": "6dc5b86b-660f-4eb1-aadc-0133c5302938", + "execution_count": null, + "id": "a337ec69-1288-4c5c-a55e-c3f6a1b7bcba", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9ce7685657a34e82a9635f93aed4c2c9", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Add the animation update function\n", "figure[0, 0].add_animations(update_frame)\n", "\n", "figure.show()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "efe0d7c0-f69b-4842-9567-bb8d549140a4", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/demos/minimal/actors/sample_generator.py b/demos/minimal/actors/sample_generator.py index 74611707..bc83eabe 100644 --- a/demos/minimal/actors/sample_generator.py +++ b/demos/minimal/actors/sample_generator.py @@ -8,8 +8,7 @@ class Generator(Actor): - """Sample actor to generate data to pass into a sample processor. - + """Sample actor generate sine/cosine waves based on odd/even frame numbers respectively to pass into a sample processor. Intended for use along with sample_processor.py. """ @@ -17,48 +16,62 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.name = "Generator" self.frame_num = 0 # Initialize frame counter - # Generate xs (x-coordinates) - self.xs = np.linspace(-10, 10, 100) + self.data = None + self.max_frames = 20 # Set the limit for number of frames def __str__(self): - return f"Name: {self.name}" + return f"Name: {self.name}, Data: {self.data}" def setup(self): - """Initial setup for Generator.""" + """Initial setup for Generator""" logger.info("Beginning setup for Generator") + xs = np.linspace(-10, 10, 100) + ys = np.sin(xs) + self.data = np.column_stack((xs, ys)) logger.info("Completed setup for Generator") def stop(self): - """Actions to perform on stopping.""" + """Save current wave vector to file.""" logger.info("Generator stopping") + np.save("sample_generator_data.npy", self.data) return 0 def runStep(self): - """Sends a dictionary containing frame number and 2D array with x and y coordinates.""" - time.sleep(0.5) # Delay for half a second + """Generates additional data after initial setup data is exhausted. + + Data is a sine wave if the frame number is even or a cosine wave if the frame number is odd.""" + time.sleep(0.5) # Add a slight pause between frame generation + """Sends a flattened array with x and y coordinates followed by frame number.""" + if self.frame_num >= self.max_frames: + logger.info(f"Reached maximum frame count ({self.max_frames}). Stopping generation.") + return + + xs = np.linspace(-10, 10, 100) # Generate sine or cosine values based on frame number if self.frame_num % 2 == 0: # Even frame: Generate sine wave - ys = np.sin(self.xs) + ys = np.sin(xs) + # wave_type = "sine" else: # Odd frame: Generate cosine wave - ys = np.cos(self.xs) + ys = np.cos(xs) + # wave_type = "cosine" # Combine x and y into a 2D array - values = np.column_stack((self.xs, ys)) # Shape (100, 2) + data = np.column_stack((xs, ys)) # Shape (100, 2) + + # Flatten the 2D array to 1D + flattened_values = data.flatten() # Shape (200,) - # Prepare the data to send - data_to_send = { - "frame_num": self.frame_num, - "values": values, # 2D array with x and y coordinates - } + # Append frame_num as the last element + data_to_send = np.append(flattened_values, self.frame_num) # Shape (201,) - # Send the dictionary + # Send the flattened array with frame_num try: data_id = self.client.put(data_to_send, f"Frame: {self.frame_num}") self.q_out.put([[data_id, f"Frame: {self.frame_num}"]]) - logger.info(f"Sent frame {self.frame_num} with x and y coordinates") + # logger.info(f"Sent frame {self.frame_num} with flattened x and y coordinates ({wave_type} wave)") except Exception as e: logger.error(f"Generator Exception: {e}") diff --git a/demos/minimal/actors/sample_processor.py b/demos/minimal/actors/sample_processor.py index 0819ca21..595d5c23 100644 --- a/demos/minimal/actors/sample_processor.py +++ b/demos/minimal/actors/sample_processor.py @@ -10,7 +10,7 @@ class Processor(Actor): """ - Process data and send it through zmq to be visualized. + Process data by scaling y coordinates by 2 and send it through zmq to be visualized. """ def __init__(self, *args, **kwargs): @@ -18,9 +18,11 @@ def __init__(self, *args, **kwargs): def setup(self): """ - Creates and binds the socket for zmq. + Creates and binds the socket for zmq and initializes processed data storage. """ self.name = "Processor" + self.processed_data = None # Initialize variable to store processed data + self.frame = None # Initialize variable to store the current frame context = zmq.Context() self.socket = context.socket(zmq.PUB) @@ -35,33 +37,41 @@ def stop(self): def runStep(self): """ - Receives data from the queue, prepares 2D data with x and y coordinates, - and sends it through the socket along with the frame number. + Receives data ID from the queue, retrieves data from the Plasma store, + processes it, stores it in `processed_data`, and sends it through the + socket as flattened data with the frame number appended. """ try: - # Retrieve data from the queue - frame = self.q_in.get(timeout=0.05) + # Retrieve data ID from the queue + data_id = self.q_in.get(timeout=0.05) except Empty: return # No data received, skip this step except Exception as e: - logger.error(f"Error retrieving frame: {e}") + logger.error(f"Error retrieving data ID: {e}") return - if frame is not None: + if data_id is not None: try: - # Fetch the dictionary from the data store - data_dict = self.client.getID(frame[0][0]) + # Fetch the data from the client using the ObjectID + self.frame = self.client.getID(data_id[0][0]) # Retrieve the frame data - # Extract frame number and values (2D array with x and y) - frame_num = data_dict["frame_num"] - values = data_dict["values"] # Shape (N, 2), with columns [x, y] + # Unpack the flattened data + data = np.array(self.frame, dtype=np.float64) # Ensure it's a NumPy array + values = data[:-1] # Exclude the last element + frame_num = int(data[-1]) # Extract the last element as frame number - # Combine frame number and flattened data into a single array - flat_data = np.concatenate(([frame_num], values.ravel())).astype(np.float64) + # Reshape values to 2D array (N, 2) for processing + values = values.reshape(-1, 2) - # Send the serialized buffer through the socket - self.socket.send(flat_data.tobytes()) - logger.info(f"Frame {frame_num}: Sent {values.shape[0]} points") + # Perform processing (e.g., scaling the y-values) + values[:, 1] *= 2 # Example: Scale y-coordinates by 2 + + # Flatten processed values and append frame number + self.processed_data = np.append(values.flatten(), frame_num) + + # Send the processed data through the ZMQ socket + self.socket.send(self.processed_data.tobytes()) + # logger.info(f"Frame {frame_num}: Sent {values.shape[0]} points after processing") except Exception as e: logger.error(f"Error processing frame: {e}") From 1cfdf999138850b270483a11d143f24774c70a11 Mon Sep 17 00:00:00 2001 From: Edwin Ma Date: Mon, 20 Jan 2025 01:34:38 -0500 Subject: [PATCH 11/27] Added lorenz_data generation, processing, and visualization sample --- demos/minimal/actors/lorenz_fpl.ipynb | 146 +++++++++++++++++++ demos/minimal/actors/lorenz_generator.py | 96 ++++++++++--- demos/minimal/actors/lorenz_processor.py | 174 ++++++----------------- 3 files changed, 263 insertions(+), 153 deletions(-) create mode 100644 demos/minimal/actors/lorenz_fpl.ipynb diff --git a/demos/minimal/actors/lorenz_fpl.ipynb b/demos/minimal/actors/lorenz_fpl.ipynb new file mode 100644 index 00000000..b8322081 --- /dev/null +++ b/demos/minimal/actors/lorenz_fpl.ipynb @@ -0,0 +1,146 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "068919b3-0a3a-47ff-836b-221079d6e095", + "metadata": {}, + "outputs": [], + "source": [ + "import zmq\n", + "import numpy as np\n", + "import fastplotlib as fpl" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c665d7f4-d7eb-4ef7-a9c8-d737b9f0b073", + "metadata": {}, + "outputs": [], + "source": [ + "# ZMQ context and socket setup\n", + "context = zmq.Context()\n", + "socket = context.socket(zmq.SUB)\n", + "socket.connect(\"tcp://127.0.0.1:5555\")\n", + "socket.setsockopt_string(zmq.SUBSCRIBE, \"\")\n", + "print(\"Listening...\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "147e4be8-fb62-4463-bf6f-0a8e32593c4e", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the figure\n", + "figure = fpl.Figure()\n", + "\n", + "# Placeholder for the first data reception\n", + "is_first_data = True\n", + "\n", + "# Set the dimension for reshaping\n", + "dimension = 2 # Default to 2 for [x, y] data; adjust as needed" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28f334db-aadb-40fc-b067-c5b4c4a146d1", + "metadata": {}, + "outputs": [], + "source": [ + "def get_buffer():\n", + " \"\"\"\n", + " Retrieve the buffer from the socket.\n", + " \"\"\"\n", + " try:\n", + " b = socket.recv(zmq.NOBLOCK) # Non-blocking receive\n", + " return b\n", + " except zmq.Again:\n", + " return None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c3ac076-d4a8-4877-8843-c5e6cc54a20c", + "metadata": {}, + "outputs": [], + "source": [ + "def update_frame(p):\n", + " \"\"\"\n", + " Update the frame using data received from the socket and reshape it based on the specified dimension.\n", + " \"\"\"\n", + " global is_first_data\n", + "\n", + " buff = get_buffer()\n", + " if buff is not None:\n", + " # Deserialize the buffer into a NumPy array\n", + " data = np.frombuffer(buff, dtype=np.float64)\n", + "\n", + " # Extract the frame number from the last index\n", + " frame_num = int(data[-1]) # Last element is the frame number\n", + "\n", + " # Reshape the remaining data based on the specified dimension\n", + " if dimension > 0:\n", + " values = data[:-1].reshape(-1, dimension) # Reshape all but the last element\n", + " else:\n", + " values = data[:-1] # No reshaping if dimension <= 0\n", + "\n", + " if is_first_data:\n", + " # Initialize the plot with the appropriate number of points\n", + " n_points = (len(data) - 1) // dimension\n", + " xs = np.linspace(-10, 10, n_points)\n", + " ys = np.zeros_like(xs)\n", + " figure[0, 0].add_line(data=np.column_stack((xs, ys)), name=\"wave\")\n", + " is_first_data = False\n", + "\n", + " # Update the line plot\n", + " if dimension >= 2: # Ensure at least [x, y] data is available for plotting\n", + " p[\"wave\"].data[:, :dimension] = values\n", + " \n", + " else:\n", + " print(f\"Received frame {frame_num}, but dimension {dimension} is insufficient for plotting.\")\n", + "\n", + " # Update the plot title with the frame number\n", + " p.name = f\"frame: {frame_num}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "439abe77-263c-4ab4-b810-5c7d5e6029e1", + "metadata": {}, + "outputs": [], + "source": [ + "# Add the animation update function\n", + "figure[0, 0].add_animations(update_frame)\n", + "\n", + "figure.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (fpl)", + "language": "python", + "name": "fpl" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demos/minimal/actors/lorenz_generator.py b/demos/minimal/actors/lorenz_generator.py index 0a86fbc0..ee71f7cb 100644 --- a/demos/minimal/actors/lorenz_generator.py +++ b/demos/minimal/actors/lorenz_generator.py @@ -10,48 +10,102 @@ class Generator(Actor): """ - Generates the initial coordinates for the Lorenz system. - Intended to provide initial data for the LorenzProcessor. + Generates coordinates for the Lorenz system in real time. + Computes the next Lorenz coordinates every half second. + Outputs a flattened array with progressively filled 100 (x, y) pairs + and appends the frame number at the end. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.data = None + self.data = None # Placeholder for the 2D array + self.coordinates = [] self.name = "lorenz_generator" - self.frame_num = 0 + self.dt = 0.01 # Time step for numerical integration + self.max_points = 1000 # Total number of (x, y) pairs + self.points_per_frame = 10 # Number of points to add per frame def __str__(self): - return f"Name: {self.name}, Data: {self.data}" + return f"Name: {self.name}, Current Coordinates: {self.coordinates[-1] if self.coordinates else None}" def setup(self): """ - Initializes the Lorenz system with the starting coordinates. + Initializes the Lorenz system with the starting coordinates and the data array. """ logger.info("Beginning setup for LorenzGenerator") - self.data = np.array([[1.0, 1.0, 1.0]]) # Initial coordinates (x, y, z) - logger.info(f"Initialized Lorenz system with coordinates: {self.data}") + initial_coordinate = np.array([1.0, 1.0, 1.0]) # Initial coordinates (x, y, z) + self.coordinates = [initial_coordinate] + + # Initialize the data array as a 2D array (100 rows for x, y coordinates) + self.data = np.zeros((self.max_points, 2)) # Shape (100, 2) + self.frame_num = 0 + self.current_index = 0 # Tracks the current position to fill in the data arrayx + logger.info(f"Initialized Lorenz system with initial coordinates: {initial_coordinate}") def stop(self): """ - Save the current Lorenz coordinates to a file for persistence. + Save the last Lorenz coordinates to a file for persistence. """ logger.info("LorenzGenerator stopping") - np.save("lorenz_initial_data.npy", self.data) + np.save("lorenz_last_coordinate.npy", self.coordinates[-1]) return 0 def runStep(self): """ - Sends the initial Lorenz system coordinates. + Generates the next 10 Lorenz coordinates and fills them into the 2D data array. + Sends the progressively filled data as a flattened array with the frame number appended. """ time.sleep(0.5) # Delay for half a second - if self.frame_num < np.shape(self.data)[0]: - data_id = self.client.put( - self.data[self.frame_num], str(f"Lorenz_Initial: {self.frame_num}") - ) - try: - self.q_out.put([[data_id, str(self.frame_num)]]) - logger.info(f"Sent initial Lorenz coordinate: {self.data[self.frame_num]}") - self.frame_num += 1 - except Exception as e: - logger.error(f"LorenzGenerator Exception: {e}") + try: + # Add the next 10 points + for _ in range(self.points_per_frame): + if self.current_index >= self.max_points: + logger.info(f"Data array fully filled for frame {self.frame_num}.") + break # Stop filling if all 100 points are generated + + # Compute the next coordinate + derivative = lorenz(self.coordinates[-1]) + next_coordinate = self.coordinates[-1] + derivative * self.dt + self.coordinates.append(next_coordinate) + + # Fill x and y coordinates into the 2D data array + self.data[self.current_index, 0] = next_coordinate[0] # x-coordinate + self.data[self.current_index, 1] = next_coordinate[1] # y-coordinate + + self.current_index += 1 # Increment the index for the next point + + # Flatten the 2D array and append the frame number + flattened_data = self.data.flatten() # Shape (200,) + data_to_send = np.append(flattened_data, self.frame_num) # Shape (201,) + + # Send the data + data_id = self.client.put(data_to_send, str(f"Lorenz_Frame: {self.frame_num}")) + self.q_out.put([[data_id, str(self.frame_num)]]) + logger.info(f"Generated Lorenz frame {self.frame_num} with progressively filled data array.") + + # Increment frame number for the next step + self.frame_num += 1 + except Exception as e: + logger.error(f"LorenzGenerator Exception: {e}") + + +def lorenz(xyz, s=10, r=28, b=2.667): + """ + Parameters + ---------- + xyz : array-like, shape (3,) + Point of interest in three-dimensional space. + s, r, b : float + Parameters defining the Lorenz attractor. + + Returns + ------- + xyz_dot : array, shape (3,) + Values of the Lorenz attractor's partial derivatives at *xyz*. + """ + x, y, z = xyz + x_dot = s * (y - x) + y_dot = r * x - y - x * z + z_dot = x * y - b * z + return np.array([x_dot, y_dot, z_dot]) diff --git a/demos/minimal/actors/lorenz_processor.py b/demos/minimal/actors/lorenz_processor.py index 8b3553df..d967ccc2 100644 --- a/demos/minimal/actors/lorenz_processor.py +++ b/demos/minimal/actors/lorenz_processor.py @@ -8,30 +8,11 @@ logger.setLevel(logging.INFO) -def lorenz(xyz, s=10, r=28, b=2.667): - """ - Parameters - ---------- - xyz : array-like, shape (3,) - Point of interest in three-dimensional space. - s, r, b : float - Parameters defining the Lorenz attractor. - - Returns - ------- - xyz_dot : array, shape (3,) - Values of the Lorenz attractor's partial derivatives at *xyz*. - """ - x, y, z = xyz - x_dot = s * (y - x) - y_dot = r * x - y - x * z - z_dot = x * y - b * z - return np.array([x_dot, y_dot, z_dot]) - - class Processor(Actor): """ - Process data, calculate updated Lorenz coordinates, and send them via ZMQ. + Processes Lorenz data by performing custom transformations on the coordinates + (e.g., scaling and applying mathematical operations) and sends the processed + data through a ZMQ socket for visualization. """ def __init__(self, *args, **kwargs): @@ -39,139 +20,68 @@ def __init__(self, *args, **kwargs): def setup(self): """ - Creates and binds the socket for ZMQ and initializes the Lorenz system. + Sets up the ZMQ socket and initializes storage for processed data. """ - self.name = "lorenz_processor" + self.name = "Processor" + self.processed_data = None # Storage for processed data + self.frame = None # Storage for the current frame # Set up ZMQ PUB socket context = zmq.Context() self.socket = context.socket(zmq.PUB) self.socket.bind("tcp://127.0.0.1:5555") - self.frame_num = 1 - self.current_coords = None # Will be initialized with generator data - self.dt = 0.01 # Small time step for Euler's method - - logger.info("Completed setup for Processor") + logger.info("Processor setup completed. ZMQ PUB socket bound to tcp://127.0.0.1:5555") def stop(self): + """ + Closes the ZMQ socket. + """ logger.info("Processor stopping") self.socket.close() return 0 def runStep(self): """ - Receives initial coordinates from the Generator, calculates updated Lorenz coordinates, - and sends them via ZMQ. + Processes incoming Lorenz data, applies transformations, and sends + the processed data through a ZMQ socket. """ - if self.current_coords is None: - # Get initial coordinates from the queue - try: - frame = self.q_in.get(timeout=0.05) - data_id = frame[0][0] # Data ID in the store - # Fetch data and make a writable copy - self.current_coords = np.array(self.client.getID(data_id), copy=True) - - logger.info(f"Initialized Lorenz coordinates: {self.current_coords}") - - except Empty: - logger.info("Waiting for initial Lorenz coordinates...") - return - except Exception as e: - logger.error(f"Failed to initialize Lorenz coordinates: {e}") - return - try: - # Calculate the Lorenz derivatives - derivatives = lorenz(self.current_coords) - - # Update coordinates using Euler's method - self.current_coords += derivatives * self.dt - frame_ix = self.frame_num - - logger.info(f"Frame {frame_ix}: Updated Lorenz coordinates: {self.current_coords}") - - # Combine the updated coordinates and frame index for sending - out = np.concatenate([self.current_coords, [frame_ix]], dtype=np.float64) - - # Send the data via ZMQ - self.socket.send(out.tobytes()) - logger.info(f"Sent frame {frame_ix}: {self.current_coords}") - - self.frame_num += 1 - + # Retrieve data ID from the input queue + data_id = self.q_in.get(timeout=0.05) + except Empty: + return # No data received, skip this step except Exception as e: - logger.error(f"Error during processing: {e}") - - -# from improv.actor import Actor -# from queue import Empty -# import logging -# import zmq -# import numpy as np - -# logger = logging.getLogger(__name__) -# logger.setLevel(logging.INFO) - - -# class Processor(Actor): -# """ -# Process data and send it through zmq to be visualized in Jupyter Notebook. -# """ + logger.error(f"Error retrieving data ID: {e}") + return -# def __init__(self, *args, **kwargs): -# super().__init__(*args, **kwargs) - -# def setup(self): -# """ -# Creates and binds the socket for zmq. -# """ -# self.name = "lorenz_processor" - -# # Set up ZMQ PUB socket -# context = zmq.Context() -# self.socket = context.socket(zmq.PUB) -# self.socket.bind("tcp://127.0.0.1:5555") - -# self.frame_num = 1 - -# logger.info("Completed setup for Processor") - -# def stop(self): -# logger.info("Processor stopping") -# self.socket.close() -# return 0 + if data_id is not None: + try: + # Fetch the data from the client using the ObjectID + self.frame = self.client.getID(data_id[0][0]) # Retrieve the frame data -# def runStep(self): -# """ -# Fetches data from the queue, processes it, and sends it via ZMQ. -# """ -# frame = None -# try: -# frame = self.q_in.get(timeout=0.05) -# except Empty: -# logger.info("Queue is empty; no frame to process.") -# except Exception as e: -# logger.error(f"Could not get frame! Exception: {e}") + # Convert the frame data to a NumPy array + data = np.array(self.frame, dtype=np.float64) # Ensure it's a NumPy array + values = data[:-1] # Exclude the last element (frame number) + frame_num = int(data[-1]) # Extract the last element as the frame number -# if frame is not None: -# try: -# # Fetch the data from the store -# self.frame = self.client.getID(frame[0][0]) -# coordinates = self.frame.ravel() -# frame_ix = self.frame_num + # Reshape values to 2D array (N, 2) for processing + values = values.reshape(-1, 2) -# logger.info(f"Retrieved coordinates: {coordinates}") -# logger.info(f"Sending frame {frame_ix}") + # Perform processing on the Lorenz coordinates + # Example 1: Scale x-coordinates by 0.5 and y-coordinates by 2 + values[:, 0] *= 2 # Scale x-coordinates + values[:, 1] *= 2 # Scale y-coordinates -# # Combine the coordinates and frame index for sending -# out = np.concatenate([coordinates, [frame_ix]], dtype=np.float64) + # Example 2: Add sinusoidal noise to the y-coordinates + values[:, 1] += np.sin(values[:, 0]) -# # Send the data -# self.socket.send(out.tobytes()) # Send as bytes -# logger.info(f"Sent frame {frame_ix}: {coordinates}") + # Flatten processed values and append the frame number + self.processed_data = np.append(values.flatten(), frame_num) -# self.frame_num += 1 + # Send the processed data through the ZMQ socket + self.socket.send(self.processed_data.tobytes()) + logger.info(f"Frame {frame_num}: Sent {values.shape[0]} points after processing") -# except Exception as e: -# logger.error(f"Error during processing: {e}") + except Exception as e: + logger.error(f"Error processing frame: {e}") From f0f550c7c89567c1fe961b027090bb51be14921d Mon Sep 17 00:00:00 2001 From: Edwin Ma Date: Tue, 21 Jan 2025 17:39:22 -0500 Subject: [PATCH 12/27] updated lorenz data generation --- demos/minimal/actors/lorenz_fpl.ipynb | 17 ++++++++++--- demos/minimal/actors/lorenz_generator.py | 32 +++++++++++++----------- demos/minimal/actors/lorenz_processor.py | 21 ++++++++-------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/demos/minimal/actors/lorenz_fpl.ipynb b/demos/minimal/actors/lorenz_fpl.ipynb index b8322081..86443135 100644 --- a/demos/minimal/actors/lorenz_fpl.ipynb +++ b/demos/minimal/actors/lorenz_fpl.ipynb @@ -35,13 +35,16 @@ "outputs": [], "source": [ "# Create the figure\n", - "figure = fpl.Figure()\n", + "figure = fpl.Figure(\n", + " cameras=\"3d\",\n", + " controller_types=\"fly\",\n", + ")\n", "\n", "# Placeholder for the first data reception\n", "is_first_data = True\n", "\n", "# Set the dimension for reshaping\n", - "dimension = 2 # Default to 2 for [x, y] data; adjust as needed" + "dimension = 3 # Default to 2 for [x, y] data; adjust as needed" ] }, { @@ -93,10 +96,16 @@ " # Initialize the plot with the appropriate number of points\n", " n_points = (len(data) - 1) // dimension\n", " xs = np.linspace(-10, 10, n_points)\n", - " ys = np.zeros_like(xs)\n", - " figure[0, 0].add_line(data=np.column_stack((xs, ys)), name=\"wave\")\n", + " \n", + " # Create additional columns of zeros based on the number of dimensions\n", + " columns = [xs] + [np.zeros_like(xs) for _ in range(dimension - 1)]\n", + " formatted_data = np.column_stack(columns)\n", + " \n", + " # Add the line to the plot with the generated data\n", + " figure[0, 0].add_line(data=formatted_data, name=\"wave\", cmap='jet')\n", " is_first_data = False\n", "\n", + "\n", " # Update the line plot\n", " if dimension >= 2: # Ensure at least [x, y] data is available for plotting\n", " p[\"wave\"].data[:, :dimension] = values\n", diff --git a/demos/minimal/actors/lorenz_generator.py b/demos/minimal/actors/lorenz_generator.py index ee71f7cb..9ea79f4d 100644 --- a/demos/minimal/actors/lorenz_generator.py +++ b/demos/minimal/actors/lorenz_generator.py @@ -7,12 +7,11 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) - class Generator(Actor): """ Generates coordinates for the Lorenz system in real time. Computes the next Lorenz coordinates every half second. - Outputs a flattened array with progressively filled 100 (x, y) pairs + Outputs a flattened array with progressively filled 100 (x, y, z) triplets and appends the frame number at the end. """ @@ -22,24 +21,29 @@ def __init__(self, *args, **kwargs): self.coordinates = [] self.name = "lorenz_generator" self.dt = 0.01 # Time step for numerical integration - self.max_points = 1000 # Total number of (x, y) pairs + self.max_points = 1000 # Total number of (x, y, z) triplets self.points_per_frame = 10 # Number of points to add per frame def __str__(self): return f"Name: {self.name}, Current Coordinates: {self.coordinates[-1] if self.coordinates else None}" def setup(self): - """ - Initializes the Lorenz system with the starting coordinates and the data array. + """Initializes all class variables. + + self.coordinates (list): List containing the initial 3D coordinate [x, y, z]. + self.data (ndarray): 2D NumPy array initialized with zeros to store x, y, z coordinates + for a maximum of `self.max_points` rows. + self.frame_num (int): Index of the current frame, initialized to 0. + self.current_index (int): Tracks the current position in the `self.data` array for filling new values. """ logger.info("Beginning setup for LorenzGenerator") initial_coordinate = np.array([1.0, 1.0, 1.0]) # Initial coordinates (x, y, z) self.coordinates = [initial_coordinate] - # Initialize the data array as a 2D array (100 rows for x, y coordinates) - self.data = np.zeros((self.max_points, 2)) # Shape (100, 2) + # Initialize the data array as a 2D array (100 rows for x, y, z coordinates) + self.data = np.zeros((self.max_points, 3)) # Shape (1000, 3) self.frame_num = 0 - self.current_index = 0 # Tracks the current position to fill in the data arrayx + self.current_index = 0 # Tracks the current position to fill in the data array logger.info(f"Initialized Lorenz system with initial coordinates: {initial_coordinate}") def stop(self): @@ -62,34 +66,32 @@ def runStep(self): for _ in range(self.points_per_frame): if self.current_index >= self.max_points: logger.info(f"Data array fully filled for frame {self.frame_num}.") - break # Stop filling if all 100 points are generated + break # Stop filling if all 1000 points are generated # Compute the next coordinate derivative = lorenz(self.coordinates[-1]) next_coordinate = self.coordinates[-1] + derivative * self.dt self.coordinates.append(next_coordinate) - # Fill x and y coordinates into the 2D data array + # Fill x, y, and z coordinates into the 2D data array self.data[self.current_index, 0] = next_coordinate[0] # x-coordinate self.data[self.current_index, 1] = next_coordinate[1] # y-coordinate + self.data[self.current_index, 2] = next_coordinate[2] # z-coordinate self.current_index += 1 # Increment the index for the next point - # Flatten the 2D array and append the frame number - flattened_data = self.data.flatten() # Shape (200,) - data_to_send = np.append(flattened_data, self.frame_num) # Shape (201,) + # Flatten the 2D array and append the frame number in one step + data_to_send = np.append(np.ravel(self.data), self.frame_num) # Send the data data_id = self.client.put(data_to_send, str(f"Lorenz_Frame: {self.frame_num}")) self.q_out.put([[data_id, str(self.frame_num)]]) - logger.info(f"Generated Lorenz frame {self.frame_num} with progressively filled data array.") # Increment frame number for the next step self.frame_num += 1 except Exception as e: logger.error(f"LorenzGenerator Exception: {e}") - def lorenz(xyz, s=10, r=28, b=2.667): """ Parameters diff --git a/demos/minimal/actors/lorenz_processor.py b/demos/minimal/actors/lorenz_processor.py index d967ccc2..4ae1ff62 100644 --- a/demos/minimal/actors/lorenz_processor.py +++ b/demos/minimal/actors/lorenz_processor.py @@ -35,7 +35,7 @@ def setup(self): def stop(self): """ - Closes the ZMQ socket. + Trivial stop function for testing purposes. """ logger.info("Processor stopping") self.socket.close() @@ -62,26 +62,25 @@ def runStep(self): # Convert the frame data to a NumPy array data = np.array(self.frame, dtype=np.float64) # Ensure it's a NumPy array - values = data[:-1] # Exclude the last element (frame number) frame_num = int(data[-1]) # Extract the last element as the frame number - - # Reshape values to 2D array (N, 2) for processing - values = values.reshape(-1, 2) + data = data[:-1].reshape(-1, 3) # Exclude the last element (frame number) + # Perform processing on the Lorenz coordinates # Example 1: Scale x-coordinates by 0.5 and y-coordinates by 2 - values[:, 0] *= 2 # Scale x-coordinates - values[:, 1] *= 2 # Scale y-coordinates + data[:, 0] *= 2 # Scale x-coordinates + data[:, 1] *= 2 # Scale y-coordinates + data[:, 2] *= 2 # Scale z-coordinates # Example 2: Add sinusoidal noise to the y-coordinates - values[:, 1] += np.sin(values[:, 0]) + data[:, 1] += np.sin(data[:, 0]) - # Flatten processed values and append the frame number - self.processed_data = np.append(values.flatten(), frame_num) + # Flatten processed values and append frame number + self.processed_data = np.append(np.ravel(data), frame_num) # Send the processed data through the ZMQ socket self.socket.send(self.processed_data.tobytes()) - logger.info(f"Frame {frame_num}: Sent {values.shape[0]} points after processing") + logger.info(f"Frame {frame_num}: Sent points with size {data.shape} after processing") except Exception as e: logger.error(f"Error processing frame: {e}") From 02d41fed7fb38a9923b88036d9cca6de77984eed Mon Sep 17 00:00:00 2001 From: edwin-ma26 <69403205+edwin-ma26@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:42:29 -0500 Subject: [PATCH 13/27] Delete demos/minimal/actors/updating_graphic.ipynb --- demos/minimal/actors/updating_graphic.ipynb | 91 --------------------- 1 file changed, 91 deletions(-) delete mode 100644 demos/minimal/actors/updating_graphic.ipynb diff --git a/demos/minimal/actors/updating_graphic.ipynb b/demos/minimal/actors/updating_graphic.ipynb deleted file mode 100644 index 483513d7..00000000 --- a/demos/minimal/actors/updating_graphic.ipynb +++ /dev/null @@ -1,91 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 3, - "id": "3e3d7881-bf2c-40f1-943e-988c15f427da", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9f2c5dec0cea4b8e8c4eb5d7b86a3f89", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "092e0f456911455aa7d34084541d26ed", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import numpy as np\n", - "import fastplotlib as fpl\n", - "import time\n", - "\n", - "# Create the figure\n", - "figure = fpl.Figure()\n", - "\n", - "# Add a line plot placeholder\n", - "line_data = np.array([[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 3, 0]], dtype=np.float32)\n", - "new_line_data = np.array([[0, 1, 0], [1, 0, 0], [0, 1, 0], [1, 0, 0]], dtype=np.float32)\n", - "\n", - "line_graphic = figure[0, 0].add_line(data=line_data, name=\"line\")\n", - "figure.show()\n", - "\n", - "time.sleep(2)\n", - "figure[0,0].remove_graphic(line_graphic)\n", - "\n", - "new_line_graphic = figure[0, 0].add_line(data=new_line_data, name=\"new_line\")\n", - "figure.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "108675b8-3f99-4a92-adda-507116ea810c", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 497b8702b4b816947dd2b00fe7fd567c4f5d9683 Mon Sep 17 00:00:00 2001 From: edwin-ma26 <69403205+edwin-ma26@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:42:41 -0500 Subject: [PATCH 14/27] Delete demos/minimal/actors/lorenz.ipynb --- demos/minimal/actors/lorenz.ipynb | 228 ------------------------------ 1 file changed, 228 deletions(-) delete mode 100644 demos/minimal/actors/lorenz.ipynb diff --git a/demos/minimal/actors/lorenz.ipynb b/demos/minimal/actors/lorenz.ipynb deleted file mode 100644 index a040c1d7..00000000 --- a/demos/minimal/actors/lorenz.ipynb +++ /dev/null @@ -1,228 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "98c54909-f50a-42f6-a49c-8f249a279e19", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6037601cdd0d473ba4bfb0a9c639e841", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "No windowing system present. Using surfaceless platform\n", - "No config found!\n", - "No config found!\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Available devices:\n", - "✅ (default) | NVIDIA GeForce GTX 1080 Ti | DiscreteGPU | Vulkan | 555.42.06\n", - "❗ | llvmpipe (LLVM 15.0.7, 256 bits) | CPU | Vulkan | Mesa 23.2.1-1ubuntu3.1~22.04.2 (LLVM 15.0.7)\n", - "❗ | NVIDIA GeForce GTX 1080 Ti/PCIe/SSE2 | Unknown | OpenGL | \n", - "Listening for updated Lorenz coordinates...\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "261fac1ee2ee4218a9f20e74340982ce", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/edwin/edwin_improv/fpl/lib/python3.10/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32\n", - " warn(f\"casting {array.dtype} array to float32\")\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Automatically created module for IPython interactive environment\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "665ab12b09854dc4acb1d1fe23593340", - "version_major": 2, - "version_minor": 0 - }, - "text/html": [ - "
snapshot
" - ], - "text/plain": [ - "JupyterWgpuCanvas(css_height='560px', css_width='700px')" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import zmq\n", - "import numpy as np\n", - "import fastplotlib as fpl\n", - "import time # Import time module\n", - "\n", - "# Set up ZMQ subscriber\n", - "context = zmq.Context()\n", - "socket = context.socket(zmq.SUB)\n", - "socket.connect(\"tcp://127.0.0.1:5555\")\n", - "socket.setsockopt_string(zmq.SUBSCRIBE, \"\")\n", - "\n", - "print(\"Listening for updated Lorenz coordinates...\")\n", - "\n", - "try:\n", - " # Example num_steps (adjust based on your use case)\n", - " num_steps = 100 # This must match the expected size of xyzs\n", - " figure = fpl.Figure(\n", - " cameras=\"3d\",\n", - " controller_types=\"fly\",\n", - " size=(700, 560)\n", - " )\n", - " # Initialize coordinates array with zeros\n", - " lorenz_data = np.zeros((1, num_steps + 200, 3), dtype=np.float64)\n", - " # Keep track of the current frame index being filled\n", - " current_index = 0\n", - " # line_graphic = figure[0, 0].add_line(data=coordinates_array, name=\"line\")\n", - " while current_index < num_steps + 1:\n", - " # Receive raw message\n", - " msg = socket.recv()\n", - "\n", - " # Convert the message back to a numpy array\n", - " data = np.frombuffer(msg, dtype=np.float64)\n", - "\n", - " # Separate coordinates and frame index\n", - " coordinates = data[:-1] # All except last element\n", - " frame_index = int(data[-1]) # Last element is the frame index\n", - "\n", - " # Fill the corresponding row in the coordinates array\n", - " lorenz_data[0][current_index] = coordinates\n", - " \n", - " # Increment the current index\n", - " current_index += 1\n", - " \n", - " lorenz_line = figure[0, 0].add_line_collection(data=lorenz_data, thickness=.1, cmap=\"tab10\")\n", - " figure.show()\n", - "\n", - " # Wait for 2 seconds before showing the updated figure\n", - " time.sleep(2)\n", - "\n", - " figure.clear()\n", - "\n", - " while current_index < num_steps + 100:\n", - " # Receive raw message\n", - " msg = socket.recv()\n", - "\n", - " # Convert the message back to a numpy array\n", - " data = np.frombuffer(msg, dtype=np.float64)\n", - "\n", - " # Separate coordinates and frame index\n", - " coordinates = data[:-1] # All except last element\n", - " frame_index = int(data[-1]) # Last element is the frame index\n", - "\n", - " # Fill the corresponding row in the coordinates array\n", - " lorenz_data[0][current_index] = coordinates\n", - " \n", - " # Increment the current index\n", - " current_index += 1\n", - "\n", - " new_lorenz_line = figure[0, 0].add_line_collection(data=lorenz_data, thickness=.1, cmap=\"tab10\")\n", - "\n", - " figure.show()\n", - " \n", - " # Set initial camera position to make animation in gallery render better\n", - " figure[0, 0].camera.world.z = 80\n", - " \n", - " # NOTE: `if __name__ == \"__main__\"` is NOT how to use fastplotlib interactively\n", - " # please see our docs for using fastplotlib interactively in ipython and jupyter\n", - " if __name__ == \"__main__\":\n", - " print(__doc__)\n", - " fpl.run()\n", - " # Print the size of coordinates_array\n", - " # print(\"Coordinates collection complete:\")\n", - " # print(\"Size of coordinates_array:\", coordinates_array.shape)\n", - "\n", - "except KeyboardInterrupt:\n", - " print(\"Stopped listening.\")\n", - "finally:\n", - " socket.close()\n", - " context.term()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1df0f598-31d0-4a79-828b-f3ee2a43cc3a", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0dc63fe3-b01c-4176-aac2-5bbc2518dfe0", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "acc364fa-01e8-4c50-94ab-e4ee638bb2ae", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 593bc22fa7689cb19281388adcfe32b6fcc51060 Mon Sep 17 00:00:00 2001 From: Edwin Ma Date: Thu, 23 Jan 2025 15:43:57 -0500 Subject: [PATCH 15/27] removed sine wave processing --- demos/minimal/actors/lorenz_processor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/demos/minimal/actors/lorenz_processor.py b/demos/minimal/actors/lorenz_processor.py index 4ae1ff62..14e78949 100644 --- a/demos/minimal/actors/lorenz_processor.py +++ b/demos/minimal/actors/lorenz_processor.py @@ -72,9 +72,6 @@ def runStep(self): data[:, 1] *= 2 # Scale y-coordinates data[:, 2] *= 2 # Scale z-coordinates - # Example 2: Add sinusoidal noise to the y-coordinates - data[:, 1] += np.sin(data[:, 0]) - # Flatten processed values and append frame number self.processed_data = np.append(np.ravel(data), frame_num) From a8dc73713c6cf6d61f8c4a0fa2695e043cd1174f Mon Sep 17 00:00:00 2001 From: Edwin Ma Date: Fri, 24 Jan 2025 13:50:00 -0500 Subject: [PATCH 16/27] updated sine/cosine --- demos/minimal/actors/fastplotlib.ipynb | 109 ++++++++++++++++++++--- demos/minimal/actors/sample_generator.py | 36 ++++---- demos/minimal/actors/sample_processor.py | 13 ++- 3 files changed, 122 insertions(+), 36 deletions(-) diff --git a/demos/minimal/actors/fastplotlib.ipynb b/demos/minimal/actors/fastplotlib.ipynb index f9b83b22..fbc39ea8 100644 --- a/demos/minimal/actors/fastplotlib.ipynb +++ b/demos/minimal/actors/fastplotlib.ipynb @@ -10,10 +10,44 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "093c8fd4-bbf8-4374-a661-494a61a73cca", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0edc24b838634ad9b112e927d018c658", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "No windowing system present. Using surfaceless platform\n", + "No config found!\n", + "No config found!\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Available devices:\n", + "✅ (default) | NVIDIA GeForce GTX 1080 Ti | DiscreteGPU | Vulkan | 555.42.06\n", + "❗ | llvmpipe (LLVM 15.0.7, 256 bits) | CPU | Vulkan | Mesa 23.2.1-1ubuntu3.1~22.04.2 (LLVM 15.0.7)\n", + "❗ | NVIDIA GeForce GTX 1080 Ti/PCIe/SSE2 | Unknown | OpenGL | \n" + ] + } + ], "source": [ "import zmq\n", "import numpy as np\n", @@ -22,10 +56,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "24d73c48-c24d-440b-8dfc-9c4a2f476233", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Listening...\n" + ] + } + ], "source": [ "# ZMQ context and socket setup\n", "context = zmq.Context()\n", @@ -37,10 +79,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "fc5d4c34-3f4e-4526-b245-1db3fcd84bf5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f32b77ed6e5042e68c77134a54711c97", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/edwin/edwin_improv/fpl/lib/python3.10/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32\n", + " warn(f\"casting {array.dtype} array to float32\")\n" + ] + } + ], "source": [ "# Create the figure\n", "figure = fpl.Figure()\n", @@ -56,7 +121,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "8f637c0c-3854-49ce-9d19-c692d2af61e8", "metadata": {}, "outputs": [], @@ -74,7 +139,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "d5240ab4-3bb4-4840-8a30-b0a60abe4aa1", "metadata": {}, "outputs": [], @@ -109,16 +174,40 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "a337ec69-1288-4c5c-a55e-c3f6a1b7bcba", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0fb5f408f5c14338b537bc78363b867e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Add the animation update function\n", "figure[0, 0].add_animations(update_frame)\n", "\n", "figure.show()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b134c339-4570-413f-ad2d-1fd709434a3a", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/demos/minimal/actors/sample_generator.py b/demos/minimal/actors/sample_generator.py index bc83eabe..57addf44 100644 --- a/demos/minimal/actors/sample_generator.py +++ b/demos/minimal/actors/sample_generator.py @@ -8,28 +8,34 @@ class Generator(Actor): - """Sample actor generate sine/cosine waves based on odd/even frame numbers respectively to pass into a sample processor. - Intended for use along with sample_processor.py. + """Sample actor to generate a sine/cosine wave based on frame number to pass into a sample processor. Odd frames generate a sine wave and even frames generate a cosine wave. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.name = "Generator" - self.frame_num = 0 # Initialize frame counter self.data = None - self.max_frames = 20 # Set the limit for number of frames + self.max_frames = 500 # Set the limit for number of frames def __str__(self): return f"Name: {self.name}, Data: {self.data}" def setup(self): - """Initial setup for Generator""" + """Initializes all class variables. + + self.data (ndarray): 2D NumPy array where the first column is x-values + (linearly spaced between -10 and 10) and the second column + is the sine of these x-values. + self.frame_num (int): index of the current frame, initialized to 0. + """ logger.info("Beginning setup for Generator") xs = np.linspace(-10, 10, 100) ys = np.sin(xs) self.data = np.column_stack((xs, ys)) + self.frame_num = 0 # Initialize frame counter logger.info("Completed setup for Generator") + def stop(self): """Save current wave vector to file.""" logger.info("Generator stopping") @@ -39,9 +45,10 @@ def stop(self): def runStep(self): """Generates additional data after initial setup data is exhausted. - Data is a sine wave if the frame number is even or a cosine wave if the frame number is odd.""" + Data is a sine wave if the frame number is odd or a cosine wave if the frame number is even.""" time.sleep(0.5) # Add a slight pause between frame generation - """Sends a flattened array with x and y coordinates followed by frame number.""" + + #Sends a flattened array with x and y coordinates followed by frame number. if self.frame_num >= self.max_frames: logger.info(f"Reached maximum frame count ({self.max_frames}). Stopping generation.") return @@ -49,29 +56,20 @@ def runStep(self): xs = np.linspace(-10, 10, 100) # Generate sine or cosine values based on frame number - if self.frame_num % 2 == 0: + if self.frame_num % 2 == 1: # Even frame: Generate sine wave ys = np.sin(xs) - # wave_type = "sine" else: # Odd frame: Generate cosine wave ys = np.cos(xs) - # wave_type = "cosine" - - # Combine x and y into a 2D array - data = np.column_stack((xs, ys)) # Shape (100, 2) - - # Flatten the 2D array to 1D - flattened_values = data.flatten() # Shape (200,) - # Append frame_num as the last element - data_to_send = np.append(flattened_values, self.frame_num) # Shape (201,) + # Combine x and y into a 1D array, append frame_num + data_to_send = np.append(np.ravel(np.column_stack((xs, ys))), self.frame_num) # Shape (201,) # Send the flattened array with frame_num try: data_id = self.client.put(data_to_send, f"Frame: {self.frame_num}") self.q_out.put([[data_id, f"Frame: {self.frame_num}"]]) - # logger.info(f"Sent frame {self.frame_num} with flattened x and y coordinates ({wave_type} wave)") except Exception as e: logger.error(f"Generator Exception: {e}") diff --git a/demos/minimal/actors/sample_processor.py b/demos/minimal/actors/sample_processor.py index 595d5c23..30714b55 100644 --- a/demos/minimal/actors/sample_processor.py +++ b/demos/minimal/actors/sample_processor.py @@ -31,6 +31,7 @@ def setup(self): logger.info("Completed setup for Processor") def stop(self): + """Trivial stop function for testing purposes.""" logger.info("Processor stopping") self.socket.close() return 0 @@ -57,21 +58,19 @@ def runStep(self): # Unpack the flattened data data = np.array(self.frame, dtype=np.float64) # Ensure it's a NumPy array - values = data[:-1] # Exclude the last element frame_num = int(data[-1]) # Extract the last element as frame number - - # Reshape values to 2D array (N, 2) for processing - values = values.reshape(-1, 2) + data = data[:-1].reshape(-1, 2) # Exclude the last element # Perform processing (e.g., scaling the y-values) - values[:, 1] *= 2 # Example: Scale y-coordinates by 2 + data[:, 1] *= 2 # Example: Scale y-coordinates by 2 # Flatten processed values and append frame number - self.processed_data = np.append(values.flatten(), frame_num) + self.processed_data = np.append(np.ravel(data), frame_num) + # Send the processed data through the ZMQ socket self.socket.send(self.processed_data.tobytes()) - # logger.info(f"Frame {frame_num}: Sent {values.shape[0]} points after processing") + logger.info(f"Frame {frame_num}: Sent {data.shape[0]} points after processing") except Exception as e: logger.error(f"Error processing frame: {e}") From 4ef50c17ef21238c3dbcff69bbd205b795fa0807 Mon Sep 17 00:00:00 2001 From: Edwin Ma Date: Thu, 30 Jan 2025 11:05:56 -0500 Subject: [PATCH 17/27] update sine/cosine generator + processor --- demos/minimal/actors/sample_generator.py | 5 +++-- demos/minimal/actors/sample_processor.py | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/demos/minimal/actors/sample_generator.py b/demos/minimal/actors/sample_generator.py index 57addf44..cc09a870 100644 --- a/demos/minimal/actors/sample_generator.py +++ b/demos/minimal/actors/sample_generator.py @@ -64,14 +64,15 @@ def runStep(self): ys = np.cos(xs) # Combine x and y into a 1D array, append frame_num - data_to_send = np.append(np.ravel(np.column_stack((xs, ys))), self.frame_num) # Shape (201,) + data = np.append(np.column_stack((xs, ys)).ravel(), self.frame_num) # Shape (201,) # Send the flattened array with frame_num try: - data_id = self.client.put(data_to_send, f"Frame: {self.frame_num}") + data_id = self.client.put(data, f"Frame: {self.frame_num}") self.q_out.put([[data_id, f"Frame: {self.frame_num}"]]) except Exception as e: logger.error(f"Generator Exception: {e}") # Increment frame number self.frame_num += 1 + self.data = np.column_stack((xs, ys)) diff --git a/demos/minimal/actors/sample_processor.py b/demos/minimal/actors/sample_processor.py index 30714b55..0b55bfdd 100644 --- a/demos/minimal/actors/sample_processor.py +++ b/demos/minimal/actors/sample_processor.py @@ -38,9 +38,10 @@ def stop(self): def runStep(self): """ - Receives data ID from the queue, retrieves data from the Plasma store, - processes it, stores it in `processed_data`, and sends it through the - socket as flattened data with the frame number appended. + Gets from the input queue and scales the data in the y-dimension. + + Receives an ObjectID, references data in the store using that ObjectID, + processes it, and sends it through the zmq socket to be visualized. """ try: # Retrieve data ID from the queue @@ -65,7 +66,7 @@ def runStep(self): data[:, 1] *= 2 # Example: Scale y-coordinates by 2 # Flatten processed values and append frame number - self.processed_data = np.append(np.ravel(data), frame_num) + self.processed_data = np.append(data.ravel(), frame_num) # Send the processed data through the ZMQ socket From 62ff41e141b976040c4b2e66a64acf259e3a9d52 Mon Sep 17 00:00:00 2001 From: Edwin Ma Date: Fri, 7 Feb 2025 15:49:40 -0500 Subject: [PATCH 18/27] updated directories --- demos/minimal/actors/sample_generator.py | 11 +- demos/minimal/actors/sample_processor.py | 5 +- demos/minimal/minimal.yaml | 4 +- demos/sample_actors/visual/fastplotlib.ipynb | 234 ++++++++++++++++++ .../sample_actors/visual/lorenz_generator.py | 113 +++++++++ .../sample_actors/visual/lorenz_processor.py | 83 +++++++ .../sample_actors/visual/sample_processor.py | 76 ++++++ 7 files changed, 516 insertions(+), 10 deletions(-) create mode 100644 demos/sample_actors/visual/fastplotlib.ipynb create mode 100644 demos/sample_actors/visual/lorenz_generator.py create mode 100644 demos/sample_actors/visual/lorenz_processor.py create mode 100644 demos/sample_actors/visual/sample_processor.py diff --git a/demos/minimal/actors/sample_generator.py b/demos/minimal/actors/sample_generator.py index cc09a870..29caac5a 100644 --- a/demos/minimal/actors/sample_generator.py +++ b/demos/minimal/actors/sample_generator.py @@ -1,4 +1,5 @@ from improv.actor import Actor, RunManager +# from demos.sample_actors.visual.sample_processor import Processor import numpy as np import logging import time # Importing time module for the delay @@ -45,10 +46,11 @@ def stop(self): def runStep(self): """Generates additional data after initial setup data is exhausted. - Data is a sine wave if the frame number is odd or a cosine wave if the frame number is even.""" + Data is a sine wave if the frame number is odd or a cosine wave if the frame number is even. + """ time.sleep(0.5) # Add a slight pause between frame generation - #Sends a flattened array with x and y coordinates followed by frame number. + #Sends a flattened array with x and y coordinates along with the current frame number. if self.frame_num >= self.max_frames: logger.info(f"Reached maximum frame count ({self.max_frames}). Stopping generation.") return @@ -64,7 +66,8 @@ def runStep(self): ys = np.cos(xs) # Combine x and y into a 1D array, append frame_num - data = np.append(np.column_stack((xs, ys)).ravel(), self.frame_num) # Shape (201,) + self.data = np.column_stack((xs, ys)) + data = np.append(self.data.ravel(), self.frame_num) # Shape (201,) # Send the flattened array with frame_num try: @@ -75,4 +78,4 @@ def runStep(self): # Increment frame number self.frame_num += 1 - self.data = np.column_stack((xs, ys)) + diff --git a/demos/minimal/actors/sample_processor.py b/demos/minimal/actors/sample_processor.py index 0b55bfdd..74b0a063 100644 --- a/demos/minimal/actors/sample_processor.py +++ b/demos/minimal/actors/sample_processor.py @@ -68,10 +68,7 @@ def runStep(self): # Flatten processed values and append frame number self.processed_data = np.append(data.ravel(), frame_num) - - # Send the processed data through the ZMQ socket + logger.info(f"Frame {frame_num}: Processed {data.shape[0]} points") self.socket.send(self.processed_data.tobytes()) - logger.info(f"Frame {frame_num}: Sent {data.shape[0]} points after processing") - except Exception as e: logger.error(f"Error processing frame: {e}") diff --git a/demos/minimal/minimal.yaml b/demos/minimal/minimal.yaml index 230282d1..1bd76d33 100644 --- a/demos/minimal/minimal.yaml +++ b/demos/minimal/minimal.yaml @@ -4,8 +4,8 @@ actors: class: Generator Processor: - package: actors.sample_processor + package: sample_actors.visual.sample_processor class: Processor connections: - Generator.q_out: [Processor.q_in] \ No newline at end of file + Generator.q_out: [Processor.q_in] diff --git a/demos/sample_actors/visual/fastplotlib.ipynb b/demos/sample_actors/visual/fastplotlib.ipynb new file mode 100644 index 00000000..67d5b772 --- /dev/null +++ b/demos/sample_actors/visual/fastplotlib.ipynb @@ -0,0 +1,234 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "83566503-c31b-4d83-b6f0-173bc9b2102d", + "metadata": {}, + "source": [ + "## `fastplotlib` demo" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "093c8fd4-bbf8-4374-a661-494a61a73cca", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fd0f2ced59444d188a10ace6904915ba", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "No windowing system present. Using surfaceless platform\n", + "No config found!\n", + "No config found!\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Available devices:\n", + "✅ (default) | NVIDIA GeForce GTX 1080 Ti | DiscreteGPU | Vulkan | 555.42.06\n", + "❗ | llvmpipe (LLVM 15.0.7, 256 bits) | CPU | Vulkan | Mesa 23.2.1-1ubuntu3.1~22.04.2 (LLVM 15.0.7)\n", + "❗ | NVIDIA GeForce GTX 1080 Ti/PCIe/SSE2 | Unknown | OpenGL | \n" + ] + } + ], + "source": [ + "import zmq\n", + "import numpy as np\n", + "import fastplotlib as fpl" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "24d73c48-c24d-440b-8dfc-9c4a2f476233", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Listening...\n" + ] + } + ], + "source": [ + "# ZMQ context and socket setup\n", + "context = zmq.Context()\n", + "socket = context.socket(zmq.SUB)\n", + "socket.connect(\"tcp://127.0.0.1:5555\")\n", + "socket.setsockopt_string(zmq.SUBSCRIBE, \"\")\n", + "print(\"Listening...\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "fc5d4c34-3f4e-4526-b245-1db3fcd84bf5", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b4b88abf2ac244b78cfb4d970bb2a71e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/edwin/edwin_improv/fpl/lib/python3.10/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32\n", + " warn(f\"casting {array.dtype} array to float32\")\n" + ] + } + ], + "source": [ + "# Create the figure\n", + "figure = fpl.Figure()\n", + "\n", + "# Add a line plot placeholder\n", + "xs = np.linspace(-10, 10, 100)\n", + "cos_ys = np.cos(xs)\n", + "figure[0, 0].add_line(data=np.column_stack((xs, cos_ys)), name=\"wave\")\n", + "\n", + "# Set the dimension for reshaping\n", + "dimension = 2 # Default to 2 for [x, y] data; adjust as needed" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8f637c0c-3854-49ce-9d19-c692d2af61e8", + "metadata": {}, + "outputs": [], + "source": [ + "def get_buffer():\n", + " \"\"\"\n", + " Retrieve the buffer from the socket.\n", + " \"\"\"\n", + " try:\n", + " b = socket.recv(zmq.NOBLOCK) # Non-blocking receive\n", + " return b\n", + " except zmq.Again:\n", + " return None" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d5240ab4-3bb4-4840-8a30-b0a60abe4aa1", + "metadata": {}, + "outputs": [], + "source": [ + "def update_frame(p):\n", + " \"\"\"\n", + " Update the frame using data received from the socket and reshape it based on the specified dimension.\n", + " \"\"\"\n", + " buff = get_buffer()\n", + " if buff is not None:\n", + " # Deserialize the buffer into a NumPy array\n", + " data = np.frombuffer(buff, dtype=np.float64)\n", + "\n", + " # Extract the frame number from the last index\n", + " frame_num = int(data[-1]) # Last element is the frame number\n", + "\n", + " # Reshape the remaining data based on the specified dimension\n", + " if dimension > 0:\n", + " values = data[:-1].reshape(-1, dimension) # Reshape all but the last element\n", + " else:\n", + " values = data[:-1] # No reshaping if dimension <= 0\n", + "\n", + " # Update the line plot\n", + " if dimension >= 2: # Ensure at least [x, y] data is available for plotting\n", + " p[\"wave\"].data[:, :dimension] = values\n", + " else:\n", + " print(f\"Received frame {frame_num}, but dimension {dimension} is insufficient for plotting.\")\n", + "\n", + " # Update the plot title with the frame number\n", + " p.name = f\"frame: {frame_num}\"" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a337ec69-1288-4c5c-a55e-c3f6a1b7bcba", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "42e21b2f097740199785b92b27d596a2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Add the animation update function\n", + "figure[0, 0].add_animations(update_frame)\n", + "\n", + "figure.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b134c339-4570-413f-ad2d-1fd709434a3a", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demos/sample_actors/visual/lorenz_generator.py b/demos/sample_actors/visual/lorenz_generator.py new file mode 100644 index 00000000..5cde9fc1 --- /dev/null +++ b/demos/sample_actors/visual/lorenz_generator.py @@ -0,0 +1,113 @@ +from improv.actor import Actor, RunManager +from datetime import date # used for saving +import numpy as np +import logging +import time # Importing time module for the delay + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +class Generator(Actor): + """ + Generates coordinates for the Lorenz system in real time. + Computes the next Lorenz coordinates every half second. + Outputs a flattened array with progressively filled 100 (x, y, z) triplets + and appends the frame number at the end. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.data = None # Placeholder for the 2D array + self.coordinates = [] + self.name = "lorenz_generator" + self.dt = 0.01 # Time step for numerical integration + self.max_points = 6000 # Total number of (x, y, z) triplets + self.points_per_frame = 50 # Number of points to add per frame + + def __str__(self): + return f"Name: {self.name}, Current Coordinates: {self.coordinates[-1] if self.coordinates else None}" + + def setup(self): + """Initializes all class variables. + + self.coordinates (list): List containing the initial 3D coordinate [x, y, z]. + self.data (ndarray): 2D NumPy array initialized with zeros to store x, y, z coordinates + for a maximum of `self.max_points` rows. + self.frame_num (int): Index of the current frame, initialized to 0. + self.current_index (int): Tracks the current position in the `self.data` array for filling new values. + """ + logger.info("Beginning setup for LorenzGenerator") + initial_coordinate = np.array([1.0, 1.0, 1.0]) # Initial coordinates (x, y, z) + self.coordinates = [initial_coordinate] + + # Initialize the data array as a 2D array (100 rows for x, y, z coordinates) + self.data = np.zeros((self.max_points, 3)) # Shape (1000, 3) + self.frame_num = 0 + self.current_index = 0 # Tracks the current position to fill in the data array + logger.info(f"Initialized Lorenz system with initial coordinates: {initial_coordinate}") + + def stop(self): + """ + Save the last Lorenz coordinates to a file for persistence. + """ + logger.info("LorenzGenerator stopping") + np.save("lorenz_last_coordinate.npy", self.coordinates[-1]) + return 0 + + def runStep(self): + """ + Generates the next 10 Lorenz coordinates and fills them into the 2D data array. + Sends the progressively filled data as a flattened array with the frame number appended. + """ + time.sleep(0.5) # Delay for half a second + + try: + # Add the next 10 points + for _ in range(self.points_per_frame): + if self.current_index >= self.max_points: + logger.info(f"Data array fully filled for frame {self.frame_num}.") + break # Stop filling if all 1000 points are generated + + # Compute the next coordinate + derivative = lorenz(self.coordinates[-1]) + next_coordinate = self.coordinates[-1] + derivative * self.dt + self.coordinates.append(next_coordinate) + + # Fill x, y, and z coordinates into the 2D data array + self.data[self.current_index, 0] = next_coordinate[0] # x-coordinate + self.data[self.current_index, 1] = next_coordinate[1] # y-coordinate + self.data[self.current_index, 2] = next_coordinate[2] # z-coordinate + + self.current_index += 1 # Increment the index for the next point + + # Flatten the 2D array and append the frame number in one step + data_to_send = np.append(np.ravel(self.data), self.frame_num) + + # Send the data + data_id = self.client.put(data_to_send, str(f"Lorenz_Frame: {self.frame_num}")) + self.q_out.put([[data_id, str(self.frame_num)]]) + + # Increment frame number for the next step + self.frame_num += 1 + except Exception as e: + logger.error(f"LorenzGenerator Exception: {e}") + +def lorenz(xyz, s=10, r=28, b=2.667): + """ + Parameters + ---------- + xyz : array-like, shape (3,) + Point of interest in three-dimensional space. + s, r, b : float + Parameters defining the Lorenz attractor. + + Returns + ------- + xyz_dot : array, shape (3,) + Values of the Lorenz attractor's partial derivatives at *xyz*. + """ + x, y, z = xyz + x_dot = s * (y - x) + y_dot = r * x - y - x * z + z_dot = x * y - b * z + return np.array([x_dot, y_dot, z_dot]) diff --git a/demos/sample_actors/visual/lorenz_processor.py b/demos/sample_actors/visual/lorenz_processor.py new file mode 100644 index 00000000..14e78949 --- /dev/null +++ b/demos/sample_actors/visual/lorenz_processor.py @@ -0,0 +1,83 @@ +from improv.actor import Actor +from queue import Empty +import logging +import zmq +import numpy as np + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class Processor(Actor): + """ + Processes Lorenz data by performing custom transformations on the coordinates + (e.g., scaling and applying mathematical operations) and sends the processed + data through a ZMQ socket for visualization. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def setup(self): + """ + Sets up the ZMQ socket and initializes storage for processed data. + """ + self.name = "Processor" + self.processed_data = None # Storage for processed data + self.frame = None # Storage for the current frame + + # Set up ZMQ PUB socket + context = zmq.Context() + self.socket = context.socket(zmq.PUB) + self.socket.bind("tcp://127.0.0.1:5555") + + logger.info("Processor setup completed. ZMQ PUB socket bound to tcp://127.0.0.1:5555") + + def stop(self): + """ + Trivial stop function for testing purposes. + """ + logger.info("Processor stopping") + self.socket.close() + return 0 + + def runStep(self): + """ + Processes incoming Lorenz data, applies transformations, and sends + the processed data through a ZMQ socket. + """ + try: + # Retrieve data ID from the input queue + data_id = self.q_in.get(timeout=0.05) + except Empty: + return # No data received, skip this step + except Exception as e: + logger.error(f"Error retrieving data ID: {e}") + return + + if data_id is not None: + try: + # Fetch the data from the client using the ObjectID + self.frame = self.client.getID(data_id[0][0]) # Retrieve the frame data + + # Convert the frame data to a NumPy array + data = np.array(self.frame, dtype=np.float64) # Ensure it's a NumPy array + frame_num = int(data[-1]) # Extract the last element as the frame number + data = data[:-1].reshape(-1, 3) # Exclude the last element (frame number) + + + # Perform processing on the Lorenz coordinates + # Example 1: Scale x-coordinates by 0.5 and y-coordinates by 2 + data[:, 0] *= 2 # Scale x-coordinates + data[:, 1] *= 2 # Scale y-coordinates + data[:, 2] *= 2 # Scale z-coordinates + + # Flatten processed values and append frame number + self.processed_data = np.append(np.ravel(data), frame_num) + + # Send the processed data through the ZMQ socket + self.socket.send(self.processed_data.tobytes()) + logger.info(f"Frame {frame_num}: Sent points with size {data.shape} after processing") + + except Exception as e: + logger.error(f"Error processing frame: {e}") diff --git a/demos/sample_actors/visual/sample_processor.py b/demos/sample_actors/visual/sample_processor.py new file mode 100644 index 00000000..561b831d --- /dev/null +++ b/demos/sample_actors/visual/sample_processor.py @@ -0,0 +1,76 @@ +from improv.actor import Actor +from queue import Empty +import logging +import zmq +import numpy as np + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class Processor(Actor): + """ + Process data by scaling y coordinates by 2 and send it through zmq to be visualized. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def setup(self): + """ + Creates and binds the socket for zmq and initializes processed data storage. + """ + self.name = "Processor" + self.processed_data = None # Initialize variable to store processed data + self.frame = None # Initialize variable to store the current frame + + context = zmq.Context() + self.socket = context.socket(zmq.PUB) + self.socket.bind("tcp://127.0.0.1:5555") + + logger.info("Completed setup for Processor") + + def stop(self): + """Trivial stop function for testing purposes.""" + logger.info("Processor stopping") + self.socket.close() + return 0 + + def runStep(self): + """ + Gets from the input queue and scales the data in the y-dimension. + + Receives an ObjectID, references data in the store using that ObjectID, + processes it, and sends it through the zmq socket to be visualized. + """ + try: + # Retrieve data ID from the queue + data_id = self.q_in.get(timeout=0.05) + except Empty: + return # No data received, skip this step + except Exception as e: + logger.error(f"Error retrieving data ID: {e}") + return + + if data_id is not None: + try: + # Fetch the data from the client using the ObjectID + self.frame = self.client.getID(data_id[0][0]) # Retrieve the frame data + + # Unpack the flattened data + data = np.array(self.frame, dtype=np.float64) # Ensure it's a NumPy array + frame_num = int(data[-1]) # Extract the last element as frame number + data = data[:-1].reshape(-1, 2) # Exclude the last element + + # Perform processing (e.g., scaling the y-values) + data[:, 1] *= 2 # Example: Scale y-coordinates by 2 + + # Flatten processed values and append frame number + self.processed_data = np.append(data.ravel(), frame_num) + + # Send the processed data through the ZMQ socket + self.socket.send(self.processed_data.tobytes()) + logger.info(f"Frame {frame_num}: Sent {data.shape[0]} points after processing") + + except Exception as e: + logger.error(f"Error processing frame: {e}") From 243ef6b7370a07975c51a1d6b5235ea6cb691963 Mon Sep 17 00:00:00 2001 From: clewis7 Date: Mon, 10 Feb 2025 14:51:16 -0500 Subject: [PATCH 19/27] reorganization --- .../actors}/lorenz_generator.py | 0 .../actors/lorenz_processor.py | 0 .../actors/visual_processor.py} | 2 +- .../actors => fastplotlib}/fastplotlib.ipynb | 0 .../actors => fastplotlib}/lorenz_fpl.ipynb | 71 +++++- demos/minimal/actors/lorenz_generator.py | 113 --------- demos/minimal/minimal.yaml | 2 +- demos/sample_actors/visual/fastplotlib.ipynb | 234 ------------------ .../sample_actors/visual/lorenz_processor.py | 83 ------- 9 files changed, 61 insertions(+), 444 deletions(-) rename demos/{sample_actors/visual => fastplotlib/actors}/lorenz_generator.py (100%) rename demos/{minimal => fastplotlib}/actors/lorenz_processor.py (100%) rename demos/{sample_actors/visual/sample_processor.py => fastplotlib/actors/visual_processor.py} (97%) rename demos/{minimal/actors => fastplotlib}/fastplotlib.ipynb (100%) rename demos/{minimal/actors => fastplotlib}/lorenz_fpl.ipynb (77%) delete mode 100644 demos/minimal/actors/lorenz_generator.py delete mode 100644 demos/sample_actors/visual/fastplotlib.ipynb delete mode 100644 demos/sample_actors/visual/lorenz_processor.py diff --git a/demos/sample_actors/visual/lorenz_generator.py b/demos/fastplotlib/actors/lorenz_generator.py similarity index 100% rename from demos/sample_actors/visual/lorenz_generator.py rename to demos/fastplotlib/actors/lorenz_generator.py diff --git a/demos/minimal/actors/lorenz_processor.py b/demos/fastplotlib/actors/lorenz_processor.py similarity index 100% rename from demos/minimal/actors/lorenz_processor.py rename to demos/fastplotlib/actors/lorenz_processor.py diff --git a/demos/sample_actors/visual/sample_processor.py b/demos/fastplotlib/actors/visual_processor.py similarity index 97% rename from demos/sample_actors/visual/sample_processor.py rename to demos/fastplotlib/actors/visual_processor.py index 561b831d..4bc289fd 100644 --- a/demos/sample_actors/visual/sample_processor.py +++ b/demos/fastplotlib/actors/visual_processor.py @@ -28,7 +28,7 @@ def setup(self): self.socket = context.socket(zmq.PUB) self.socket.bind("tcp://127.0.0.1:5555") - logger.info("Completed setup for Processor") + logger.info("Completed setup for Processor **visual**") def stop(self): """Trivial stop function for testing purposes.""" diff --git a/demos/minimal/actors/fastplotlib.ipynb b/demos/fastplotlib/fastplotlib.ipynb similarity index 100% rename from demos/minimal/actors/fastplotlib.ipynb rename to demos/fastplotlib/fastplotlib.ipynb diff --git a/demos/minimal/actors/lorenz_fpl.ipynb b/demos/fastplotlib/lorenz_fpl.ipynb similarity index 77% rename from demos/minimal/actors/lorenz_fpl.ipynb rename to demos/fastplotlib/lorenz_fpl.ipynb index 86443135..ac4b5831 100644 --- a/demos/minimal/actors/lorenz_fpl.ipynb +++ b/demos/fastplotlib/lorenz_fpl.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "068919b3-0a3a-47ff-836b-221079d6e095", "metadata": {}, "outputs": [], @@ -14,10 +14,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "c665d7f4-d7eb-4ef7-a9c8-d737b9f0b073", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Listening...\n" + ] + } + ], "source": [ "# ZMQ context and socket setup\n", "context = zmq.Context()\n", @@ -29,10 +37,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "147e4be8-fb62-4463-bf6f-0a8e32593c4e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8d34249a9edf438e8d1d6bde727c0252", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Create the figure\n", "figure = fpl.Figure(\n", @@ -49,7 +72,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "28f334db-aadb-40fc-b067-c5b4c4a146d1", "metadata": {}, "outputs": [], @@ -67,7 +90,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "9c3ac076-d4a8-4877-8843-c5e6cc54a20c", "metadata": {}, "outputs": [], @@ -119,23 +142,47 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "439abe77-263c-4ab4-b810-5c7d5e6029e1", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9e90468fbbb7456f9b9b12f1a60cdafc", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Add the animation update function\n", "figure[0, 0].add_animations(update_frame)\n", "\n", "figure.show()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94f0128b-1188-44f0-8b93-2ae1d54045e8", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python (fpl)", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "fpl" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -147,7 +194,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.16" } }, "nbformat": 4, diff --git a/demos/minimal/actors/lorenz_generator.py b/demos/minimal/actors/lorenz_generator.py deleted file mode 100644 index 9ea79f4d..00000000 --- a/demos/minimal/actors/lorenz_generator.py +++ /dev/null @@ -1,113 +0,0 @@ -from improv.actor import Actor, RunManager -from datetime import date # used for saving -import numpy as np -import logging -import time # Importing time module for the delay - -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - -class Generator(Actor): - """ - Generates coordinates for the Lorenz system in real time. - Computes the next Lorenz coordinates every half second. - Outputs a flattened array with progressively filled 100 (x, y, z) triplets - and appends the frame number at the end. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.data = None # Placeholder for the 2D array - self.coordinates = [] - self.name = "lorenz_generator" - self.dt = 0.01 # Time step for numerical integration - self.max_points = 1000 # Total number of (x, y, z) triplets - self.points_per_frame = 10 # Number of points to add per frame - - def __str__(self): - return f"Name: {self.name}, Current Coordinates: {self.coordinates[-1] if self.coordinates else None}" - - def setup(self): - """Initializes all class variables. - - self.coordinates (list): List containing the initial 3D coordinate [x, y, z]. - self.data (ndarray): 2D NumPy array initialized with zeros to store x, y, z coordinates - for a maximum of `self.max_points` rows. - self.frame_num (int): Index of the current frame, initialized to 0. - self.current_index (int): Tracks the current position in the `self.data` array for filling new values. - """ - logger.info("Beginning setup for LorenzGenerator") - initial_coordinate = np.array([1.0, 1.0, 1.0]) # Initial coordinates (x, y, z) - self.coordinates = [initial_coordinate] - - # Initialize the data array as a 2D array (100 rows for x, y, z coordinates) - self.data = np.zeros((self.max_points, 3)) # Shape (1000, 3) - self.frame_num = 0 - self.current_index = 0 # Tracks the current position to fill in the data array - logger.info(f"Initialized Lorenz system with initial coordinates: {initial_coordinate}") - - def stop(self): - """ - Save the last Lorenz coordinates to a file for persistence. - """ - logger.info("LorenzGenerator stopping") - np.save("lorenz_last_coordinate.npy", self.coordinates[-1]) - return 0 - - def runStep(self): - """ - Generates the next 10 Lorenz coordinates and fills them into the 2D data array. - Sends the progressively filled data as a flattened array with the frame number appended. - """ - time.sleep(0.5) # Delay for half a second - - try: - # Add the next 10 points - for _ in range(self.points_per_frame): - if self.current_index >= self.max_points: - logger.info(f"Data array fully filled for frame {self.frame_num}.") - break # Stop filling if all 1000 points are generated - - # Compute the next coordinate - derivative = lorenz(self.coordinates[-1]) - next_coordinate = self.coordinates[-1] + derivative * self.dt - self.coordinates.append(next_coordinate) - - # Fill x, y, and z coordinates into the 2D data array - self.data[self.current_index, 0] = next_coordinate[0] # x-coordinate - self.data[self.current_index, 1] = next_coordinate[1] # y-coordinate - self.data[self.current_index, 2] = next_coordinate[2] # z-coordinate - - self.current_index += 1 # Increment the index for the next point - - # Flatten the 2D array and append the frame number in one step - data_to_send = np.append(np.ravel(self.data), self.frame_num) - - # Send the data - data_id = self.client.put(data_to_send, str(f"Lorenz_Frame: {self.frame_num}")) - self.q_out.put([[data_id, str(self.frame_num)]]) - - # Increment frame number for the next step - self.frame_num += 1 - except Exception as e: - logger.error(f"LorenzGenerator Exception: {e}") - -def lorenz(xyz, s=10, r=28, b=2.667): - """ - Parameters - ---------- - xyz : array-like, shape (3,) - Point of interest in three-dimensional space. - s, r, b : float - Parameters defining the Lorenz attractor. - - Returns - ------- - xyz_dot : array, shape (3,) - Values of the Lorenz attractor's partial derivatives at *xyz*. - """ - x, y, z = xyz - x_dot = s * (y - x) - y_dot = r * x - y - x * z - z_dot = x * y - b * z - return np.array([x_dot, y_dot, z_dot]) diff --git a/demos/minimal/minimal.yaml b/demos/minimal/minimal.yaml index 1bd76d33..b14b5ed6 100644 --- a/demos/minimal/minimal.yaml +++ b/demos/minimal/minimal.yaml @@ -4,7 +4,7 @@ actors: class: Generator Processor: - package: sample_actors.visual.sample_processor + package: actors.visual_processor class: Processor connections: diff --git a/demos/sample_actors/visual/fastplotlib.ipynb b/demos/sample_actors/visual/fastplotlib.ipynb deleted file mode 100644 index 67d5b772..00000000 --- a/demos/sample_actors/visual/fastplotlib.ipynb +++ /dev/null @@ -1,234 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "83566503-c31b-4d83-b6f0-173bc9b2102d", - "metadata": {}, - "source": [ - "## `fastplotlib` demo" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "093c8fd4-bbf8-4374-a661-494a61a73cca", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "fd0f2ced59444d188a10ace6904915ba", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "No windowing system present. Using surfaceless platform\n", - "No config found!\n", - "No config found!\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Available devices:\n", - "✅ (default) | NVIDIA GeForce GTX 1080 Ti | DiscreteGPU | Vulkan | 555.42.06\n", - "❗ | llvmpipe (LLVM 15.0.7, 256 bits) | CPU | Vulkan | Mesa 23.2.1-1ubuntu3.1~22.04.2 (LLVM 15.0.7)\n", - "❗ | NVIDIA GeForce GTX 1080 Ti/PCIe/SSE2 | Unknown | OpenGL | \n" - ] - } - ], - "source": [ - "import zmq\n", - "import numpy as np\n", - "import fastplotlib as fpl" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "24d73c48-c24d-440b-8dfc-9c4a2f476233", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Listening...\n" - ] - } - ], - "source": [ - "# ZMQ context and socket setup\n", - "context = zmq.Context()\n", - "socket = context.socket(zmq.SUB)\n", - "socket.connect(\"tcp://127.0.0.1:5555\")\n", - "socket.setsockopt_string(zmq.SUBSCRIBE, \"\")\n", - "print(\"Listening...\")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "fc5d4c34-3f4e-4526-b245-1db3fcd84bf5", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b4b88abf2ac244b78cfb4d970bb2a71e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/edwin/edwin_improv/fpl/lib/python3.10/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32\n", - " warn(f\"casting {array.dtype} array to float32\")\n" - ] - } - ], - "source": [ - "# Create the figure\n", - "figure = fpl.Figure()\n", - "\n", - "# Add a line plot placeholder\n", - "xs = np.linspace(-10, 10, 100)\n", - "cos_ys = np.cos(xs)\n", - "figure[0, 0].add_line(data=np.column_stack((xs, cos_ys)), name=\"wave\")\n", - "\n", - "# Set the dimension for reshaping\n", - "dimension = 2 # Default to 2 for [x, y] data; adjust as needed" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "8f637c0c-3854-49ce-9d19-c692d2af61e8", - "metadata": {}, - "outputs": [], - "source": [ - "def get_buffer():\n", - " \"\"\"\n", - " Retrieve the buffer from the socket.\n", - " \"\"\"\n", - " try:\n", - " b = socket.recv(zmq.NOBLOCK) # Non-blocking receive\n", - " return b\n", - " except zmq.Again:\n", - " return None" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "d5240ab4-3bb4-4840-8a30-b0a60abe4aa1", - "metadata": {}, - "outputs": [], - "source": [ - "def update_frame(p):\n", - " \"\"\"\n", - " Update the frame using data received from the socket and reshape it based on the specified dimension.\n", - " \"\"\"\n", - " buff = get_buffer()\n", - " if buff is not None:\n", - " # Deserialize the buffer into a NumPy array\n", - " data = np.frombuffer(buff, dtype=np.float64)\n", - "\n", - " # Extract the frame number from the last index\n", - " frame_num = int(data[-1]) # Last element is the frame number\n", - "\n", - " # Reshape the remaining data based on the specified dimension\n", - " if dimension > 0:\n", - " values = data[:-1].reshape(-1, dimension) # Reshape all but the last element\n", - " else:\n", - " values = data[:-1] # No reshaping if dimension <= 0\n", - "\n", - " # Update the line plot\n", - " if dimension >= 2: # Ensure at least [x, y] data is available for plotting\n", - " p[\"wave\"].data[:, :dimension] = values\n", - " else:\n", - " print(f\"Received frame {frame_num}, but dimension {dimension} is insufficient for plotting.\")\n", - "\n", - " # Update the plot title with the frame number\n", - " p.name = f\"frame: {frame_num}\"" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "a337ec69-1288-4c5c-a55e-c3f6a1b7bcba", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "42e21b2f097740199785b92b27d596a2", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Add the animation update function\n", - "figure[0, 0].add_animations(update_frame)\n", - "\n", - "figure.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b134c339-4570-413f-ad2d-1fd709434a3a", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/demos/sample_actors/visual/lorenz_processor.py b/demos/sample_actors/visual/lorenz_processor.py deleted file mode 100644 index 14e78949..00000000 --- a/demos/sample_actors/visual/lorenz_processor.py +++ /dev/null @@ -1,83 +0,0 @@ -from improv.actor import Actor -from queue import Empty -import logging -import zmq -import numpy as np - -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - - -class Processor(Actor): - """ - Processes Lorenz data by performing custom transformations on the coordinates - (e.g., scaling and applying mathematical operations) and sends the processed - data through a ZMQ socket for visualization. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def setup(self): - """ - Sets up the ZMQ socket and initializes storage for processed data. - """ - self.name = "Processor" - self.processed_data = None # Storage for processed data - self.frame = None # Storage for the current frame - - # Set up ZMQ PUB socket - context = zmq.Context() - self.socket = context.socket(zmq.PUB) - self.socket.bind("tcp://127.0.0.1:5555") - - logger.info("Processor setup completed. ZMQ PUB socket bound to tcp://127.0.0.1:5555") - - def stop(self): - """ - Trivial stop function for testing purposes. - """ - logger.info("Processor stopping") - self.socket.close() - return 0 - - def runStep(self): - """ - Processes incoming Lorenz data, applies transformations, and sends - the processed data through a ZMQ socket. - """ - try: - # Retrieve data ID from the input queue - data_id = self.q_in.get(timeout=0.05) - except Empty: - return # No data received, skip this step - except Exception as e: - logger.error(f"Error retrieving data ID: {e}") - return - - if data_id is not None: - try: - # Fetch the data from the client using the ObjectID - self.frame = self.client.getID(data_id[0][0]) # Retrieve the frame data - - # Convert the frame data to a NumPy array - data = np.array(self.frame, dtype=np.float64) # Ensure it's a NumPy array - frame_num = int(data[-1]) # Extract the last element as the frame number - data = data[:-1].reshape(-1, 3) # Exclude the last element (frame number) - - - # Perform processing on the Lorenz coordinates - # Example 1: Scale x-coordinates by 0.5 and y-coordinates by 2 - data[:, 0] *= 2 # Scale x-coordinates - data[:, 1] *= 2 # Scale y-coordinates - data[:, 2] *= 2 # Scale z-coordinates - - # Flatten processed values and append frame number - self.processed_data = np.append(np.ravel(data), frame_num) - - # Send the processed data through the ZMQ socket - self.socket.send(self.processed_data.tobytes()) - logger.info(f"Frame {frame_num}: Sent points with size {data.shape} after processing") - - except Exception as e: - logger.error(f"Error processing frame: {e}") From 3daec64717ff8ad6e77fe01ffaf46e97a3166732 Mon Sep 17 00:00:00 2001 From: Edwin Ma Date: Thu, 13 Feb 2025 12:54:21 -0500 Subject: [PATCH 20/27] updated readme file --- demos/minimal/README.md | 112 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 demos/minimal/README.md diff --git a/demos/minimal/README.md b/demos/minimal/README.md new file mode 100644 index 00000000..c646049c --- /dev/null +++ b/demos/minimal/README.md @@ -0,0 +1,112 @@ +# minimal demo + +This folder contains a minimal demo for running improv. In this demo, data generated by the **generator** `actor` is +stored in a data store and a key to the generated data is sent via a queue to the **processor** `actor` that accesses +the data in the store and processes it. + +### Make sure the generator and processor in the minimal.yaml file are actors.sample_generator and actors.sample_processor respectively. + +Usage: + +```bash +# cd to this dir +cd .../improv/demos/minimal + +# start improv +improv run ./minimal.yaml + +# call `setup` in the improv TUI +setup + +# call `run` in the improv TUI +run + +# when you are ready to stop the process, call `stop` in the improv TUI +stop +``` + +## Sample Visualization using `fastplotlib` + +You can also run the minimal demo and visualize the generated data using `fastplotlib`. + +As before, data is generated by the **generator** `actor` is stored in a data store and a key to the generated data is +sent via a queue to the **processor** `actor` that accesses the data in the store and processes it. Additionally, the +`fastplotlib.ipynb` notebook then receives the most recent data via `zmq` and displays it using +[`fastplotlib`](https://github.com/fastplotlib/fastplotlib). + +### Instructions + +1. Update the processor in minimal.yaml to actors.visual_processor and keep generator as actors.sample_generator. + +2. Run `pip install -r requirements.txt` in this directory. + +Usage: + +```bash +# cd to this dir +cd .../improv/demos + +# start improv with actor paths to locate the sample_generator and visual_processor actor files +improv run -a fastplotlib/ -a minimal/ minimal/minimal.yaml + +# call `setup` in the improv TUI +setup + +# Run the cells in the jupyter notebook until you receive +# a plot that has a white cosine wave + +# once the plot is ready call `run` in the improv TUI +run + +# You should see the plot updating between a cosine and sine wave depending on +# whether the frame number is even or odd + +# when you are ready to stop the process, call `stop` in the improv TUI +stop +``` + +#### Note: The `fastplotlib.ipynb` can only be run in `jupyter lab` + + +## Sample Lorenz Visualization using `fastplotlib` + +You can also run the minimal demo and visualize the generated data using `fastplotlib`. + +As before, data is generated by the **generator** `actor` is stored in a data store and a key to the generated data is +sent via a queue to the **processor** `actor` that accesses the data in the store and processes it. Additionally, the +`lorenz_fpl.ipynb` notebook then receives the most recent data via `zmq` and displays it using +[`fastplotlib`](https://github.com/fastplotlib/fastplotlib). + +### Instructions + +1. Update the processor in minimal.yaml to actors.lorenz_processor and generator to actors.lorenz_generator. + +2. Run `pip install -r requirements.txt` in this directory if not ran already from previous sample visualization. + +Usage: + +```bash +# cd to this dir +cd .../improv/demos + +# start improv with actor paths to locate the sample_generator and visual_processor actor files +improv run -a fastplotlib/ minimal/minimal.yaml + +# call `setup` in the improv TUI +setup + +# Run the cells in the jupyter notebook until you receive +# a plot that has a black box. + +# once the plot is ready call `run` in the improv TUI +run + +# Note: If the visualization is still a black box, press +# the auto-scale scene button in the bottom left corner. + +# You should see the plot updating with a lorenz attractor +# curve that changes colors according to the z values. + +# when you are ready to stop the process, call `stop` in the improv TUI +stop + From 361cdc995ca39a7d9525210168524d9cb716d33f Mon Sep 17 00:00:00 2001 From: clewis7 Date: Thu, 13 Feb 2025 16:49:48 -0500 Subject: [PATCH 21/27] clean up minimal demo --- demos/minimal/actors/sample_generator.py | 57 ++++++++++--------- demos/minimal/actors/sample_processor.py | 70 ++++++++++++------------ 2 files changed, 63 insertions(+), 64 deletions(-) diff --git a/demos/minimal/actors/sample_generator.py b/demos/minimal/actors/sample_generator.py index 7c54fc09..b087e300 100644 --- a/demos/minimal/actors/sample_generator.py +++ b/demos/minimal/actors/sample_generator.py @@ -8,33 +8,34 @@ class Generator(Actor): - """Sample actor to generate a sine/cosine wave based on frame number to pass into a sample processor. Odd frames generate a sine wave and even frames generate a cosine wave. + """Sample actor to generate a sine/cosine wave based on frame number to pass into a sample processor. + + Intended for use along with sample_processor.py. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.name = "Generator" self.data = None - self.max_frames = 500 # Set the limit for number of frames + self.frame_num = 0 def __str__(self): return f"Name: {self.name}, Data: {self.data}" def setup(self): - """Initializes all class variables. + """Generates an array that serves as an initial source of data. - self.data (ndarray): 2D NumPy array where the first column is x-values - (linearly spaced between -10 and 10) and the second column - is the sine of these x-values. - self.frame_num (int): index of the current frame, initialized to 0. + Initial data is a 2D cosine wave consisting of 100 evenly spaced xy points ranging from -10 to 10 inclusive. """ logger.info("Beginning setup for Generator") + + # generate 100 evenly spaced values from -10 to 10 xs = np.linspace(-10, 10, 100) - ys = np.sin(xs) - self.data = np.column_stack((xs, ys)) - self.frame_num = 0 # Initialize frame counter - logger.info("Completed setup for Generator") + ys = np.cos(xs) + # stack xs and ys to create a (100, 2) array of xy points + self.data = np.column_stack([xs, ys]) + logger.info("Completed setup for Generator") def stop(self): """Save current wave vector to file.""" @@ -44,37 +45,35 @@ def stop(self): def runStep(self): """Generates additional data after initial setup data is exhausted. - - Data is a sine wave if the frame number is odd or a cosine wave if the frame number is even. - """ - time.sleep(0.5) # Add a slight pause between frame generation - - #Sends a flattened array with x and y coordinates along with the current frame number. - if self.frame_num >= self.max_frames: - logger.info(f"Reached maximum frame count ({self.max_frames}). Stopping generation.") - return + If the frame number is odd, the data is a sine wave. If the frame number is even, the data is a cosine wave. + """ xs = np.linspace(-10, 10, 100) # Generate sine or cosine values based on frame number if self.frame_num % 2 == 1: - # Even frame: Generate sine wave ys = np.sin(xs) else: - # Odd frame: Generate cosine wave ys = np.cos(xs) - # Combine x and y into a 1D array, append frame_num - self.data = np.column_stack((xs, ys)) + # update data + self.data = np.column_stack([xs, ys]) + + # create flattened array with x and y coordinates along with the current frame number data = np.append(self.data.ravel(), self.frame_num) # Shape (201,) # Send the flattened array with frame_num try: - data_id = self.client.put(data, f"Frame: {self.frame_num}") - self.q_out.put([[data_id, f"Frame: {self.frame_num}"]]) + data_id = self.client.put(data) + if self.store_loc: + self.q_out.put([[data_id, str(self.frame_num)]]) + else: + self.q_out.put(data_id) + + # Increment frame number + self.frame_num += 1 except Exception as e: - logger.error(f"Generator Exception: {e}") + logger.error(f"--------------------------------Generator Exception: {e}") + - # Increment frame number - self.frame_num += 1 diff --git a/demos/minimal/actors/sample_processor.py b/demos/minimal/actors/sample_processor.py index b3832bbc..b9bf6242 100644 --- a/demos/minimal/actors/sample_processor.py +++ b/demos/minimal/actors/sample_processor.py @@ -3,73 +3,73 @@ import logging import zmq import numpy as np +import random logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) class Processor(Actor): - """ - Process data by scaling y coordinates by 2 and send it through zmq to be visualized. + """Sample processor used to scale a sine or cosine wave and calculate the amplitude. + Intended for use with sample_generator.py. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def setup(self): - """ - Creates and binds the socket for zmq and initializes processed data storage. + """Initializes all class variables. + + self.name (string): name of the actor. + self.frame (ObjectID): StoreInterface object id referencing data from the store. + self.frame_num (int): index of current frame. """ self.name = "Processor" - self.processed_data = None # Initialize variable to store processed data - self.frame = None # Initialize variable to store the current frame - - context = zmq.Context() - self.socket = context.socket(zmq.PUB) - self.socket.bind("tcp://127.0.0.1:5555") + self.frame = None + self.frame_num = 0 logger.info("Completed setup for Processor") def stop(self): """Trivial stop function for testing purposes.""" logger.info("Processor stopping") - self.socket.close() return 0 def runStep(self): """ - Gets from the input queue and scales the data in the y-dimension. + Gets from the input queue, scales the data in the y-dimension by a random number between 1-10 inclusive and then + calculates the amplitude. - Receives an ObjectID, references data in the store using that ObjectID, - processes it, and sends it through the zmq socket to be visualized. """ + data_id = None try: - # Retrieve data ID from the queue data_id = self.q_in.get(timeout=0.05) - except Empty: - return # No data received, skip this step - except Exception as e: - logger.error(f"Error retrieving data ID: {e}") - return + except Exception: + logger.error(f"Could not get frame!") + pass if data_id is not None: try: - # Fetch the data from the client using the ObjectID - self.frame = self.client.getID(data_id[0][0]) # Retrieve the frame data - - # Unpack the flattened data - data = np.array(self.frame, dtype=np.float64) # Ensure it's a NumPy array - frame_num = int(data[-1]) # Extract the last element as frame number - data = data[:-1].reshape(-1, 2) # Exclude the last element - - # Perform processing (e.g., scaling the y-values) - data[:, 1] *= 2 # Example: Scale y-coordinates by 2 - - # Flatten processed values and append frame number - self.processed_data = np.append(data.ravel(), frame_num) + if self.store_loc: + # Fetch the data from the client using the ObjectID + self.frame = self.client.getID(data_id[0][0]) + else: + self.frame = self.client.get(data_id) + + # Unpack the frame to get the data and frame number + data = np.array(self.frame, dtype=np.float64) + self.frame_num = int(data[-1]) + # reshape the data to 2D array + data = data[:-1].reshape(-1, 2) + + # Scale the y-values of the sine or cosine wave by random factor + scale_factor = random.randint(1, 10) + data[:, 1] *= scale_factor + + # calculate the amplitude and frequency + amplitude = np.round((data.max(axis=0)[1] - data.min(axis=0)[1]) / 2) + logger.info(f"Frame {self.frame_num} has amplitude {amplitude}") - logger.info(f"Frame {frame_num}: Processed {data.shape[0]} points") - self.socket.send(self.processed_data.tobytes()) except Exception as e: logger.error(f"Error processing frame: {e}") From 6efe127207ac01b86dddf958944f4cd26d74598c Mon Sep 17 00:00:00 2001 From: clewis7 Date: Thu, 13 Feb 2025 17:48:21 -0500 Subject: [PATCH 22/27] clean up simple viz --- demos/fastplotlib/actors/visual_processor.py | 75 ++++--- demos/fastplotlib/fastplotlib.ipynb | 209 +++++++------------ 2 files changed, 125 insertions(+), 159 deletions(-) diff --git a/demos/fastplotlib/actors/visual_processor.py b/demos/fastplotlib/actors/visual_processor.py index 4bc289fd..727a2dd3 100644 --- a/demos/fastplotlib/actors/visual_processor.py +++ b/demos/fastplotlib/actors/visual_processor.py @@ -1,76 +1,91 @@ from improv.actor import Actor -from queue import Empty +import random import logging import zmq import numpy as np +import time logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) class Processor(Actor): - """ - Process data by scaling y coordinates by 2 and send it through zmq to be visualized. + """Sample processor used to scale a sine or cosine wave and calculate the amplitude. + Intended for use with sample_generator.py. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def setup(self): - """ - Creates and binds the socket for zmq and initializes processed data storage. + """Initializes all class variables. Create and bind the socket for zmq to send to fastplotlib.ipynb + for visualization. + + self.name (string): name of the actor. + self.frame (ObjectID): StoreInterface object id referencing data from the store. + self.frame_num (int): index of current frame. + self.processed_data (np.array): raveled array containing the processed data appended with the current frame number """ self.name = "Processor" - self.processed_data = None # Initialize variable to store processed data - self.frame = None # Initialize variable to store the current frame + self.frame = None + self.frame_num = 0 + self.processed_data = None context = zmq.Context() self.socket = context.socket(zmq.PUB) self.socket.bind("tcp://127.0.0.1:5555") - logger.info("Completed setup for Processor **visual**") + logger.info("Completed setup for Processor") def stop(self): - """Trivial stop function for testing purposes.""" + """Stop function that closes the zmq socket to visualization notebook.""" logger.info("Processor stopping") self.socket.close() return 0 def runStep(self): """ - Gets from the input queue and scales the data in the y-dimension. - - Receives an ObjectID, references data in the store using that ObjectID, - processes it, and sends it through the zmq socket to be visualized. + Gets from the input queue, scales the data in the y-dimension by a random number between 1-10 inclusive and then + calculates the amplitude. """ + data_id = None try: - # Retrieve data ID from the queue data_id = self.q_in.get(timeout=0.05) - except Empty: - return # No data received, skip this step except Exception as e: - logger.error(f"Error retrieving data ID: {e}") - return + logger.error(f"Could not get frame!") + pass if data_id is not None: try: - # Fetch the data from the client using the ObjectID - self.frame = self.client.getID(data_id[0][0]) # Retrieve the frame data - - # Unpack the flattened data - data = np.array(self.frame, dtype=np.float64) # Ensure it's a NumPy array - frame_num = int(data[-1]) # Extract the last element as frame number - data = data[:-1].reshape(-1, 2) # Exclude the last element - - # Perform processing (e.g., scaling the y-values) - data[:, 1] *= 2 # Example: Scale y-coordinates by 2 + if self.store_loc: + # Fetch the data from the client using the ObjectID + self.frame = self.client.getID(data_id[0][0]) + else: + self.frame = self.client.get(data_id) + + # Unpack the frame to get the data and frame number + data = np.array(self.frame, dtype=np.float64) + self.frame_num = int(data[-1]) + # reshape the data to 2D array + data = data[:-1].reshape(-1, 2) + + # Scale the y-values of the sine or cosine wave by random factor + scale_factor = random.randint(1, 10) + data[:, 1] *= scale_factor + + # calculate the amplitude + amplitude = np.round((data.max(axis=0)[1] - data.min(axis=0)[1]) / 2) + logger.info(f"Frame {self.frame_num} has amplitude {amplitude}") # Flatten processed values and append frame number - self.processed_data = np.append(data.ravel(), frame_num) + self.processed_data = np.append(data.ravel(), self.frame_num) + + # slight pause for visualization + time.sleep(2) + logger.info("Sending data to visualization notebook!") # Send the processed data through the ZMQ socket self.socket.send(self.processed_data.tobytes()) - logger.info(f"Frame {frame_num}: Sent {data.shape[0]} points after processing") except Exception as e: logger.error(f"Error processing frame: {e}") diff --git a/demos/fastplotlib/fastplotlib.ipynb b/demos/fastplotlib/fastplotlib.ipynb index fbc39ea8..c06b4f99 100644 --- a/demos/fastplotlib/fastplotlib.ipynb +++ b/demos/fastplotlib/fastplotlib.ipynb @@ -10,44 +10,10 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "093c8fd4-bbf8-4374-a661-494a61a73cca", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0edc24b838634ad9b112e927d018c658", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "No windowing system present. Using surfaceless platform\n", - "No config found!\n", - "No config found!\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Available devices:\n", - "✅ (default) | NVIDIA GeForce GTX 1080 Ti | DiscreteGPU | Vulkan | 555.42.06\n", - "❗ | llvmpipe (LLVM 15.0.7, 256 bits) | CPU | Vulkan | Mesa 23.2.1-1ubuntu3.1~22.04.2 (LLVM 15.0.7)\n", - "❗ | NVIDIA GeForce GTX 1080 Ti/PCIe/SSE2 | Unknown | OpenGL | \n" - ] - } - ], + "outputs": [], "source": [ "import zmq\n", "import numpy as np\n", @@ -55,118 +21,90 @@ ] }, { - "cell_type": "code", - "execution_count": 2, - "id": "24d73c48-c24d-440b-8dfc-9c4a2f476233", + "cell_type": "markdown", + "id": "54d4aeca-cc13-4818-92f7-4ab7ea5734f9", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Listening...\n" - ] - } - ], "source": [ - "# ZMQ context and socket setup\n", - "context = zmq.Context()\n", - "socket = context.socket(zmq.SUB)\n", - "socket.connect(\"tcp://127.0.0.1:5555\")\n", - "socket.setsockopt_string(zmq.SUBSCRIBE, \"\")\n", - "print(\"Listening...\")" + "## Setup zmq subscriber client" ] }, { "cell_type": "code", - "execution_count": 3, - "id": "fc5d4c34-3f4e-4526-b245-1db3fcd84bf5", + "execution_count": null, + "id": "24d73c48-c24d-440b-8dfc-9c4a2f476233", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "f32b77ed6e5042e68c77134a54711c97", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/edwin/edwin_improv/fpl/lib/python3.10/site-packages/fastplotlib/graphics/_features/_base.py:21: UserWarning: casting float64 array to float32\n", - " warn(f\"casting {array.dtype} array to float32\")\n" - ] - } - ], + "outputs": [], "source": [ - "# Create the figure\n", - "figure = fpl.Figure()\n", + "context = zmq.Context()\n", + "sub = context.socket(zmq.SUB)\n", + "sub.setsockopt(zmq.SUBSCRIBE, b\"\")\n", "\n", - "# Add a line plot placeholder\n", - "xs = np.linspace(-10, 10, 100)\n", - "cos_ys = np.cos(xs)\n", - "figure[0, 0].add_line(data=np.column_stack((xs, cos_ys)), name=\"wave\")\n", + "# keep only the most recent message\n", + "sub.setsockopt(zmq.CONFLATE, 1)\n", "\n", - "# Set the dimension for reshaping\n", - "dimension = 2 # Default to 2 for [x, y] data; adjust as needed" + "# address must match publisher in Processor actor\n", + "sub.connect(\"tcp://127.0.0.1:5555\")" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "8f637c0c-3854-49ce-9d19-c692d2af61e8", "metadata": {}, "outputs": [], "source": [ "def get_buffer():\n", - " \"\"\"\n", - " Retrieve the buffer from the socket.\n", - " \"\"\"\n", + " \"\"\"Gets the buffer from the publisher.\"\"\"\n", " try:\n", - " b = socket.recv(zmq.NOBLOCK) # Non-blocking receive\n", - " return b\n", + " b = sub.recv(zmq.NOBLOCK)\n", " except zmq.Again:\n", - " return None" + " pass\n", + " else:\n", + " return b\n", + " \n", + " return None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc5d4c34-3f4e-4526-b245-1db3fcd84bf5", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the figure\n", + "figure = fpl.Figure()\n", + "\n", + "# Add a line plot placeholder\n", + "xs = np.linspace(-10, 10, 100)\n", + "ys = np.cos(xs)\n", + "wave_graphic = figure[0, 0].add_line(data=np.column_stack((xs, ys)), name=\"wave\")" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "d5240ab4-3bb4-4840-8a30-b0a60abe4aa1", "metadata": {}, "outputs": [], "source": [ "def update_frame(p):\n", - " \"\"\"\n", - " Update the frame using data received from the socket and reshape it based on the specified dimension.\n", - " \"\"\"\n", + " \"\"\"Update the frame using data received from the socket.\"\"\"\n", " buff = get_buffer()\n", " if buff is not None:\n", " # Deserialize the buffer into a NumPy array\n", " data = np.frombuffer(buff, dtype=np.float64)\n", "\n", + " print(data.shape)\n", + "\n", " # Extract the frame number from the last index\n", - " frame_num = int(data[-1]) # Last element is the frame number\n", + " frame_num = int(data[-1]) \n", "\n", - " # Reshape the remaining data based on the specified dimension\n", - " if dimension > 0:\n", - " values = data[:-1].reshape(-1, dimension) # Reshape all but the last element\n", - " else:\n", - " values = data[:-1] # No reshaping if dimension <= 0\n", + " # reshape the data to (100, 2)\n", + " data = data[:-1].reshape(100, 2) \n", "\n", " # Update the line plot\n", - " if dimension >= 2: # Ensure at least [x, y] data is available for plotting\n", - " p[\"wave\"].data[:, :dimension] = values\n", - " else:\n", - " print(f\"Received frame {frame_num}, but dimension {dimension} is insufficient for plotting.\")\n", + " p[\"wave\"].data = data\n", "\n", " # Update the plot title with the frame number\n", " p.name = f\"frame: {frame_num}\"" @@ -174,37 +112,50 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "a337ec69-1288-4c5c-a55e-c3f6a1b7bcba", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0fb5f408f5c14338b537bc78363b867e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Add the animation update function\n", "figure[0, 0].add_animations(update_frame)\n", "\n", + "# show the figure, should initially see a white cosine wave\n", "figure.show()" ] }, + { + "cell_type": "markdown", + "id": "9dcb7d8f-3f87-4965-8f66-93326954f257", + "metadata": {}, + "source": [ + "## `fastplotlib` is non-blocking :D" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bf3f56cf-2058-4b7e-af20-6561b5459e84", + "metadata": {}, + "outputs": [], + "source": [ + "wave_graphic.cmap = \"autumn\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48567be2-94b5-4eab-99ff-9d6fdc7e6038", + "metadata": {}, + "outputs": [], + "source": [ + "figure.canvas.get_stats()" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "b134c339-4570-413f-ad2d-1fd709434a3a", + "id": "383fc5f4-2d3e-4f63-bf88-22b512a2aeed", "metadata": {}, "outputs": [], "source": [] @@ -226,7 +177,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.16" } }, "nbformat": 4, From ef23ce5ca535bb62b1e92bbff50effa0dc6174a0 Mon Sep 17 00:00:00 2001 From: clewis7 Date: Thu, 13 Feb 2025 22:18:08 -0500 Subject: [PATCH 23/27] add lorenz yaml --- demos/fastplotlib/lorenz.yaml | 14 ++++++ demos/fastplotlib/lorenz_fpl.ipynb | 78 ++++++++++++++++++++++-------- 2 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 demos/fastplotlib/lorenz.yaml diff --git a/demos/fastplotlib/lorenz.yaml b/demos/fastplotlib/lorenz.yaml new file mode 100644 index 00000000..e4c36e05 --- /dev/null +++ b/demos/fastplotlib/lorenz.yaml @@ -0,0 +1,14 @@ +actors: + Generator: + package: actors.lorenz_generator + class: Generator + + Processor: + package: actors.lorenz_processor + class: Processor + +connections: + Generator.q_out: [Processor.q_in] + +redis_config: + port: 6381 \ No newline at end of file diff --git a/demos/fastplotlib/lorenz_fpl.ipynb b/demos/fastplotlib/lorenz_fpl.ipynb index ac4b5831..459e7590 100644 --- a/demos/fastplotlib/lorenz_fpl.ipynb +++ b/demos/fastplotlib/lorenz_fpl.ipynb @@ -2,10 +2,47 @@ "cells": [ { "cell_type": "code", - "execution_count": 7, + "execution_count": 1, "id": "068919b3-0a3a-47ff-836b-221079d6e095", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5e2fa5961fe740028e3c5f5f4de983d0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", + "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", + "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", + "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", + "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Available devices:\n", + "🯄 | Intel(R) Arc(tm) Graphics (MTL) | IntegratedGPU | Vulkan | Mesa 24.3.2\n", + "✅ (default) | NVIDIA GeForce RTX 4060 Laptop GPU | DiscreteGPU | Vulkan | 565.77\n", + "❗ | llvmpipe (LLVM 19.1.5, 256 bits) | CPU | Vulkan | Mesa 24.3.2 (LLVM 19.1.5)\n", + "❗ | Mesa Intel(R) Arc(tm) Graphics (MTL) | IntegratedGPU | OpenGL | \n" + ] + } + ], "source": [ "import zmq\n", "import numpy as np\n", @@ -14,7 +51,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 2, "id": "c665d7f4-d7eb-4ef7-a9c8-d737b9f0b073", "metadata": {}, "outputs": [ @@ -37,14 +74,14 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 3, "id": "147e4be8-fb62-4463-bf6f-0a8e32593c4e", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8d34249a9edf438e8d1d6bde727c0252", + "model_id": "5bc90b57532946aaac4b1ac010422a5b", "version_major": 2, "version_minor": 0 }, @@ -54,25 +91,26 @@ }, "metadata": {}, "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n" + ] } ], "source": [ "# Create the figure\n", "figure = fpl.Figure(\n", - " cameras=\"3d\",\n", - " controller_types=\"fly\",\n", - ")\n", - "\n", - "# Placeholder for the first data reception\n", - "is_first_data = True\n", - "\n", - "# Set the dimension for reshaping\n", - "dimension = 3 # Default to 2 for [x, y] data; adjust as needed" + " cameras=\"3d\",\n", + " controller_types=\"fly\",\n", + ")" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 4, "id": "28f334db-aadb-40fc-b067-c5b4c4a146d1", "metadata": {}, "outputs": [], @@ -90,7 +128,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 5, "id": "9c3ac076-d4a8-4877-8843-c5e6cc54a20c", "metadata": {}, "outputs": [], @@ -142,14 +180,14 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 6, "id": "439abe77-263c-4ab4-b810-5c7d5e6029e1", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9e90468fbbb7456f9b9b12f1a60cdafc", + "model_id": "c0bc0608398d4e0dbfbb548b53b6181b", "version_major": 2, "version_minor": 0 }, @@ -157,14 +195,14 @@ "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" ] }, - "execution_count": 12, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Add the animation update function\n", - "figure[0, 0].add_animations(update_frame)\n", + "#figure[0, 0].add_animations(update_frame)\n", "\n", "figure.show()" ] From 5a7fefaf398324de19475fcc75b1835c4bfe0142 Mon Sep 17 00:00:00 2001 From: clewis7 Date: Thu, 13 Feb 2025 23:11:52 -0500 Subject: [PATCH 24/27] finalize simple viz demo --- demos/fastplotlib/actors/visual_processor.py | 18 ++++++------ demos/fastplotlib/fastplotlib.ipynb | 29 +++++++++++++++++--- demos/minimal/actors/sample_generator.py | 4 +++ demos/minimal/actors/sample_processor.py | 2 +- demos/minimal/requirements.txt | 2 ++ 5 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 demos/minimal/requirements.txt diff --git a/demos/fastplotlib/actors/visual_processor.py b/demos/fastplotlib/actors/visual_processor.py index 727a2dd3..9dc9ec10 100644 --- a/demos/fastplotlib/actors/visual_processor.py +++ b/demos/fastplotlib/actors/visual_processor.py @@ -4,6 +4,7 @@ import zmq import numpy as np import time +from queue import Empty logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) @@ -46,14 +47,18 @@ def stop(self): def runStep(self): """ Gets from the input queue, scales the data in the y-dimension by a random number between 1-10 inclusive and then - calculates the amplitude. + calculates the amplitude of the wave. """ + # delay frame unpacking for visualization purposes + time.sleep(0.5) + data_id = None try: data_id = self.q_in.get(timeout=0.05) + except Empty: + pass except Exception as e: logger.error(f"Could not get frame!") - pass if data_id is not None: try: @@ -63,6 +68,7 @@ def runStep(self): else: self.frame = self.client.get(data_id) + # Unpack the frame to get the data and frame number data = np.array(self.frame, dtype=np.float64) self.frame_num = int(data[-1]) @@ -80,12 +86,8 @@ def runStep(self): # Flatten processed values and append frame number self.processed_data = np.append(data.ravel(), self.frame_num) - # slight pause for visualization - time.sleep(2) - - logger.info("Sending data to visualization notebook!") - # Send the processed data through the ZMQ socket - self.socket.send(self.processed_data.tobytes()) + # Send the processed data through the ZMQ socket to be visualized + self.socket.send(self.processed_data) except Exception as e: logger.error(f"Error processing frame: {e}") diff --git a/demos/fastplotlib/fastplotlib.ipynb b/demos/fastplotlib/fastplotlib.ipynb index c06b4f99..ba122d59 100644 --- a/demos/fastplotlib/fastplotlib.ipynb +++ b/demos/fastplotlib/fastplotlib.ipynb @@ -75,12 +75,20 @@ "# Create the figure\n", "figure = fpl.Figure()\n", "\n", - "# Add a line plot placeholder\n", + "# Add an initial cosine wave\n", "xs = np.linspace(-10, 10, 100)\n", "ys = np.cos(xs)\n", "wave_graphic = figure[0, 0].add_line(data=np.column_stack((xs, ys)), name=\"wave\")" ] }, + { + "cell_type": "markdown", + "id": "4b08bd74-9c6f-4c0a-981f-009028e62133", + "metadata": {}, + "source": [ + "## Define function to fetch the buffer, unpack it, and update the plot " + ] + }, { "cell_type": "code", "execution_count": null, @@ -104,10 +112,13 @@ " data = data[:-1].reshape(100, 2) \n", "\n", " # Update the line plot\n", - " p[\"wave\"].data = data\n", + " p[\"wave\"].data[:,:2] = data\n", "\n", " # Update the plot title with the frame number\n", - " p.name = f\"frame: {frame_num}\"" + " p.name = f\"frame: {frame_num}\"\n", + "\n", + " # data is scaled in the y\n", + " p.auto_scale()" ] }, { @@ -155,7 +166,17 @@ { "cell_type": "code", "execution_count": null, - "id": "383fc5f4-2d3e-4f63-bf88-22b512a2aeed", + "id": "811943fd-72e2-4323-be98-4694e86fbd2b", + "metadata": {}, + "outputs": [], + "source": [ + "figure[0,0].auto_scale()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8c0f7fb-e6df-484f-b7ff-fa402fac373a", "metadata": {}, "outputs": [], "source": [] diff --git a/demos/minimal/actors/sample_generator.py b/demos/minimal/actors/sample_generator.py index b087e300..370ca6b4 100644 --- a/demos/minimal/actors/sample_generator.py +++ b/demos/minimal/actors/sample_generator.py @@ -48,6 +48,10 @@ def runStep(self): If the frame number is odd, the data is a sine wave. If the frame number is even, the data is a cosine wave. """ + # set a max number of frames to generate + if self.frame_num > 1000: + return + xs = np.linspace(-10, 10, 100) # Generate sine or cosine values based on frame number diff --git a/demos/minimal/actors/sample_processor.py b/demos/minimal/actors/sample_processor.py index b9bf6242..2904f82e 100644 --- a/demos/minimal/actors/sample_processor.py +++ b/demos/minimal/actors/sample_processor.py @@ -38,7 +38,7 @@ def stop(self): def runStep(self): """ Gets from the input queue, scales the data in the y-dimension by a random number between 1-10 inclusive and then - calculates the amplitude. + calculates the amplitude of the wave. """ data_id = None diff --git a/demos/minimal/requirements.txt b/demos/minimal/requirements.txt new file mode 100644 index 00000000..384e4951 --- /dev/null +++ b/demos/minimal/requirements.txt @@ -0,0 +1,2 @@ +simplejpeg +fastplotlib[notebook] \ No newline at end of file From 4cbeeca0fee60cf5d3662dd2939b80747777cc0d Mon Sep 17 00:00:00 2001 From: clewis7 Date: Fri, 14 Feb 2025 00:20:26 -0500 Subject: [PATCH 25/27] fix lorenz data generation, update README --- demos/fastplotlib/actors/lorenz_generator.py | 100 +++++++---------- .../{lorenz_fpl.ipynb => lorenz.ipynb} | 102 ++++++++++-------- demos/minimal/README.md | 56 ++++------ 3 files changed, 123 insertions(+), 135 deletions(-) rename demos/fastplotlib/{lorenz_fpl.ipynb => lorenz.ipynb} (62%) diff --git a/demos/fastplotlib/actors/lorenz_generator.py b/demos/fastplotlib/actors/lorenz_generator.py index 5cde9fc1..336d6f81 100644 --- a/demos/fastplotlib/actors/lorenz_generator.py +++ b/demos/fastplotlib/actors/lorenz_generator.py @@ -1,8 +1,7 @@ -from improv.actor import Actor, RunManager -from datetime import date # used for saving +from improv.actor import Actor import numpy as np import logging -import time # Importing time module for the delay +import time logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) @@ -10,87 +9,70 @@ class Generator(Actor): """ Generates coordinates for the Lorenz system in real time. - Computes the next Lorenz coordinates every half second. - Outputs a flattened array with progressively filled 100 (x, y, z) triplets - and appends the frame number at the end. + Computes the next 50 Lorenz coordinates every half second. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.data = None # Placeholder for the 2D array - self.coordinates = [] - self.name = "lorenz_generator" - self.dt = 0.01 # Time step for numerical integration - self.max_points = 6000 # Total number of (x, y, z) triplets - self.points_per_frame = 50 # Number of points to add per frame + self.data = None + self.name = "Lorenz Generator" + self.dt = 0.01 # time step for numerical integration + self.frame_num = 0 def __str__(self): return f"Name: {self.name}, Current Coordinates: {self.coordinates[-1] if self.coordinates else None}" def setup(self): - """Initializes all class variables. + """Generates an array that serves as an initial source of data. - self.coordinates (list): List containing the initial 3D coordinate [x, y, z]. - self.data (ndarray): 2D NumPy array initialized with zeros to store x, y, z coordinates - for a maximum of `self.max_points` rows. - self.frame_num (int): Index of the current frame, initialized to 0. - self.current_index (int): Tracks the current position in the `self.data` array for filling new values. + Initial data is the starting point (x,y,z) for the lorenz attractor. """ - logger.info("Beginning setup for LorenzGenerator") - initial_coordinate = np.array([1.0, 1.0, 1.0]) # Initial coordinates (x, y, z) - self.coordinates = [initial_coordinate] + logger.info("Beginning setup for Lorenz Generator") - # Initialize the data array as a 2D array (100 rows for x, y, z coordinates) - self.data = np.zeros((self.max_points, 3)) # Shape (1000, 3) - self.frame_num = 0 - self.current_index = 0 # Tracks the current position to fill in the data array - logger.info(f"Initialized Lorenz system with initial coordinates: {initial_coordinate}") + # initial condition + self.data = np.array([1.0, 1.0, 1.0]).reshape(1,3) + + logger.info(f"Initialized Lorenz system with initial coordinates: {self.data}") def stop(self): - """ - Save the last Lorenz coordinates to a file for persistence. - """ - logger.info("LorenzGenerator stopping") - np.save("lorenz_last_coordinate.npy", self.coordinates[-1]) + """Trivial stop function.""" + logger.info("Lorenz Generator stopping") return 0 def runStep(self): - """ - Generates the next 10 Lorenz coordinates and fills them into the 2D data array. - Sends the progressively filled data as a flattened array with the frame number appended. - """ - time.sleep(0.5) # Delay for half a second - - try: - # Add the next 10 points - for _ in range(self.points_per_frame): - if self.current_index >= self.max_points: - logger.info(f"Data array fully filled for frame {self.frame_num}.") - break # Stop filling if all 1000 points are generated + """Generates the next 10 points in the lorenz system. - # Compute the next coordinate - derivative = lorenz(self.coordinates[-1]) - next_coordinate = self.coordinates[-1] + derivative * self.dt - self.coordinates.append(next_coordinate) + Sends the progressively filled data as a flattened array with the frame number appended to the processor. + """ + if self.data.shape[0] >= 3000: + logger.info(f"Reached end of data generation.") + return - # Fill x, y, and z coordinates into the 2D data array - self.data[self.current_index, 0] = next_coordinate[0] # x-coordinate - self.data[self.current_index, 1] = next_coordinate[1] # y-coordinate - self.data[self.current_index, 2] = next_coordinate[2] # z-coordinate + time.sleep(0.5) # Delay for half a second - self.current_index += 1 # Increment the index for the next point + # Add the next 10 points + for _ in range(10): + # Compute the next coordinate + derivative = lorenz(self.data[-1]) + next_coordinate = self.data[-1] + derivative * self.dt + self.data = np.vstack((self.data, next_coordinate)) - # Flatten the 2D array and append the frame number in one step - data_to_send = np.append(np.ravel(self.data), self.frame_num) + # create flattened array with xyz coordinates along with the current frame number + data = np.append(self.data.ravel(), self.frame_num) - # Send the data - data_id = self.client.put(data_to_send, str(f"Lorenz_Frame: {self.frame_num}")) - self.q_out.put([[data_id, str(self.frame_num)]]) + # Send the flattened array with frame_num + try: + data_id = self.client.put(data) + if self.store_loc: + self.q_out.put([[data_id, str(self.frame_num)]]) + else: + self.q_out.put(data_id) - # Increment frame number for the next step + # Increment frame number self.frame_num += 1 except Exception as e: - logger.error(f"LorenzGenerator Exception: {e}") + logger.error(f"--------------------------------Generator Exception: {e}") + def lorenz(xyz, s=10, r=28, b=2.667): """ diff --git a/demos/fastplotlib/lorenz_fpl.ipynb b/demos/fastplotlib/lorenz.ipynb similarity index 62% rename from demos/fastplotlib/lorenz_fpl.ipynb rename to demos/fastplotlib/lorenz.ipynb index 459e7590..ddcd5509 100644 --- a/demos/fastplotlib/lorenz_fpl.ipynb +++ b/demos/fastplotlib/lorenz.ipynb @@ -1,15 +1,34 @@ { "cells": [ + { + "cell_type": "markdown", + "id": "df609e65-ba82-41fa-82b2-a20aa81b5803", + "metadata": {}, + "source": [ + "## `fastplotlib` lorenz attractor demo\n", + "\n", + "The Lorenz system is a dynamical system known for having chaotic solutions for certain parameter values and initial conditions. See [here](https://en.wikipedia.org/wiki/Lorenz_system) for more details. " + ] + }, { "cell_type": "code", "execution_count": 1, "id": "068919b3-0a3a-47ff-836b-221079d6e095", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", + "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", + "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n" + ] + }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5e2fa5961fe740028e3c5f5f4de983d0", + "model_id": "3cb0a2d60f434a5ba8a5b8a70c9ce4f6", "version_major": 2, "version_minor": 0 }, @@ -20,27 +39,25 @@ "metadata": {}, "output_type": "display_data" }, + { + "data": { + "text/html": [ + "Available devices:
ValidDeviceTypeBackendDriver
Intel(R) Arc(tm) Graphics (MTL)IntegratedGPUVulkanMesa 24.3.2
✅ (default) NVIDIA GeForce RTX 4060 Laptop GPUDiscreteGPUVulkan565.77
❗ limitedllvmpipe (LLVM 19.1.5, 256 bits)CPUVulkanMesa 24.3.2 (LLVM 19.1.5)
Mesa Intel(R) Arc(tm) Graphics (MTL)IntegratedGPUOpenGL4.6 (Core Profile) Mesa 24.3.2
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, { "name": "stderr", "output_type": "stream", "text": [ - "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", - "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", - "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n" ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Available devices:\n", - "🯄 | Intel(R) Arc(tm) Graphics (MTL) | IntegratedGPU | Vulkan | Mesa 24.3.2\n", - "✅ (default) | NVIDIA GeForce RTX 4060 Laptop GPU | DiscreteGPU | Vulkan | 565.77\n", - "❗ | llvmpipe (LLVM 19.1.5, 256 bits) | CPU | Vulkan | Mesa 24.3.2 (LLVM 19.1.5)\n", - "❗ | Mesa Intel(R) Arc(tm) Graphics (MTL) | IntegratedGPU | OpenGL | \n" - ] } ], "source": [ @@ -81,7 +98,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5bc90b57532946aaac4b1ac010422a5b", + "model_id": "e0894ff80ae843b59e3cc470681f56e2", "version_major": 2, "version_minor": 0 }, @@ -110,7 +127,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "28f334db-aadb-40fc-b067-c5b4c4a146d1", "metadata": {}, "outputs": [], @@ -128,7 +145,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "9c3ac076-d4a8-4877-8843-c5e6cc54a20c", "metadata": {}, "outputs": [], @@ -137,42 +154,43 @@ " \"\"\"\n", " Update the frame using data received from the socket and reshape it based on the specified dimension.\n", " \"\"\"\n", - " global is_first_data\n", "\n", " buff = get_buffer()\n", " if buff is not None:\n", " # Deserialize the buffer into a NumPy array\n", " data = np.frombuffer(buff, dtype=np.float64)\n", "\n", + " print(data.shape)\n", + "\n", " # Extract the frame number from the last index\n", " frame_num = int(data[-1]) # Last element is the frame number\n", "\n", - " # Reshape the remaining data based on the specified dimension\n", - " if dimension > 0:\n", - " values = data[:-1].reshape(-1, dimension) # Reshape all but the last element\n", - " else:\n", - " values = data[:-1] # No reshaping if dimension <= 0\n", + " # # Reshape the remaining data based on the specified dimension\n", + " # if dimension > 0:\n", + " # values = data[:-1].reshape(-1, dimension) # Reshape all but the last element\n", + " # else:\n", + " # values = data[:-1] # No reshaping if dimension <= 0\n", "\n", - " if is_first_data:\n", - " # Initialize the plot with the appropriate number of points\n", - " n_points = (len(data) - 1) // dimension\n", - " xs = np.linspace(-10, 10, n_points)\n", + " # if is_first_data:\n", + " # # Initialize the plot with the appropriate number of points\n", + " # n_points = (len(data) - 1) // dimension\n", + " # xs = np.linspace(-10, 10, n_points)\n", " \n", - " # Create additional columns of zeros based on the number of dimensions\n", - " columns = [xs] + [np.zeros_like(xs) for _ in range(dimension - 1)]\n", - " formatted_data = np.column_stack(columns)\n", + " # # Create additional columns of zeros based on the number of dimensions\n", + " # columns = [xs] + [np.zeros_like(xs) for _ in range(dimension - 1)]\n", + " # formatted_data = np.column_stack(columns)\n", " \n", - " # Add the line to the plot with the generated data\n", - " figure[0, 0].add_line(data=formatted_data, name=\"wave\", cmap='jet')\n", - " is_first_data = False\n", + " # # Add the line to the plot with the generated data\n", + " # figure[0, 0].add_line(data=formatted_data, name=\"wave\", cmap='jet')\n", + " # is_first_data = False\n", "\n", "\n", - " # Update the line plot\n", - " if dimension >= 2: # Ensure at least [x, y] data is available for plotting\n", - " p[\"wave\"].data[:, :dimension] = values\n", + " # # Update the line plot\n", + " # if dimension >= 2: # Ensure at least [x, y] data is available for plotting\n", + " # p[\"wave\"].data[:, :dimension] = values\n", " \n", - " else:\n", - " print(f\"Received frame {frame_num}, but dimension {dimension} is insufficient for plotting.\")\n", + " # else:\n", + " # print(f\"Received frame {frame_num}, but dimension {dimension} is insufficient for plotting.\")\n", "\n", " # Update the plot title with the frame number\n", " p.name = f\"frame: {frame_num}\"" @@ -180,14 +198,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "439abe77-263c-4ab4-b810-5c7d5e6029e1", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c0bc0608398d4e0dbfbb548b53b6181b", + "model_id": "a7dfb755fc464070a3e0d3eaae3a6210", "version_major": 2, "version_minor": 0 }, @@ -195,7 +213,7 @@ "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" ] }, - "execution_count": 6, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } diff --git a/demos/minimal/README.md b/demos/minimal/README.md index c646049c..4c46572d 100644 --- a/demos/minimal/README.md +++ b/demos/minimal/README.md @@ -4,16 +4,17 @@ This folder contains a minimal demo for running improv. In this demo, data gener stored in a data store and a key to the generated data is sent via a queue to the **processor** `actor` that accesses the data in the store and processes it. -### Make sure the generator and processor in the minimal.yaml file are actors.sample_generator and actors.sample_processor respectively. +### Running the minimal demo + +> **_NOTE:_** Make sure the generator and processor specified in the minimal.yaml file +> are `actors.sample_generator` and `actors.sample_processor` respectively. Usage: ```bash -# cd to this dir -cd .../improv/demos/minimal # start improv -improv run ./minimal.yaml +improv run ./demos/minimal/minimal.yaml # call `setup` in the improv TUI setup @@ -25,29 +26,29 @@ run stop ``` -## Sample Visualization using `fastplotlib` +If you look at the `global.log` file, you should see outputted information about each generated frame. + +## Simple Visualization using `fastplotlib` You can also run the minimal demo and visualize the generated data using `fastplotlib`. -As before, data is generated by the **generator** `actor` is stored in a data store and a key to the generated data is +As before, data generated by the **generator** `actor` is stored in a data store and a key to the generated data is sent via a queue to the **processor** `actor` that accesses the data in the store and processes it. Additionally, the -`fastplotlib.ipynb` notebook then receives the most recent data via `zmq` and displays it using +`fastplotlib.ipynb` notebook receives the most recent data from the processor via `zmq` and displays it using [`fastplotlib`](https://github.com/fastplotlib/fastplotlib). ### Instructions -1. Update the processor in minimal.yaml to actors.visual_processor and keep generator as actors.sample_generator. +1. Update the processor in minimal.yaml to `actors.visual_processor`. 2. Run `pip install -r requirements.txt` in this directory. Usage: ```bash -# cd to this dir -cd .../improv/demos # start improv with actor paths to locate the sample_generator and visual_processor actor files -improv run -a fastplotlib/ -a minimal/ minimal/minimal.yaml +improv run -a ./demos/fastplotlib/ -a ./demos/minimal/ ./demos/minimal/minimal.yaml # call `setup` in the improv TUI setup @@ -65,48 +66,35 @@ run stop ``` -#### Note: The `fastplotlib.ipynb` can only be run in `jupyter lab` +> **_NOTE:_** The `fastplotlib.ipynb` can only be run in `jupyter lab` -## Sample Lorenz Visualization using `fastplotlib` +## Dynamical System Visualization using `fastplotlib` -You can also run the minimal demo and visualize the generated data using `fastplotlib`. +The Lorenz system is a dynamical system known for having chaotic solutions for certain parameter values and initial +conditions. See [here](https://en.wikipedia.org/wiki/Lorenz_system) for more details. -As before, data is generated by the **generator** `actor` is stored in a data store and a key to the generated data is -sent via a queue to the **processor** `actor` that accesses the data in the store and processes it. Additionally, the -`lorenz_fpl.ipynb` notebook then receives the most recent data via `zmq` and displays it using -[`fastplotlib`](https://github.com/fastplotlib/fastplotlib). +This demo incrementally generates points in a Lorenz system based off of pre-defined parameters and an initial condition. +The data sent from the processor via `zmq` to the `lorenz.ipynb` updates the visualization, adding new points as they arrive. +Over time, you should see the build-up of a lorenz attractor. ### Instructions -1. Update the processor in minimal.yaml to actors.lorenz_processor and generator to actors.lorenz_generator. - -2. Run `pip install -r requirements.txt` in this directory if not ran already from previous sample visualization. - -Usage: - ```bash -# cd to this dir -cd .../improv/demos - -# start improv with actor paths to locate the sample_generator and visual_processor actor files -improv run -a fastplotlib/ minimal/minimal.yaml +improv run ./demos/fastplotlib/lorenz.yaml # call `setup` in the improv TUI setup -# Run the cells in the jupyter notebook until you receive -# a plot that has a black box. +# Run the cells in the jupyter notebook until you get an empty plot # once the plot is ready call `run` in the improv TUI run -# Note: If the visualization is still a black box, press -# the auto-scale scene button in the bottom left corner. - # You should see the plot updating with a lorenz attractor # curve that changes colors according to the z values. # when you are ready to stop the process, call `stop` in the improv TUI stop +``` From 29cfec87339a9115abfb7196631f89215591b4c8 Mon Sep 17 00:00:00 2001 From: clewis7 Date: Fri, 14 Feb 2025 10:24:33 -0500 Subject: [PATCH 26/27] clean up lorenz demo --- demos/fastplotlib/actors/lorenz_generator.py | 10 +- demos/fastplotlib/actors/lorenz_processor.py | 55 ++--- demos/fastplotlib/lorenz.ipynb | 233 +++++++------------ 3 files changed, 109 insertions(+), 189 deletions(-) diff --git a/demos/fastplotlib/actors/lorenz_generator.py b/demos/fastplotlib/actors/lorenz_generator.py index 336d6f81..ced8bb21 100644 --- a/demos/fastplotlib/actors/lorenz_generator.py +++ b/demos/fastplotlib/actors/lorenz_generator.py @@ -1,7 +1,6 @@ from improv.actor import Actor import numpy as np import logging -import time logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) @@ -40,18 +39,15 @@ def stop(self): return 0 def runStep(self): - """Generates the next 10 points in the lorenz system. + """Generates the next 25 points in the lorenz system. Sends the progressively filled data as a flattened array with the frame number appended to the processor. """ - if self.data.shape[0] >= 3000: - logger.info(f"Reached end of data generation.") + if self.frame_num >= 150: return - time.sleep(0.5) # Delay for half a second - # Add the next 10 points - for _ in range(10): + for _ in range(25): # Compute the next coordinate derivative = lorenz(self.data[-1]) next_coordinate = self.data[-1] + derivative * self.dt diff --git a/demos/fastplotlib/actors/lorenz_processor.py b/demos/fastplotlib/actors/lorenz_processor.py index 14e78949..31d9e8fc 100644 --- a/demos/fastplotlib/actors/lorenz_processor.py +++ b/demos/fastplotlib/actors/lorenz_processor.py @@ -1,5 +1,5 @@ from improv.actor import Actor -from queue import Empty +import time import logging import zmq import numpy as np @@ -20,11 +20,11 @@ def __init__(self, *args, **kwargs): def setup(self): """ - Sets up the ZMQ socket and initializes storage for processed data. + Sets up the ZMQ socket and initialize class variables. """ self.name = "Processor" - self.processed_data = None # Storage for processed data - self.frame = None # Storage for the current frame + self.frame = None + self.frame_num = None # Set up ZMQ PUB socket context = zmq.Context() @@ -35,7 +35,7 @@ def setup(self): def stop(self): """ - Trivial stop function for testing purposes. + Stop function. Closes the ZMQ socket connection. """ logger.info("Processor stopping") self.socket.close() @@ -43,41 +43,36 @@ def stop(self): def runStep(self): """ - Processes incoming Lorenz data, applies transformations, and sends - the processed data through a ZMQ socket. + Trivial processing step that gets the lorenz data and passes it through the + ZMQ socket for visualization. """ + # Delay for half a second for visualization purposes + time.sleep(0.5) + + data_id = None try: - # Retrieve data ID from the input queue data_id = self.q_in.get(timeout=0.05) - except Empty: - return # No data received, skip this step - except Exception as e: - logger.error(f"Error retrieving data ID: {e}") - return + except Exception: + logger.error(f"Could not get frame!") + pass if data_id is not None: try: - # Fetch the data from the client using the ObjectID - self.frame = self.client.getID(data_id[0][0]) # Retrieve the frame data + if self.store_loc: + # Fetch the data from the client using the ObjectID + self.frame = self.client.getID(data_id[0][0]) + else: + self.frame = self.client.get(data_id) - # Convert the frame data to a NumPy array - data = np.array(self.frame, dtype=np.float64) # Ensure it's a NumPy array - frame_num = int(data[-1]) # Extract the last element as the frame number - data = data[:-1].reshape(-1, 3) # Exclude the last element (frame number) - - # Perform processing on the Lorenz coordinates - # Example 1: Scale x-coordinates by 0.5 and y-coordinates by 2 - data[:, 0] *= 2 # Scale x-coordinates - data[:, 1] *= 2 # Scale y-coordinates - data[:, 2] *= 2 # Scale z-coordinates + # unpack the frame to get the frame number + data = np.array(self.frame, dtype=np.float64) + self.frame_num = int(data[-1]) - # Flatten processed values and append frame number - self.processed_data = np.append(np.ravel(data), frame_num) # Send the processed data through the ZMQ socket - self.socket.send(self.processed_data.tobytes()) - logger.info(f"Frame {frame_num}: Sent points with size {data.shape} after processing") + self.socket.send(data) + logger.info(f"Frame {self.frame_num}: Sent points with size {data.shape} after processing") except Exception as e: - logger.error(f"Error processing frame: {e}") + logger.error(f"Error processing frame: {e}") \ No newline at end of file diff --git a/demos/fastplotlib/lorenz.ipynb b/demos/fastplotlib/lorenz.ipynb index ddcd5509..efb2cbe7 100644 --- a/demos/fastplotlib/lorenz.ipynb +++ b/demos/fastplotlib/lorenz.ipynb @@ -12,54 +12,10 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "068919b3-0a3a-47ff-836b-221079d6e095", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", - "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", - "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3cb0a2d60f434a5ba8a5b8a70c9ce4f6", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "Available devices:
ValidDeviceTypeBackendDriver
Intel(R) Arc(tm) Graphics (MTL)IntegratedGPUVulkanMesa 24.3.2
✅ (default) NVIDIA GeForce RTX 4060 Laptop GPUDiscreteGPUVulkan565.77
❗ limitedllvmpipe (LLVM 19.1.5, 256 bits)CPUVulkanMesa 24.3.2 (LLVM 19.1.5)
Mesa Intel(R) Arc(tm) Graphics (MTL)IntegratedGPUOpenGL4.6 (Core Profile) Mesa 24.3.2
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n", - "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n" - ] - } - ], + "outputs": [], "source": [ "import zmq\n", "import numpy as np\n", @@ -67,160 +23,125 @@ ] }, { - "cell_type": "code", - "execution_count": 2, - "id": "c665d7f4-d7eb-4ef7-a9c8-d737b9f0b073", + "cell_type": "markdown", + "id": "ae2be309-45ad-4e5b-b8a7-1acd4c6aa112", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Listening...\n" - ] - } - ], "source": [ - "# ZMQ context and socket setup\n", - "context = zmq.Context()\n", - "socket = context.socket(zmq.SUB)\n", - "socket.connect(\"tcp://127.0.0.1:5555\")\n", - "socket.setsockopt_string(zmq.SUBSCRIBE, \"\")\n", - "print(\"Listening...\")" + "## Setup zmq subscriber client" ] }, { "cell_type": "code", - "execution_count": 3, - "id": "147e4be8-fb62-4463-bf6f-0a8e32593c4e", + "execution_count": null, + "id": "c665d7f4-d7eb-4ef7-a9c8-d737b9f0b073", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e0894ff80ae843b59e3cc470681f56e2", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Detected skylake derivative running on mesa i915. Clears to srgb textures will use manual shader clears.\n" - ] - } - ], + "outputs": [], "source": [ - "# Create the figure\n", - "figure = fpl.Figure(\n", - " cameras=\"3d\",\n", - " controller_types=\"fly\",\n", - ")" + "context = zmq.Context()\n", + "sub = context.socket(zmq.SUB)\n", + "sub.setsockopt(zmq.SUBSCRIBE, b\"\")\n", + "\n", + "# keep only the most recent message\n", + "sub.setsockopt(zmq.CONFLATE, 1)\n", + "\n", + "# address must match publisher in Processor actor\n", + "sub.connect(\"tcp://127.0.0.1:5555\")" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "28f334db-aadb-40fc-b067-c5b4c4a146d1", "metadata": {}, "outputs": [], "source": [ "def get_buffer():\n", - " \"\"\"\n", - " Retrieve the buffer from the socket.\n", - " \"\"\"\n", + " \"\"\"Gets the buffer from the publisher.\"\"\"\n", " try:\n", - " b = socket.recv(zmq.NOBLOCK) # Non-blocking receive\n", - " return b\n", + " b = sub.recv(zmq.NOBLOCK)\n", " except zmq.Again:\n", - " return None" + " pass\n", + " else:\n", + " return b\n", + " \n", + " return None" + ] + }, + { + "cell_type": "markdown", + "id": "b4e60560-73f3-41a7-a680-1394109eafcb", + "metadata": {}, + "source": [ + "## Create a figure" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, + "id": "147e4be8-fb62-4463-bf6f-0a8e32593c4e", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the figure\n", + "figure = fpl.Figure(\n", + " cameras=\"3d\",\n", + " controller_types=\"fly\",\n", + ")\n", + "\n", + "# turn off axes\n", + "figure[0,0].axes.visible = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, "id": "9c3ac076-d4a8-4877-8843-c5e6cc54a20c", "metadata": {}, "outputs": [], "source": [ "def update_frame(p):\n", - " \"\"\"\n", - " Update the frame using data received from the socket and reshape it based on the specified dimension.\n", - " \"\"\"\n", - "\n", + " \"\"\"Update the frame using data received from the socket.\"\"\"\n", " buff = get_buffer()\n", " if buff is not None:\n", " # Deserialize the buffer into a NumPy array\n", " data = np.frombuffer(buff, dtype=np.float64)\n", "\n", - " print(data.shape)\n", - "\n", " # Extract the frame number from the last index\n", - " frame_num = int(data[-1]) # Last element is the frame number\n", - "\n", - " # # Reshape the remaining data based on the specified dimension\n", - " # if dimension > 0:\n", - " # values = data[:-1].reshape(-1, dimension) # Reshape all but the last element\n", - " # else:\n", - " # values = data[:-1] # No reshaping if dimension <= 0\n", + " frame_num = int(data[-1]) \n", "\n", - " # if is_first_data:\n", - " # # Initialize the plot with the appropriate number of points\n", - " # n_points = (len(data) - 1) // dimension\n", - " # xs = np.linspace(-10, 10, n_points)\n", - " \n", - " # # Create additional columns of zeros based on the number of dimensions\n", - " # columns = [xs] + [np.zeros_like(xs) for _ in range(dimension - 1)]\n", - " # formatted_data = np.column_stack(columns)\n", - " \n", - " # # Add the line to the plot with the generated data\n", - " # figure[0, 0].add_line(data=formatted_data, name=\"wave\", cmap='jet')\n", - " # is_first_data = False\n", + " # after the first couple of frames generated, need to auto scale\n", + " if frame_num == 3:\n", + " p.auto_scale()\n", "\n", + " data = data[:-1].reshape(-1, 3)\n", "\n", - " # # Update the line plot\n", - " # if dimension >= 2: # Ensure at least [x, y] data is available for plotting\n", - " # p[\"wave\"].data[:, :dimension] = values\n", + " # clear the plot to add updated data\n", + " p.clear()\n", " \n", - " # else:\n", - " # print(f\"Received frame {frame_num}, but dimension {dimension} is insufficient for plotting.\")\n", + " # # add graphic for current data received\n", + " p.add_line(data, cmap=\"jet\", thickness=2)\n", "\n", " # Update the plot title with the frame number\n", " p.name = f\"frame: {frame_num}\"" ] }, + { + "cell_type": "markdown", + "id": "d72d810d-36df-4b00-8b17-f90bde8cc473", + "metadata": {}, + "source": [ + "## Use can use the `w, a, s, d` keys to \"fly\" around the plot as the data is being generated in 3D" + ] + }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "439abe77-263c-4ab4-b810-5c7d5e6029e1", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a7dfb755fc464070a3e0d3eaae3a6210", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Add the animation update function\n", - "#figure[0, 0].add_animations(update_frame)\n", + "figure[0, 0].add_animations(update_frame)\n", "\n", "figure.show()" ] @@ -228,7 +149,15 @@ { "cell_type": "code", "execution_count": null, - "id": "94f0128b-1188-44f0-8b93-2ae1d54045e8", + "id": "cab2edd6-c7d9-4ae2-bb5f-33d54efb7134", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74b6e08b-83b2-4394-a8a8-4745ca09a88a", "metadata": {}, "outputs": [], "source": [] From 9208d4259e8536685200e7063524fb8080bb7cf5 Mon Sep 17 00:00:00 2001 From: clewis7 Date: Fri, 14 Feb 2025 10:25:07 -0500 Subject: [PATCH 27/27] small change --- demos/minimal/actors/sample_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/minimal/actors/sample_processor.py b/demos/minimal/actors/sample_processor.py index 2904f82e..f24fb12f 100644 --- a/demos/minimal/actors/sample_processor.py +++ b/demos/minimal/actors/sample_processor.py @@ -26,7 +26,7 @@ def setup(self): """ self.name = "Processor" self.frame = None - self.frame_num = 0 + self.frame_num = None logger.info("Completed setup for Processor")