8000 Upload spectrum data support by pro100watt · Pull Request #207 · vikinganalytics/mvg · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Upload spectrum data support #207

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

Merged
merged 10 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[pylint]
extension-pkg-whitelist=pydantic
disable =
R0904,
too-many-public-methods,
too-few-public-methods,
too-many-arguments,
logging-fstring-interpolation,
Expand Down
115 changes: 114 additions & 1 deletion mvg/mvg.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from mvg.exceptions import MVGConnectionError
from mvg.utils.response_processing import (
FrequencyRange,
SortOrder,
get_paginated_analysis_results,
get_paginated_items,
Expand Down Expand Up @@ -61,7 +62,7 @@ def __init__(self, endpoint: str, token: str):
self.endpoint = endpoint
self.token = token

self.mvg_version = self.parse_version("v0.14.6")
self.mvg_version = self.parse_version("v0.14.7")
self.tested_api_version = self.parse_version("v0.5.12")

# Get API version
Expand Down Expand Up @@ -281,6 +282,49 @@ def create_source(
source_info = {"source_id": sid, "meta": meta, "channels": channels}
self._request("post", "/sources/", do_not_raise, json=source_info)

def create_spectrum_source(
self,
sid: str,
channels: List[str],
meta: dict = None,
exist_ok: bool = False,
):
"""
Creates a spectrum source on the server side.

Parameters
----------
sid : str
Source ID

channels : List[str]
Channels of spectrum Data. For instance axial, vertical and horizontal.
Channel names must be unique.
Cannot be updated after creating source.

meta : dict
Meta information of source [optional].

exist_ok : bool
Set to true to prevent exceptions for 409 Conflict errors
caused by trying to create an existing source. Defaults to False.
"""

logger.info("endpoint %s", self.endpoint)
logger.info("creating spectrum source with source id=%s", sid)
logger.info("metadata: %s", meta)
logger.info("channels: %s", channels)

if meta is None:
meta = {}

do_not_raise = []
if exist_ok:
do_not_raise.append(requests.codes["conflict"]) # 409

source_info = {"source_id": sid, "meta": meta, "channels": channels}
self._request("post", "/sources/spectrum", do_not_raise, json=source_info)

def create_tabular_source(
self,
sid: str,
Expand Down Expand Up @@ -471,6 +515,75 @@ def create_measurement(
"post", f"/sources/{sid}/measurements", do_not_raise, json=meas_struct
)

def create_spectrum_measurement(
self,
sid: str,
freq_range: FrequencyRange,
timestamp: int,
data: Dict[str, List[float]],
meta: dict = None,
exist_ok: bool = False,
):
"""Stores a measurement on the server side.

Although it is up to the client side to handle the
scaling of data it is recommended that the values
represent the acceleration in g.
The timestamp shall represent the time when the measurement was
recorded.
Comment on lines +529 to +533
Copy link
Contributor

Choose a reason for hiding this comment

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

@tuix Could you review this description? This is a copy of the description for create_measurement. For spectrum measurement, do we need to provide this recommendation that the spectrum data could be in units of
acceleration?


Parameters
----------
sid: str
source Id.

freq_range: FrequencyRange
lowest and highest frequency available in the spectrum as a list [MIN, MAX].

timestamp: int
in milliseconds since EPOCH.

data: Dict[str, List[float]]
Data on the format {channel: values}.
This format can be generated by pandas.DataFrame.to_dict("list").

meta: dict
Meta information to attach to data [optional].

exist_ok: bool
Set to true to prevent exceptions for 409 Conflict errors
caused by trying to create an existing measurement. Defaults to False.
"""

logger.info("endpoint %s", self.endpoint)
logger.info("creating spectrum measurement for source id=%s", sid)
logger.info(" freq_range: %s", freq_range)
logger.info(" timestamp: %s", timestamp)
logger.info(" meta data: %s", meta)

if meta is None:
meta = {}

do_not_raise = []
if exist_ok:
do_not_raise.append(requests.codes["conflict"]) # 409

meas_struct = [
{
"timestamp": timestamp,
"freq_range": freq_range,
"data": data,
"meta": meta,
}
]

self._request(
"post",
f"/sources/{sid}/measurements/spectrum",
do_not_raise,
json=meas_struct,
)

def create_tabular_measurement(
self,
sid: str,
Expand Down
2 changes: 1 addition & 1 deletion mvg/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def modes_boxplot(data, feature, request_id, total_modes=None, axes=None):
modes = modes.astype("category")
data["Modes"] = data["labels"].copy()
data["Modes"] = data["Modes"].astype("category")
data["Modes"].cat.set_categories(modes.tolist(), inplace=True)
data["Modes"] = data["Modes"].cat.set_categories(modes.tolist())

# Plot and format figure
image = data.boxplot(column=feature, by="Modes", ax=axes)
Expand Down
4 changes: 4 additions & 0 deletions mvg/utils/response_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from enum import Enum
from typing import Dict, Callable

from pydantic import conlist

FrequencyRange = conlist(float, min_items=2, max_items=2)


class SortOrder(Enum):
ASC = "asc"
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ numpy
semver
matplotlib
tabulate
pydantic
1 change: 1 addition & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ pytest-cov
# for callback test server
uvicorn==0.16.0
fastapi
scipy
64 changes: 55 additions & 9 deletions tests/conftest.py
F438
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import argparse
import json
import os
import sys
from pathlib import Path

import pandas as pd
import pytest
import requests
import uuid
import json
import pandas as pd

from mvg import MVG
from mvg.exceptions import MVGAPIError

import argparse
import sys

from tests.helpers import (
generate_random_source_id,
generate_sources_patterns,
make_channel_names,
simulate_spectrum_data,
stub_multiaxial_data,
upload_measurements,
)
Expand Down Expand Up @@ -54,9 +55,9 @@ def pytest_addoption(parser):

# Pytest initial configuration
def pytest_configure():
pytest.SOURCE_ID_WAVEFORM = uuid.uuid1().hex
pytest.SOURCE_ID_WAVEFORM = generate_random_source_id()
pytest.REF_DB_PATH = Path.cwd() / "tests" / "test_data" / "mini_charlie"
pytest.SOURCE_ID_TABULAR = uuid.uuid1().hex
pytest.SOURCE_ID_TABULAR = generate_random_source_id()
pytest.VALID_TOKEN = os.environ["TEST_TOKEN"]


Expand Down Expand Up @@ -226,6 +227,51 @@ def fixture(session):
)


def make_spectrum_source_fixture(pattern, n_channels=1):
@pytest.fixture
def fixture(session: MVG):
sid = generate_random_source_id()
channels = make_channel_names(n_channels=n_channels)

source_info = {"sid": sid, "channels": channels, "meta": {}}
session.create_spectrum_source(**source_info)
timestamps = []
measurements = []
if pattern:
timestamps, measurements = simulate_spectrum_data(
pattern=pattern, channels=channels
)
for meas in measurements:
session.create_spectrum_measurement(
sid=sid,
freq_range=meas["freq_range"],
timestamp=meas["timestamp"],
data=meas["data"],
meta=meas["meta"],
)

info = {
"measurements": measurements,
"timestamps": timestamps,
"pattern": pattern,
"channels": channels,
"meta": source_info["meta"],
}
try:
yield sid, info
finally:
session.delete_source(sid)

return fixture


spectrum_source_with_zero_measurements = make_spectrum_source_fixture(pattern=[])

spectrum_source_with_measurements = make_spectrum_source_fixture(
pattern=7 * [0] + 15 * [1]
)


@pytest.fixture()
def tabular_source(session):
source_id = pytest.SOURCE_ID_TABULAR
Expand Down
66 changes: 65 additions & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
from typing import List
import uuid
from itertools import cycle

import numpy as np
from scipy import fft

from mvg.mvg import MVG


def generate_random_source_id():
return uuid.uuid1().hex


def make_channel_names(n_channels):
return [f"acc_{i}" for i in range(n_channels)]


def upload_measurements(session: MVG, sid: str, data: list):
"""Upload measurements for a source"""
timestamps = list(data.keys())
Expand All @@ -14,6 +26,29 @@ def upload_measurements(session: MVG, sid: str, data: list):
)


def FFT(data, sample_freq) -> list():
"""Calculates FFT of the given time-series data.

Parameters
----------
data : array
data to be transformed.
sample_freq : float
sampling frequency.

Returns
-------
tuple : the frequency range and FFT
"""
N = len(data) # Number of samples
T = 1 / sample_freq # Sample spacing
xf = np.linspace(0.0, 1.0 / (2.0 * T), int(N / 2)) # Creates frequency vector
yf = fft.rfft(data) # Performs FFT
# FFTpeak is currently calculated, to calculate FFTrms reaplce 2.0 by np.sqrt(2)
abs_yf = (2.0 / N) * np.absolute(yf[: N // 2]) # Calculates norm
return xf, abs_yf


def stub_multiaxial_data(samp_freq=3000, duration=3.0, pattern={}):
"""Create measurements based on cosine signals (with added noise) for a multiaxial source.

Expand All @@ -37,7 +72,7 @@ def stub_multiaxial_data(samp_freq=3000, duration=3.0, pattern={}):

Examples
--------
>>> pattern = {'acc_x': [0] * 1 + [1] * 1, 'acc_x': [0] * 2, 'acc_z': [1] * 2}
>>> pattern = {'acc_x': [0] * 1 + [1] * 1, 'acc_y': [0] * 2, 'acc_z': [1] * 2}
>>> timestamps, data, duration = stub_multiaxial_data()
>>> print(data)
{0: {"data": {"acc_x": [...], "acc_y": [...], "acc_z": [...]}, "meta": {}}, 3600000: {"data": {"acc_x": [...], "acc_y": [...], "acc_z": [...]}, "meta": {}}}
Expand Down Expand Up @@ -86,6 +121,35 @@ def stub_multiaxial_data(samp_freq=3000, duration=3.0, pattern={}):
return timestamps, data, duration


def simulate_spectrum_data(pattern, channels, sample_freq=3000):
channels_pattern = {channel: pattern for channel in channels}

timestamps, data, _ = stub_multiaxial_data(
samp_freq=sample_freq, pattern=channels_pattern
)

measurements: List[dict] = []
for timestamp in timestamps:
waveform_data = data[timestamp]["data"]
spectrum_data = {}
for channel, channel_data in waveform_data.items():
# convert the waveform data in each channel to its
# respective spectrum
_, fft_y = FFT(channel_data, sample_freq)
spectrum_data[channel] = list(fft_y)

measurements.append(
{
"timestamp": timestamp,
"freq_range": [0, sample_freq],
"meta": {},
"data": spectrum_data,
}
)

return ti 57B2 mestamps, measurements


def generate_sources_patterns():
"""Generate a list of multiaxial sources with axis-based patterns"""
sources_patterns = [
Expand Down
Loading
0