Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of stimulator encoder agent / driver. #612

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

dixilo
Copy link
Contributor

@dixilo dixilo commented Jan 9, 2024

Description

This pull request adds reader for stimulator encoder.
This agent run in Kria KR260 inside the stimulator electronics box.
This reader accesses a custom-made IP inside the PL of ZynqMP device.
The firmware code can be found in https://github.com/dixilo/kr260_stm.

Motivation and Context

These codes will be used to read encoder of the stimulator to know exact rotation speed and timing of the signal chopping.

How Has This Been Tested?

We tested the functionality using the stimulator in Princeton.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.

@dixilo dixilo changed the title Add stimulator encoder agent / driver. Implementation of stimulator encoder agent / driver. Jan 9, 2024
@dixilo dixilo self-assigned this Jun 5, 2024
@dixilo dixilo requested a review from BrianJKoopman June 5, 2024 10:33
@dixilo dixilo marked this pull request as ready for review June 5, 2024 10:38
@dixilo dixilo removed their assignment Jun 12, 2024
@BrianJKoopman BrianJKoopman added the new agent New OCS agent needs to be created label Oct 3, 2024
@davidvng davidvng self-requested a review January 28, 2025 19:54
Copy link
Contributor

@davidvng davidvng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Looks good overall, I added some comments for consistency similar to my previous PRs.

en_st_list.append(_d.state)

if len(ts_list) != 0:
data['timestamps'] = ts_list
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go into data['data']['timestamps'].

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense for the session.data object, but since data is being used in the self.agent.publish_to_feed() call 'timestamps' needs to be at this higher level. See https://ocs.readthedocs.io/en/main/developer/feeds.html#recorded-feed-message-format.

