Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d40fc9e
make sure pypi only runs on a tag of main (release)
jmxpearson Jan 15, 2024
5c2cf8b
Merge tag 'main' into docs
jmxpearson Jan 15, 2024
6260012
Merge pull request #172 from project-improv/docs
draelos Jan 15, 2024
035f1ab
reverting to pypi on tag push
jmxpearson Jan 15, 2024
2cdcaa1
small docs and readme updates (#175)
jmxpearson Jan 25, 2024
5682eeb
adding improv logo (#176)
jmxpearson Feb 17, 2024
01e0874
fixing broken image links
jmxpearson Aug 13, 2024
79efa47
fixing test formatting
jmxpearson Aug 13, 2024
e267cc0
Docs (#189)
draelos Sep 6, 2024
c673319
Added working cosine/sine wave generator based on frame index. Add wo…
Dec 23, 2024
41a2d90
updated sine/cosine generation in sample_generator intead of jupyter lab
Jan 13, 2025
84b5c61
updated changes, removed dictionary and reformatted output, cleaned u…
Jan 17, 2025
1cfdf99
Added lorenz_data generation, processing, and visualization sample
Jan 20, 2025
f0f550c
updated lorenz data generation
Jan 21, 2025
02d41fe
Delete demos/minimal/actors/updating_graphic.ipynb
edwin-ma26 Jan 21, 2025
497b870
Delete demos/minimal/actors/lorenz.ipynb
edwin-ma26 Jan 21, 2025
593bc22
removed sine wave processing
Jan 23, 2025
980050f
Merge branch 'edwin-waveplot' of github.com:edwin-ma26/improv into ed…
Jan 23, 2025
a8dc737
updated sine/cosine
Jan 24, 2025
4ef50c1
update sine/cosine generator + processor
Jan 30, 2025
62ff41e
updated directories
Feb 7, 2025
243ef6b
reorganization
clewis7 Feb 10, 2025
3daec64
updated readme file
Feb 13, 2025
45f6634
Merge branch 'dev' into edwin-waveplot
clewis7 Feb 13, 2025
361cdc9
clean up minimal demo
clewis7 Feb 13, 2025
6efe127
clean up simple viz
clewis7 Feb 13, 2025
ef23ce5
add lorenz yaml
clewis7 Feb 14, 2025
5a7fefa
finalize simple viz demo
clewis7 Feb 14, 2025
4cbeeca
fix lorenz data generation, update README
clewis7 Feb 14, 2025
29cfec8
clean up lorenz demo
clewis7 Feb 14, 2025
9208d42
small change
clewis7 Feb 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
run: |
brew install redis
- 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
Expand Down Expand Up @@ -81,7 +81,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'

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/pypi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
branches:
- 'main'

jobs:
build:
Expand All @@ -18,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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-pypi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# improv

<img src="docs/improv_logo_horizontal.svg" style="width:40%"/>

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/)
Expand All @@ -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.duke.edu/sites/default/files/improvGif.gif)

Expand All @@ -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) 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 <a href="http://twitter.com/annedraelos" target="_blank">@annedraelos</a> or <a href="http://twitter.com/jmxpearson" target="_blank">@jmxpearson</a>.
91 changes: 91 additions & 0 deletions demos/fastplotlib/actors/lorenz_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from improv.actor import Actor
import numpy as np
import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

class Generator(Actor):
"""
Generates coordinates for the Lorenz system in real time.
Computes the next 50 Lorenz coordinates every half second.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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):
"""Generates an array that serves as an initial source of data.

Initial data is the starting point (x,y,z) for the lorenz attractor.
"""
logger.info("Beginning setup for Lorenz Generator")

# 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):
"""Trivial stop function."""
logger.info("Lorenz Generator stopping")
return 0

def runStep(self):
"""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.frame_num >= 150:
return

# Add the next 10 points
for _ in range(25):
# 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))

# create flattened array with xyz coordinates along with the current frame number
data = np.append(self.data.ravel(), 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
self.frame_num += 1
except Exception as e:
logger.error(f"--------------------------------Generator 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])
78 changes: 78 additions & 0 deletions demos/fastplotlib/actors/lorenz_processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from improv.actor import Actor
import time
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 initialize class variables.
"""
self.name = "Processor"
self.frame = None
self.frame_num = None

# 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):
"""
Stop function. Closes the ZMQ socket connection.
"""
logger.info("Processor stopping")
self.socket.close()
return 0

def runStep(self):
"""
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:
data_id = self.q_in.get(timeout=0.05)
except Exception:
logger.error(f"Could not get frame!")
pass

if data_id is not None:
try:
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 frame number
data = np.array(self.frame, dtype=np.float64)
self.frame_num = int(data[-1])


# Send the processed data through the ZMQ socket
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}")
93 changes: 93 additions & 0 deletions demos/fastplotlib/actors/visual_processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from improv.actor import Actor
import random
import logging
import zmq
import numpy as np
import time
from queue import Empty

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


class Processor(Actor):
"""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):
"""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.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")

def stop(self):
"""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, scales the data in the y-dimension by a random number between 1-10 inclusive and then
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!")

if data_id is not None:
try:
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(), self.frame_num)

# 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}")
Loading