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
|
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.
|
types. All data is normalized to standard SI units (volts and seconds) for consistency.
|
||||||
|
|
||||||
|
Scopes supported:
|
||||||
|
```
|
||||||
|
Gwinstek GEO
|
||||||
|
Owon HDS
|
||||||
|
NI LVM
|
||||||
|
```
|
||||||
|
|
||||||
## Instalation
|
## Instalation
|
||||||
```bash
|
```bash
|
||||||
# Install directly from my server
|
# Install directly from my server
|
||||||
|
|
@ -45,7 +52,7 @@ pipenv install -e .
|
||||||
from scope_parser import parse_owon_data, parse_gwinstek_data
|
from scope_parser import parse_owon_data, parse_gwinstek_data
|
||||||
|
|
||||||
# Parse your oscilloscope data (auto-detects file format)
|
# 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
|
# Access channel data
|
||||||
channel = data['CH1']
|
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
|
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
|
from .data import ScopeData, ChannelData
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__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:
|
def num_channels(self) -> int:
|
||||||
"""Get number of channels."""
|
"""Get number of channels."""
|
||||||
return len(self.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 .owon_parser import OwonParser
|
||||||
from .gwinstek_parser import GwinstekParser
|
from .gwinstek_parser import GwinstekParser
|
||||||
|
from .ni_parser import NIParser
|
||||||
from .data import ScopeData
|
from .data import ScopeData
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -17,3 +18,9 @@ def parse_gwinstek_data(file_path: str) -> ScopeData:
|
||||||
"""Parse Gwinstek oscilloscope CSV file."""
|
"""Parse Gwinstek oscilloscope CSV file."""
|
||||||
parser = GwinstekParser()
|
parser = GwinstekParser()
|
||||||
return parser.parse(file_path)
|
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