Add ability to load .lvm from multisim
This commit is contained in:
parent
d4a0503403
commit
66be32fd88
|
|
@ -7,6 +7,13 @@ This package is just neat enough to combine those together.
|
|||
Supports pulling the metadata, time encoding and etc from each of the loadable
|
||||
types. All data is normalized to standard SI units (volts and seconds) for consistency.
|
||||
|
||||
Scopes supported:
|
||||
```
|
||||
Gwinstek GEO
|
||||
Owon HDS
|
||||
NI LVM
|
||||
```
|
||||
|
||||
## Instalation
|
||||
```bash
|
||||
# Install directly from my server
|
||||
|
|
@ -45,7 +52,7 @@ pipenv install -e .
|
|||
from scope_parser import parse_owon_data, parse_gwinstek_data
|
||||
|
||||
# Parse your oscilloscope data (auto-detects file format)
|
||||
data = parse_owon_data("your_file.CSV") # or parse_gwinstek_data, etc
|
||||
data = parse_owon_data("your_file.CSV") # or parse_gwinstek_data, parse_ni_data, etc
|
||||
|
||||
# Access channel data
|
||||
channel = data['CH1']
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,47 @@
|
|||
import matplotlib.pyplot as plt
|
||||
from scope_parser import parse_ni_data
|
||||
|
||||
data = parse_ni_data("Example NI Sinsoidal.lvm")
|
||||
|
||||
print(data)
|
||||
ch1 = data["CH1"]
|
||||
ch2 = data["CH2"]
|
||||
|
||||
print(ch1.metadata)
|
||||
print(ch2.metadata)
|
||||
|
||||
# Plot both waveforms
|
||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
|
||||
|
||||
# CH 1 plot
|
||||
ax1.plot(ch1.time_values * 1000, ch1.voltage_values, "b-", linewidth=0.8)
|
||||
ax1.set_title("LVM Channel 1")
|
||||
ax1.set_xlabel("Time (ms)")
|
||||
ax1.set_ylabel("Voltage (V)")
|
||||
ax1.grid(True, alpha=0.3)
|
||||
ax1.text(
|
||||
0.02,
|
||||
0.95,
|
||||
f"Average: {ch1.average:.3f} V",
|
||||
transform=ax1.transAxes,
|
||||
verticalalignment="top",
|
||||
bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.8),
|
||||
)
|
||||
|
||||
# CH 2 plot
|
||||
ax2.plot(ch2.time_values * 1000, ch2.voltage_values, "r-", linewidth=0.8)
|
||||
ax2.set_title("LVM Channel 2")
|
||||
ax2.set_xlabel("Time (ms)")
|
||||
ax2.set_ylabel("Voltage (V)")
|
||||
ax2.grid(True, alpha=0.3)
|
||||
ax2.text(
|
||||
0.02,
|
||||
0.95,
|
||||
f"Average: {ch2.average:.3f} V",
|
||||
transform=ax2.transAxes,
|
||||
verticalalignment="top",
|
||||
bbox=dict(boxstyle="round", facecolor="lightcoral", alpha=0.8),
|
||||
)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
|
@ -2,8 +2,14 @@
|
|||
Joe's Really Simple Scope Parser
|
||||
"""
|
||||
|
||||
from .parsers import parse_owon_data, parse_gwinstek_data
|
||||
from .parsers import parse_owon_data, parse_gwinstek_data, parse_ni_data
|
||||
from .data import ScopeData, ChannelData
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__all__ = ["parse_owon_data", "parse_gwinstek_data", "ScopeData", "ChannelData"]
|
||||
__all__ = [
|
||||
"parse_owon_data",
|
||||
"parse_gwinstek_data",
|
||||
"parse_ni_data",
|
||||
"ScopeData",
|
||||
"ChannelData",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -98,3 +98,24 @@ class ScopeData:
|
|||
def num_channels(self) -> int:
|
||||
"""Get number of channels."""
|
||||
return len(self.channels)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation showing channel information."""
|
||||
lines = [f"ScopeData({self.num_channels} channels)"]
|
||||
|
||||
for channel_name, channel_data in self.channels.items():
|
||||
lines.append(f" {channel_name}:")
|
||||
lines.append(f" Samples: {len(channel_data.voltage_values)}")
|
||||
lines.append(
|
||||
f" Time: {channel_data.time_values.min():.6f} to {channel_data.time_values.max():.6f} s"
|
||||
)
|
||||
lines.append(
|
||||
f" Voltage: {channel_data.voltage_values.min():.6f} to {channel_data.voltage_values.max():.6f} V"
|
||||
)
|
||||
if channel_data.frequency:
|
||||
lines.append(f" Frequency: {channel_data.frequency} Hz")
|
||||
if channel_data.vpp:
|
||||
lines.append(f" Peak-to-peak: {channel_data.vpp:.6f} V")
|
||||
lines.append(f" Average: {channel_data.average:.6f} V")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
"""
|
||||
Parser for National Instruments LVM files.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Dict, List
|
||||
import numpy as np
|
||||
|
||||
from .base_parser import BaseOscilloscopeParser
|
||||
from .data import ScopeData, ChannelData
|
||||
|
||||
|
||||
class NIParser(BaseOscilloscopeParser):
|
||||
"""Parser for National Instruments LVM files."""
|
||||
|
||||
date_time = None
|
||||
|
||||
def can_parse(self, file_path: str) -> bool:
|
||||
"""Check if the file is valid/readable."""
|
||||
|
||||
try:
|
||||
with open(file_path, "r") as f:
|
||||
first_lines = f.readlines()
|
||||
has_header = first_lines[0].strip().startswith("LabVIEW Measurement")
|
||||
has_end_of_header = any(
|
||||
"***End_of_Header***" in line for line in first_lines
|
||||
)
|
||||
|
||||
if not (has_header and has_end_of_header):
|
||||
return False
|
||||
|
||||
date_time_pattern = re.compile(r"Date\s*(.*)\s*Time\s*(.*)")
|
||||
for line in first_lines:
|
||||
match = date_time_pattern.search(line)
|
||||
if match:
|
||||
date = match.group(1).strip()
|
||||
time = match.group(2).strip()
|
||||
self.date_time = f"{date} {time}"
|
||||
break
|
||||
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def parse(self, file_path: str) -> ScopeData:
|
||||
"""Parse NI LVM file."""
|
||||
|
||||
with open(file_path, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
channels = {}
|
||||
global_metadata = {}
|
||||
|
||||
# Parse global metadata (before first ***End_of_Header***)
|
||||
for i, line in enumerate(lines):
|
||||
line = line.strip()
|
||||
if line == "***End_of_Header***":
|
||||
break
|
||||
if "\t" in line and not line.startswith("***"):
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 2:
|
||||
key = parts[0].strip()
|
||||
value = parts[1].strip()
|
||||
if value:
|
||||
global_metadata[key] = value
|
||||
|
||||
# Find all data blocks (after ***End_of_Header***)
|
||||
block_starts = []
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip() == "***End_of_Header***":
|
||||
# Look for the next data block (X_Value_1 line)
|
||||
for j in range(i + 1, len(lines)):
|
||||
if lines[j].strip().startswith("X_Value_1"):
|
||||
if j not in block_starts: # Avoid duplicates
|
||||
block_starts.append(j)
|
||||
break
|
||||
|
||||
# Parse each block
|
||||
for block_idx, start_line in enumerate(block_starts):
|
||||
block_metadata = {}
|
||||
time_data = []
|
||||
voltage_data = []
|
||||
|
||||
# Find the block metadata (look backwards from X_Value_1 line)
|
||||
metadata_start = start_line
|
||||
for i in range(start_line - 1, -1, -1):
|
||||
line = lines[i].strip()
|
||||
if line == "***End_of_Header***":
|
||||
metadata_start = i + 1
|
||||
break
|
||||
|
||||
# Parse block metadata
|
||||
for i in range(metadata_start, start_line):
|
||||
line = lines[i].strip()
|
||||
if (
|
||||
"\t" in line
|
||||
and not line.startswith("***")
|
||||
and not line.startswith("Page_")
|
||||
):
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 2:
|
||||
key = parts[0].strip()
|
||||
value = parts[1].strip()
|
||||
if value and value != "---": # Only add non-empty values
|
||||
block_metadata[key] = value
|
||||
|
||||
# Parse data
|
||||
data_start = start_line + 1 # Skip the X_Value_1 header line
|
||||
for line in lines[data_start:]:
|
||||
if not line.strip() or line.strip().startswith("***"):
|
||||
break
|
||||
try:
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 2:
|
||||
time_data.append(float(parts[0]))
|
||||
voltage_data.append(float(parts[1]))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if time_data and voltage_data:
|
||||
# Create unique channel name for each block
|
||||
channel_name = f"CH{block_idx + 1}"
|
||||
|
||||
# Add block metadata to channel metadata
|
||||
channel_metadata = {**global_metadata, **block_metadata}
|
||||
if self.date_time:
|
||||
channel_metadata["Date_Time"] = self.date_time
|
||||
|
||||
channel_data = ChannelData(
|
||||
channel_name=channel_name,
|
||||
voltage_values=np.array(voltage_data),
|
||||
time_values=np.array(time_data),
|
||||
metadata=channel_metadata,
|
||||
)
|
||||
|
||||
channels[channel_name] = channel_data
|
||||
|
||||
return ScopeData(channels=channels, metadata=global_metadata)
|
||||
|
|
@ -4,6 +4,7 @@ Main Parsers module
|
|||
|
||||
from .owon_parser import OwonParser
|
||||
from .gwinstek_parser import GwinstekParser
|
||||
from .ni_parser import NIParser
|
||||
from .data import ScopeData
|
||||
|
||||
|
||||
|
|
@ -17,3 +18,9 @@ def parse_gwinstek_data(file_path: str) -> ScopeData:
|
|||
"""Parse Gwinstek oscilloscope CSV file."""
|
||||
parser = GwinstekParser()
|
||||
return parser.parse(file_path)
|
||||
|
||||
|
||||
def parse_ni_data(file_path: str) -> ScopeData:
|
||||
"""Parse National Instruments LVM file."""
|
||||
parser = NIParser()
|
||||
return parser.parse(file_path)
|
||||
|
|
|
|||
Loading…
Reference in New Issue