while self.take_data:
# Data acquisition
current_time = time.time()
data = {'timestamps': [], 'block_name': 'stm_enc', 'data': {}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to standard timestamp key in the data dictionary, then move the list of timestamps into data (see below).

Suggested change
data = {'timestamps': [], 'block_name': 'stm_enc', 'data': {}}
current_time = time.time()
data = {'timestamp': current_time, 'block_name': 'stm_enc', 'data': {}}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to above, plural 'timestamps' is important for the publish to feed call, and when publishing a list of points, needs to also contain a list for the associated value.

Copy link
Member

@BrianJKoopman BrianJKoopman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of small additional comments.

@mhasself
Copy link
Member

Great to see this an apologies if these two concerns are premature:

  • What's the data rate? If very fast, then you need separate feeds for aggregator / influx.
  • I don't think it's acceptable to have the UTC - TAI offset hard-coded to 37. Either explicitly store TAI, or have a way of getting utc-tai dynamically (including regular updates), or have an assertion, on start-up, that the current time is before Jan 1 2026.

@dixilo
Copy link
Contributor Author

dixilo commented Feb 5, 2025

Thanks for the review!

  • What's the data rate? If very fast, then you need separate feeds for aggregator / influx.

Data rate is around 250 Hz when rotated at the maximum speed.
The encoder will send data only during calibration time.
For the current calibration plan, data rate ~30 Hz to ~250 Hz continues for about 2 minutes during time constant calibration.

Do we need separate feeds for aggregator / influx for this situation?

@dixilo
Copy link
Contributor Author

dixilo commented Feb 5, 2025

I don't think it's acceptable to have the UTC - TAI offset hard-coded to 37. Either explicitly store TAI, or have a way of getting utc-tai dynamically (including regular updates), or have an assertion, on start-up, that the current time is before Jan 1 2026.

Thanks for the comment. I'm planning to implement the routine to get utc-tai dynamically.

@BrianJKoopman
Copy link
Member

Thanks for the review!

  • What's the data rate? If very fast, then you need separate feeds for aggregator / influx.

Data rate is around 250 Hz when rotated at the maximum speed. The encoder will send data only during calibration time. For the current calibration plan, data rate ~30 Hz to ~250 Hz continues for about 2 minutes during time constant calibration.

Do we need separate feeds for aggregator / influx for this situation?

Yes, InfluxDB doesn't handle high data rates well. You can publish full data rates to a feed with 'exclude_influx': True in the agg_params dict. Then you will need to have a separate feed to view in InfluxDB downsampled to at most 10 Hz.

The Labjack agent has a decent example of setting up the split feeds:

# Register main feed. Exclude influx due to potentially high scan rate
agg_params = {
'frame_length': 60,
'exclude_influx': True
}
self.agent.register_feed('sensors',
record=True,
agg_params=agg_params,
buffer_time=1)
# Register downsampled feed for influx.
agg_params_downsampled = {
'frame_length': 60
}
self.agent.register_feed('sensors_downsampled',
record=True,
agg_params=agg_params_downsampled,
buffer_time=1)
self.agent.register_feed('registers',
record=True,
agg_params={'frame_length': 10 * 60},
buffer_time=1.)

@dixilo
Copy link
Contributor Author

dixilo commented Feb 6, 2025

I don't think it's acceptable to have the UTC - TAI offset hard-coded to 37. Either explicitly store TAI, or have a way of getting utc-tai dynamically (including regular updates), or have an assertion, on start-up, that the current time is before Jan 1 2026.

I decided not to include UTC - TAI conversion and only to provide raw TAI values since it is difficult to implement the behavior at the leap second insertion.

Copy link
Member

@BrianJKoopman BrianJKoopman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes mostly look good to me, except for the format for the message published to the high rate feed. See the comment below.

Comment on lines 86 to 117
data = {'timestamp': current_time, 'block_name': 'stim_enc', 'data': {}}

ts_list = []
en_st_list = []

while not self._dev.fifo.empty():
_d = self._dev.fifo.get()
ts_list.append(_d.time.tai)
en_st_list.append(_d.state)

if len(ts_list) != 0:
data['data']['timestamps_tai'] = ts_list
data['data']['state'] = en_st_list

field_dict = {'timestamp_tai': ts_list[-1],
'state': en_st_list[-1]}

session.data.update(field_dict)

self.agent.publish_to_feed('stim_enc', data)
session.data.update({'timestamp': current_time})

if current_time - downsample_time > 0.1:
data_downsampled = {'timestamp': current_time,
'block_name': 'stim_enc_downsampled',
'data': {
'timestamps_tai': ts_list[-1],
'state': en_st_list[-1]
}}
self.agent.publish_to_feed('stim_enc_downsampled',
data_downsampled)
downsample_time = current_time
Copy link
Member

@BrianJKoopman BrianJKoopman Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I think there's some confusion about the format for publishing to data feeds and for session.data. (Edit: Docs for this format are here.)

For each publish_to_feed call, the format for the message argument needs to be either:

message = {
    'block_name': <Key to identify group of co-sampled data>
    'timestamp': <ctime of data>
    'data': {
        'field_name_1': <datapoint1>,
        'field_name_2': <datapoint2>
    }
}

if your fields are singular data points, or:

message = {
    'block_name': <Key to identify group of co-sampled data>
    'timestamps': [ctime1, ctime2, ... ]
    'data': {
        'field_name_1': [data1_1, data1_2, ...],
        'field_name_2': [data2_1, data2_2, ...]
    }
}

if you're publishing a list of points for each field. So 'timestamp' becomes 'timestamps' and is a list.

For the high rate data, as written currently, it looks like the format is:

message = {
    'block_name': 'stim_enc',
    'timestamp': current_time
    'data': {
        'timestamps_tai': [data1_1, data1_2, ...],
        'state': [data2_1, data2_2, ...]
    }
}

This might cause an issue in the HK aggregator. @mhasself any thoughts on the timestamps published being in TAI here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting the timestamps into a data field called timestamps_tai is a good solution.

As @BrianJKoopman says, you need to also record a full vector of timestamps, in message['timestamps']. The most appropriate thing would be to grab the system time using time.time() in the while not self._dev.fifo.empty() loop. (Those timestamps are not high precision but they're still useful and relevant... e.g. you can use them to make a very good guess of the [UTC - TAI] offset in processing, later.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have two more general comments on the data feeds here.

In the "slow" feed, for grafana, it will not be that useful to have a heavily downsampled version of the raw data. I would be much more interested for a regular published value of the spin frequency, computed from the last ~2 seconds of data. Instead of 10 Hz, just do 0.5 Hz and do a pulse count / time delta.

Furthermore -- for that slow feed, it's useful to publish the frequency even when the thing isn't spinning. That is helpful to distinguish between "there were no encoder pulses" and "the agent/process wasn't running".

If the slow feed has spin frequency, and is always running ... then I want it in the Aggregator / permanent HK data too!

So ya -- I think you should just record the fast data to aggregator, and then record a slower feed with spin frequency, including when freq=0, for both influx + aggregator.

(In the slow feed, instead of timestamps_tai you could record the most recent value of (timestamp_tai - timestamp) -- that will hover around the current UTC offset (37 or something).)

Comment on lines 91 to 94
while not self._dev.fifo.empty():
_d = self._dev.fifo.get()
ts_list.append(_d.time.tai)
en_st_list.append(_d.state)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How big is the device fifo? Like, how often do you need to clear it out?

The current loop time of 100 Hz means there will be a lot of pythony stuff happening, at 100 Hz. If this is running on a small computer, it could use a lot more CPU than necessary. Is there a way you could empty the fifo less often -- perhaps at 1 Hz, and only call "publish_to_feed" once you have a whole 1 second of data? These efficiencies might not matter; but if you find the agent is doing a lot of work, it will help to increase the size of each data chunk that gets processed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing out. We don't need 100 Hz here at all.

FIFO depth inside the FPGA firmware is 512 and the loop here is taking it into account.
Since the fifo accessed from the agent is a Queue object on Python and the device has 4GB DDR4 memory, 1 Hz reading perfectly works.

I will simply reduce the loop time to 0.5 Hz so that we don't need to care about the different loop time between fast and slow feeds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
hardware: stimulator new agent New OCS agent needs to be created
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants