"""ASAM MDF version 4 file format module"""
import bisect
from collections import defaultdict, deque
from collections.abc import Callable, Collection, Iterable, Iterator, Sequence
from copy import deepcopy
from datetime import datetime
from functools import lru_cache
from hashlib import md5
from io import StringIO
import logging
from math import ceil, floor, prod
from mimetypes import guess_type
import mmap
import os
from pathlib import Path
import re
import shutil
import sys
import tempfile
from tempfile import gettempdir
from traceback import format_exc
import typing
from typing import BinaryIO, Final, Literal, TYPE_CHECKING
from zipfile import ZIP_DEFLATED, ZipFile
import canmatrix
from canmatrix.canmatrix import CanMatrix
from lz4.frame import compress as lz_compress
import numpy as np
from numpy import (
arange,
argwhere,
array,
array_equal,
column_stack,
concatenate,
empty,
float32,
float64,
frombuffer,
full,
linspace,
nonzero,
searchsorted,
transpose,
uint8,
uint16,
uint32,
uint64,
unique,
where,
zeros,
)
from numpy.typing import DTypeLike, NDArray
from pandas import DataFrame, Series
from typing_extensions import (
Any,
Buffer,
overload,
SupportsBytes,
TypedDict,
TypeIs,
Unpack,
)
from .. import tool
from ..signal import InvalidationArray, Signal
from . import bus_logging_utils, mdf_common
from . import v4_constants as v4c
from .conversion_utils import conversion_transfer, from_dict
from .cutils import (
data_block_from_arrays,
extract,
get_channel_raw_bytes,
get_channel_raw_bytes_complete,
get_channel_raw_bytes_parallel,
get_invalidation_bits_array,
get_vlsd_max_sample_size,
sort_data_block,
)
from .mdf_common import LastCallInfo, MDF_Common, MdfCommonKwargs
from .options import GLOBAL_OPTIONS
from .source_utils import Source
from .types import (
BusType,
ChannelsType,
CompressionType,
DbcFileType,
RasterType,
StrPath,
)
from .utils import (
all_blocks_addresses,
as_non_byte_sized_signed_int,
CHANNEL_COUNT,
CONVERT,
count_channel_groups,
DataBlockInfo,
DECOMPRESS_FUNC_MAP,
extract_display_names,
extract_encryption_information,
extract_xml_comment,
FileLike,
fmt_to_datatype_v4,
Fragment,
get_fmt_v4,
get_text_v4,
handle_incomplete_block,
InvalidationBlockInfo,
is_file_like,
load_can_database,
MdfException,
NamedTemporaryFile,
SignalDataBlockInfo,
Terminated,
THREAD_COUNT,
UINT8_uf,
UINT16_uf,
UINT32_p,
UINT32_uf,
UINT64_uf,
UniqueDB,
validate_blocks,
validate_version_argument,
VirtualChannelGroup,
)
from .v4_blocks import (
AttachmentBlock,
Channel,
ChannelArrayBlock,
ChannelArrayBlockKwargs,
ChannelConversion,
ChannelGroup,
ChannelGroupKwargs,
ChannelKwargs,
DataBlock,
DataGroup,
DataList,
DataListKwargs,
DataZippedBlock,
DataZippedBlockKwargs,
EventBlock,
FileHistory,
FileIdentificationBlock,
HeaderBlock,
HeaderList,
HeaderListKwargs,
ListData,
ListDataKwargs,
SourceInformation,
TextBlock,
)
from .v4_constants import Version
try:
from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes
CRYPTOGRAPHY_AVAILABLE = True
except:
CRYPTOGRAPHY_AVAILABLE = False
if TYPE_CHECKING:
from ..mdf import MDF
try:
decode = np.strings.decode
encode = np.strings.encode
except:
decode = np.char.decode
encode = np.char.encode
MASTER_CHANNELS: Final = (v4c.CHANNEL_TYPE_MASTER, v4c.CHANNEL_TYPE_VIRTUAL_MASTER)
COMMON_SIZE: Final = v4c.COMMON_SIZE
COMMON_u = v4c.COMMON_u
COMMON_uf = v4c.COMMON_uf
COMMON_SHORT_SIZE: Final = v4c.COMMON_SHORT_SIZE
COMMON_SHORT_uf = v4c.COMMON_SHORT_uf
COMMON_SHORT_u = v4c.COMMON_SHORT_u
VALID_DATA_TYPES: Final = v4c.VALID_DATA_TYPES
EMPTY_TUPLE: Final = ()
# 100 extra steps for the sorting, 1 step after sorting and 1 step at finish
SORT_STEPS: Final = 102
logger = logging.getLogger("asammdf")
__all__ = ["MDF4"]
Group = mdf_common.GroupV4
class BusLoggingMap(TypedDict):
CAN: dict[int, dict[int, int]]
ETHERNET: dict[int, int]
FLEXRAY: dict[int, int]
LIN: dict[int, int]
class Kwargs(MdfCommonKwargs, total=False):
column_storage: bool
process_bus_logging: bool
[docs]
class MDF4(MDF_Common[Group]):
r"""The `header` attribute is a `HeaderBlock`.
The `groups` attribute is a list of `Group` objects, each one with the
following attributes:
* ``data_group`` - `DataGroup` object
* ``channel_group`` - `ChannelGroup` object
* ``channels`` - list of `Channel` objects with the same order as found in
the MDF file
* ``channel_dependencies`` - list of `ChannelArrayBlock` objects in case of
channel arrays; list of `Channel` objects in case of structure channel
composition
* ``data_blocks`` - list of `DataBlockInfo` objects, each one containing
address, type, size and other information about the block
* ``data_location`` - integer code for data location (original file,
temporary file or memory)
* ``data_block_addr`` - list of raw samples starting addresses
* ``data_block_type`` - list of codes for data block type
* ``data_block_size`` - list of raw samples block size
* ``sorted`` - sorted indicator flag
* ``record_size`` - dict that maps record IDs to record sizes in bytes
(including invalidation bytes)
* ``param`` - row size used for transposition, in case of transposed zipped
blocks
Parameters
----------
name : str | path-like | file-like, optional
MDF file name (if provided it must be a real file name) or file-like
object.
version : str, default '4.10'
MDF file version ('4.00', '4.10', '4.11', '4.20').
use_display_names : bool, default True
Parse the XML channel comment to search for the display name; XML
parsing is quite expensive so setting this to False can decrease the
loading times very much.
remove_source_from_channel_names : bool, default False
Remove source from channel names ("Speed\XCP3" -> "Speed").
compact_vlsd (False) : bool, optional
Use slower method to save the exact sample size for VLSD channels.
column_storage : bool, default True
Use column storage for MDF version >= 4.20.
password : bytes | str, optional
Use this password to decode encrypted attachments.
Attributes
----------
attachments : list
List of file attachments.
channels_db : dict
Used for fast channel access by name; for each name key the value is a
list of (group index, channel index) tuples.
events : list
List event blocks.
file_history : list
List of (FileHistory, TextBlock) pairs.
groups : list
List of data group dicts.
header : HeaderBlock
MDF file header.
identification : FileIdentificationBlock
MDF file start block.
last_call_info : dict | None
A dict to hold information about the last called method.
.. versionadded:: 5.12.0
masters_db : dict
Used for fast master channel access; for each group index key the value
is the master channel index.
name : pathlib.Path
MDF file name.
version : str
MDF version.
"""
def __init__(
self,
name: StrPath | FileLike | None = None,
version: Version = "4.10",
channels: list[str] | None = None,
**kwargs: Unpack[Kwargs],
) -> None:
if not kwargs.get("__internal__", False):
raise MdfException("Always use the MDF class; do not use the class MDF4 directly")
# bind cache to instance to avoid memory leaks
self.determine_max_vlsd_sample_size = lru_cache(maxsize=1024 * 1024)(self._determine_max_vlsd_sample_size)
self.extract_attachment = lru_cache(maxsize=128)(self._extract_attachment)
self._kwargs = kwargs
self.original_name = Path(kwargs["original_name"] or "")
self.file_history: list[FileHistory] = []
self.masters_db: dict[int, int] = {}
self.attachments: list[AttachmentBlock] = []
self._attachments_cache: dict[bytes | str, int] = {}
self.events: list[EventBlock] = []
self.bus_logging_map: BusLoggingMap = {"CAN": {}, "ETHERNET": {}, "FLEXRAY": {}, "LIN": {}}
self._attachments_map: dict[int, int] = {}
self._ch_map: dict[int, tuple[int, int]] = {}
self._master_channel_metadata: dict[int, tuple[str, int]] = {}
self._invalidation_cache: dict[tuple[int, int, int, int], InvalidationArray | None] = {}
self._external_dbc_cache: dict[bytes, CanMatrix] = {}
self._si_map: dict[bytes | int | Source, SourceInformation] = {}
self._cc_map: dict[bytes | int, ChannelConversion] = {}
self._cg_map: dict[int, int] = {}
self._cn_data_map: dict[int, tuple[int, int]] = {}
self._dbc_cache: dict[int, CanMatrix] = {}
self._closed = False
self.temporary_folder = kwargs.get("temporary_folder", GLOBAL_OPTIONS["temporary_folder"])
self._add_array_components = kwargs.get("add_array_components", GLOBAL_OPTIONS["add_array_components"])
if channels is None:
self.load_filter: set[str] = set()
self.use_load_filter = False
else:
self.load_filter = set(channels)
self.use_load_filter = True
self._tempfile = NamedTemporaryFile(dir=self.temporary_folder)
self._mapped_file: BinaryIO | None = None
self._file: FileLike | mmap.mmap | None = None
self._read_fragment_size = GLOBAL_OPTIONS["read_fragment_size"]
self._write_fragment_size = GLOBAL_OPTIONS["write_fragment_size"]
self._single_bit_uint_as_bool = GLOBAL_OPTIONS["single_bit_uint_as_bool"]
self._integer_interpolation = GLOBAL_OPTIONS["integer_interpolation"]
self._float_interpolation = GLOBAL_OPTIONS["float_interpolation"]
self._use_display_names = kwargs.get("use_display_names", GLOBAL_OPTIONS["use_display_names"])
self._raise_on_incomplete_blocks = GLOBAL_OPTIONS["raise_on_incomplete_blocks"]
self._fill_0_for_missing_computation_channels = kwargs.get(
"fill_0_for_missing_computation_channels",
GLOBAL_OPTIONS["fill_0_for_missing_computation_channels"],
)
self._ignore_invalidation_bits = kwargs.get(
"ignore_invalidation_bits",
GLOBAL_OPTIONS["ignore_invalidation_bits"],
)
self._remove_source_from_channel_names = kwargs.get("remove_source_from_channel_names", False)
self._password = kwargs.get("password", None)
self._force_attachment_encryption = kwargs.get("force_attachment_encryption", False)
self.compact_vlsd = kwargs.get("compact_vlsd", False)
self.virtual_groups: dict[int, VirtualChannelGroup] = {} # master group 2 referencing groups
self.virtual_groups_map: dict[int, int] = {} # group index 2 master group
self.vlsd_max_length: dict[tuple[int, str], int] = (
{}
) # hint about the maximum vlsd length for group_index, name pairs
self._master = None
self.last_call_info: LastCallInfo = {}
# make sure no appended block has the address 0
self._tempfile.write(b"\0")
self._delete_on_close = False
progress = kwargs.get("progress", None)
self._column_storage = False
self._use_ld_blocks = False
self._units_map = {}
self._mapped_file = None
super().__init__(kwargs.get("raise_on_multiple_occurrences", GLOBAL_OPTIONS["raise_on_multiple_occurrences"]))
if name:
if is_file_like(name):
self._file = name
self.name = self.original_name = Path("From_FileLike.mf4")
self._from_filelike = True
self._read(self._file, mapped=False, progress=progress)
else:
try:
with open(name, "rb") as stream:
identification = FileIdentificationBlock(stream=stream)
version_str = identification.version_str.decode("utf-8").strip(" \n\t\0")
flags = identification.unfinalized_standard_flags
if version_str >= "4.10" and flags:
tmpdir = Path(gettempdir())
if self.temporary_folder:
tmpdir = Path(self.temporary_folder)
self.name = tmpdir / f"{os.urandom(6).hex()}_{Path(name).name}"
shutil.copy(name, self.name)
self._mapped_file = open(self.name, "rb+")
self._file = mmap.mmap(
self._mapped_file.fileno(), 0, access=(mmap.ACCESS_READ | mmap.ACCESS_WRITE)
)
self._from_filelike = False
self._delete_on_close = True
self._read(self._file, mapped=False, progress=progress)
else:
if sys.maxsize < 2**32:
self.name = Path(name)
self._file = open(self.name, "rb")
self._from_filelike = False
self._read(self._file, mapped=False, progress=progress)
else:
self.name = Path(name)
self._mapped_file = open(self.name, "rb")
self._file = mmap.mmap(self._mapped_file.fileno(), 0, access=mmap.ACCESS_READ)
self._from_filelike = False
self._read(self._file, mapped=True, progress=progress)
except:
if self._file:
self._file.close()
if self._mapped_file:
self._mapped_file.close()
del self._file
self._mapped_file = None
raise
else:
self._from_filelike = False
version = validate_version_argument(version)
self.header = HeaderBlock()
self.identification = FileIdentificationBlock(version=version)
self.version: str = version
self.name = Path("__new__.mf4")
self._parent: MDF | None = None
def __del__(self) -> None:
self.close()
def _check_finalised(self) -> int:
flags = self.identification.unfinalized_standard_flags
if flags & 1:
message = f"Unfinalised file {self.name}: Update of cycle counters for CG/CA blocks required"
logger.info(message)
if flags & 1 << 1:
message = f"Unfinalised file {self.name}: Update of cycle counters for SR blocks required"
logger.info(message)
if flags & 1 << 2:
message = f"Unfinalised file {self.name}: Update of length for last DT block required"
logger.info(message)
if flags & 1 * 8:
message = f"Unfinalised file {self.name}: Update of length for last RD block required"
logger.info(message)
if flags & 1 << 4:
message = (
f"Unfinalised file {self.name}: Update of last DL block in each chained list of DL blocks required"
)
logger.info(message)
if flags & 1 << 5:
message = (
f"Unfinalised file {self.name}: Update of cg_data_bytes and cg_inval_bytes in VLSD CG block required"
)
logger.info(message)
if flags & 1 << 6:
message = (
f"Unfinalised file {self.name}:"
" Update of offset values for VLSD channel required"
" in case a VLSD CG block is used"
)
logger.info(message)
return flags
def _read(
self,
stream: FileLike | mmap.mmap,
mapped: bool = False,
progress: Callable[[int, int], None] | Any | None = None,
) -> None:
self._mapped = mapped
dg_cntr = 0
stream.seek(0, 2)
self.file_limit = stream.tell()
stream.seek(0)
cg_count, _ = count_channel_groups(stream)
progress_steps = cg_count + SORT_STEPS
if progress is not None:
if callable(progress):
progress(0, progress_steps)
current_cg_index = 0
self.identification = FileIdentificationBlock(stream=stream, mapped=mapped)
version = self.identification.version_str
self.version = version.decode("utf-8").strip(" \n\t\r\0")
if self.version >= "4.20":
self._column_storage = self._kwargs.get("column_storage", False)
if self._column_storage:
self._use_ld_blocks = True
else:
self._use_ld_blocks = self._kwargs.get("use_ld_blocks", False)
else:
self._column_storage = False
self._use_ld_blocks = False
if self.version >= "4.10":
# Check for finalization past version 4.10
finalisation_flags = self._check_finalised()
if finalisation_flags:
message = f"Attempting finalization of {self.name}"
logger.info(message)
self._finalize(stream)
self._mapped = mapped = False
self.header = HeaderBlock(address=0x40, stream=stream, mapped=mapped, file_limit=self.file_limit)
# read file history
fh_addr = self.header.file_history_addr
while fh_addr:
history_block = FileHistory(address=fh_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
self.file_history.append(history_block)
fh_addr = history_block.next_fh_addr
# read attachments
at_addr = self.header.first_attachment_addr
index = 0
while at_addr:
try:
at_block = AttachmentBlock(address=at_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
except MdfException:
break
self._attachments_map[at_addr] = index
self.attachments.append(at_block)
at_addr = at_block.next_at_addr
index += 1
# go to first date group and read each data group sequentially
dg_addr = self.header.first_dg_addr
while dg_addr:
new_groups = []
group = DataGroup(address=dg_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
record_id_nr = group.record_id_len
# go to first channel group of the current data group
cg_addr = first_cg_addr = group.first_cg_addr
cg_nr = 0
cg_size: dict[int, int] = {}
while cg_addr:
cg_nr += 1
if cg_addr == first_cg_addr:
grp = Group(group)
else:
grp = Group(group.copy())
# read each channel group sequentially
block = ChannelGroup(
address=cg_addr,
stream=stream,
mapped=mapped,
si_map=self._si_map,
file_limit=self.file_limit,
)
self._cg_map[cg_addr] = dg_cntr
channel_group = grp.channel_group = block
grp.record_size = cg_size
if channel_group.flags & v4c.FLAG_CG_VLSD:
# VLDS flag
record_id = channel_group.record_id
cg_size[record_id] = 0
elif channel_group.flags & v4c.FLAG_CG_BUS_EVENT:
samples_size = channel_group.samples_byte_nr
inval_size = channel_group.invalidation_bytes_nr
record_id = channel_group.record_id
cg_size[record_id] = samples_size + inval_size
else:
# in case no `cg_flags` are set
samples_size = channel_group.samples_byte_nr
inval_size = channel_group.invalidation_bytes_nr
record_id = channel_group.record_id
cg_size[record_id] = samples_size + inval_size
if record_id_nr:
grp.sorted = False
else:
grp.sorted = True
# go to first channel of the current channel group
ch_addr = channel_group.first_ch_addr
ch_cntr = 0
# Read channels by walking recursively in the channel group
# starting from the first channel
self._read_channels(ch_addr, grp, stream, dg_cntr, ch_cntr, mapped=mapped)
cg_addr = channel_group.next_cg_addr
dg_cntr += 1
current_cg_index += 1
if progress is not None:
if callable(progress):
progress(current_cg_index, progress_steps)
new_groups.append(grp)
# store channel groups record sizes dict in each
# new group data belong to the initial unsorted group, and add
# the key 'sorted' with the value False to use a flag;
address = group.data_block_addr
total_size = 0
inval_total_size = 0
record_size = 0
for new_group in new_groups:
channel_group = new_group.channel_group
if channel_group.flags & v4c.FLAG_CG_REMOTE_MASTER:
total_size += channel_group.samples_byte_nr * channel_group.cycles_nr
inval_total_size += channel_group.invalidation_bytes_nr * channel_group.cycles_nr
record_size = channel_group.samples_byte_nr
else:
total_size += (
channel_group.samples_byte_nr + channel_group.invalidation_bytes_nr + record_id_nr
) * channel_group.cycles_nr
record_size = channel_group.samples_byte_nr + channel_group.invalidation_bytes_nr
if self.identification.unfinalized_standard_flags & v4c.FLAG_UNFIN_UPDATE_CG_COUNTER:
total_size = 10**12
inval_total_size = 10**12
data_blocks_info = self._get_data_blocks_info(
address=address,
stream=stream,
mapped=mapped,
total_size=total_size,
inval_total_size=inval_total_size,
record_size=record_size,
)
data_blocks = list(data_blocks_info) # load the info blocks directly here
uses_ld = self._uses_ld(
address=address,
stream=stream,
mapped=mapped,
)
for grp in new_groups:
grp.data_location = v4c.LOCATION_ORIGINAL_FILE
grp.data_blocks_info_generator = data_blocks_info
grp.data_blocks = data_blocks
grp.uses_ld = uses_ld
self._prepare_record(grp)
self.groups.extend(new_groups)
dg_addr = group.next_dg_addr
# all channels have been loaded so now we can link the
# channel dependencies and load the signal data for VLSD channels
for gp_index, grp in enumerate(self.groups):
if self.version >= "4.20" and grp.channel_group.flags & v4c.FLAG_CG_REMOTE_MASTER:
grp.channel_group.cg_master_index = self._cg_map[grp.channel_group.cg_master_addr]
index = grp.channel_group.cg_master_index
else:
index = gp_index
self.virtual_groups_map[gp_index] = index
if index not in self.virtual_groups:
self.virtual_groups[index] = VirtualChannelGroup()
virtual_channel_group = self.virtual_groups[index]
virtual_channel_group.groups.append(gp_index)
virtual_channel_group.record_size += (
grp.channel_group.samples_byte_nr + grp.channel_group.invalidation_bytes_nr
)
virtual_channel_group.cycles_nr = grp.channel_group.cycles_nr
for ch_index, dep_list in enumerate(grp.channel_dependencies):
if not dep_list:
continue
for dep in dep_list:
if isinstance(dep, ChannelArrayBlock):
if dep.flags & v4c.FLAG_CA_DYNAMIC_AXIS:
for i in range(dep.dims):
ch_addr = typing.cast(int, dep[f"dynamic_size_{i}_ch_addr"])
if ch_addr:
ref_channel = self._ch_map[ch_addr]
dep.dynamic_size_channels.append(ref_channel)
else:
dep.dynamic_size_channels.append(None)
if dep.flags & v4c.FLAG_CA_INPUT_QUANTITY:
for i in range(dep.dims):
ch_addr = typing.cast(int, dep[f"input_quantity_{i}_ch_addr"])
if ch_addr:
ref_channel = self._ch_map[ch_addr]
dep.input_quantity_channels.append(ref_channel)
else:
dep.input_quantity_channels.append(None)
if dep.flags & v4c.FLAG_CA_OUTPUT_QUANTITY:
ch_addr = dep.output_quantity_ch_addr
if ch_addr:
ref_channel = self._ch_map[ch_addr]
dep.output_quantity_channel = ref_channel
else:
dep.output_quantity_channel = None
if dep.flags & v4c.FLAG_CA_COMPARISON_QUANTITY:
ch_addr = dep.comparison_quantity_ch_addr
if ch_addr:
ref_channel = self._ch_map[ch_addr]
dep.comparison_quantity_channel = ref_channel
else:
dep.comparison_quantity_channel = None
if (dep.flags & v4c.FLAG_CA_AXIS) and not (dep.flags & v4c.FLAG_CA_FIXED_AXIS):
for i in range(dep.dims):
ch_addr = typing.cast(int, dep[f"scale_axis_{i}_ch_addr"])
if ch_addr:
ref_channel = self._ch_map[ch_addr]
dep.axis_channels.append(ref_channel)
else:
dep.axis_channels.append(None)
else:
break
self._sort(
stream,
current_progress_index=current_cg_index,
max_progress_count=progress_steps,
progress=progress,
)
if progress is not None:
if callable(progress):
progress(progress_steps - 1, progress_steps) # second to last step now
for grp in self.groups:
channels = grp.channels
if len(channels) == 1 and channels[0].dtype_fmt.itemsize == grp.channel_group.samples_byte_nr:
grp.single_channel_dtype = channels[0].dtype_fmt
if self._kwargs.get("process_bus_logging", True):
self._process_bus_logging()
# read events
addr = self.header.first_event_addr
ev_map: dict[int, int] = {}
event_index = 0
while addr:
event = EventBlock(address=addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
event.update_references(self._ch_map, self._cg_map)
self.events.append(event)
ev_map[addr] = event_index
event_index += 1
addr = event.next_ev_addr
for event in self.events:
addr = event.parent_ev_addr
if addr:
parent = ev_map.get(addr, None)
if parent is not None:
event.parent = parent
else:
event.parent = None
addr = event.range_start_ev_addr
if addr:
range_start_ev_addr = ev_map.get(addr, None)
if range_start_ev_addr is not None:
event.parent = range_start_ev_addr
else:
event.parent = None
self._si_map.clear()
self._ch_map.clear()
self._cc_map.clear()
self._units_map.clear()
self._attachments_map.clear()
self._master = None
if progress is not None:
if callable(progress):
progress(progress_steps, progress_steps) # last step, we've completely loaded the file for sure
self.progress = cg_count, cg_count
@overload
def _read_channels(
self,
ch_addr: int,
grp: Group,
stream: FileLike | mmap.mmap,
dg_cntr: int,
ch_cntr: int,
parent_channel: Channel,
mapped: bool = ...,
) -> tuple[int, list[ChannelArrayBlock] | list[tuple[int, int]], np.dtype[Any]]: ...
@overload
def _read_channels(
self,
ch_addr: int,
grp: Group,
stream: FileLike | mmap.mmap,
dg_cntr: int,
ch_cntr: int,
parent_channel: None = ...,
mapped: bool = ...,
) -> tuple[int, None, None]: ...
def _read_channels(
self,
ch_addr: int,
grp: Group,
stream: FileLike | mmap.mmap,
dg_cntr: int,
ch_cntr: int,
parent_channel: Channel | None = None,
mapped: bool = False,
) -> tuple[int, list[ChannelArrayBlock] | list[tuple[int, int]] | None, np.dtype[Any] | None]:
filter_channels = self.use_load_filter
use_display_names = self._use_display_names
channels = grp.channels
dependencies = grp.channel_dependencies
unique_names = UniqueDB()
if parent_channel:
composition: list[tuple[int, int]] | None = []
composition_channels: list[Channel] | None = []
else:
composition = composition_channels = None
if grp.channel_group.path_separator:
path_separator = chr(grp.channel_group.path_separator)
else:
path_separator = "."
while ch_addr:
# read channel block and create channel object
if (ch_addr + v4c.COMMON_SIZE) > self.file_limit:
logger.warning(f"Channel address {ch_addr:X} is outside the file size {self.file_limit}")
break
if filter_channels:
if mapped:
(
id_,
links_nr,
next_ch_addr,
component_addr,
name_addr,
comment_addr,
) = v4c.CHANNEL_FILTER_uf(stream, ch_addr)
channel_type = stream[ch_addr + v4c.COMMON_SIZE + links_nr * 8]
name = get_text_v4(name_addr, stream, mapped=mapped, file_limit=self.file_limit)
if use_display_names:
comment = get_text_v4(
comment_addr,
stream,
mapped=mapped,
file_limit=self.file_limit,
)
display_names = extract_display_names(comment)
else:
display_names = {}
comment = None
else:
stream.seek(ch_addr)
(
id_,
links_nr,
next_ch_addr,
component_addr,
name_addr,
comment_addr,
) = v4c.CHANNEL_FILTER_u(stream.read(v4c.CHANNEL_FILTER_SIZE))
stream.seek(ch_addr + v4c.COMMON_SIZE + links_nr * 8)
channel_type = stream.read(1)[0]
name = get_text_v4(name_addr, stream, mapped=mapped, file_limit=self.file_limit)
if use_display_names:
comment = get_text_v4(
comment_addr,
stream,
mapped=mapped,
file_limit=self.file_limit,
)
display_names = extract_display_names(comment)
else:
display_names = {}
comment = None
if id_ != b"##CN":
message = f'Expected "##CN" block @{hex(ch_addr)} but found "{id_!r}"'
raise MdfException(message)
if self._remove_source_from_channel_names:
name = name.split(path_separator, 1)[0]
display_names = {_name.split(path_separator, 1)[0]: val for _name, val in display_names.items()}
if (
parent_channel
or channel_type in v4c.MASTER_TYPES
or name in self.load_filter
or (use_display_names and any(dsp_name in self.load_filter for dsp_name in display_names))
):
if comment is None:
comment = get_text_v4(
comment_addr,
stream,
mapped=mapped,
file_limit=self.file_limit,
)
channel = Channel(
address=ch_addr,
stream=stream,
cc_map=self._cc_map,
si_map=self._si_map,
at_map=self._attachments_map,
use_display_names=use_display_names,
mapped=mapped,
parsed_strings=(name, display_names, comment),
file_limit=self.file_limit,
)
si_path = getattr(channel.source, "path", "")
if si_path and not self._remove_source_from_channel_names:
path_name = f"{si_path}.{channel.name}"
if path_name not in channel.display_names:
channel.display_names[path_name] = "source_path"
elif not component_addr:
ch_addr = next_ch_addr
continue
else:
if (component_addr + v4c.CC_ALG_BLOCK_SIZE) > self.file_limit:
logger.warning(
f"Channel component address {component_addr:X} is outside the file size {self.file_limit}"
)
break
# check if it is a CABLOCK or CNBLOCK
stream.seek(component_addr)
blk_id = stream.read(4)
if blk_id == b"##CN":
(
ch_cntr,
_1,
_2,
) = self._read_channels(
component_addr,
grp,
stream,
dg_cntr,
ch_cntr,
None,
mapped=mapped,
)
ch_addr = next_ch_addr
continue
else:
channel = Channel(
address=ch_addr,
stream=stream,
cc_map=self._cc_map,
si_map=self._si_map,
at_map=self._attachments_map,
use_display_names=use_display_names,
mapped=mapped,
parsed_strings=None,
file_limit=self.file_limit,
units_map=self._units_map,
)
if channel.data_type not in VALID_DATA_TYPES:
ch_addr = channel.next_ch_addr
continue
if channel.channel_type == v4c.CHANNEL_TYPE_SYNC:
channel.attachment = self._attachments_map.get(
channel.data_block_addr,
None,
)
if channel.source and channel.source.bus_type in v4c.BUS_LOGGING_TYPES and channel.source.path:
path_name = f"{channel.source.path}.{channel.name}"
if path_name not in channel.display_names:
channel.display_names[path_name] = "source_path"
if self._remove_source_from_channel_names:
channel.name = channel.name.split(path_separator, 1)[0]
channel.display_names = {
_name.split(path_separator, 1)[0]: val for _name, val in channel.display_names.items()
}
entry = (dg_cntr, ch_cntr)
self._ch_map[ch_addr] = entry
channels.append(channel)
if composition is not None and composition_channels is not None:
composition.append(entry)
composition_channels.append(channel)
for _name in channel.display_names:
self.channels_db.add(_name, entry)
self.channels_db.add(channel.name, entry)
# signal data
cn_data_addr = channel.data_block_addr
if cn_data_addr:
grp.signal_data.append(([], self._get_signal_data_blocks_info(cn_data_addr, stream)))
else:
grp.signal_data.append(None)
if cn_data_addr:
self._cn_data_map[cn_data_addr] = entry
if channel.channel_type in MASTER_CHANNELS:
self.masters_db[dg_cntr] = ch_cntr
ch_cntr += 1
component_addr = channel.component_addr
if component_addr:
if (component_addr + 4) > self.file_limit:
logger.warning(
f"Channel component address {component_addr:X} is outside the file size {self.file_limit}"
)
break
index = ch_cntr - 1
dependencies.append(None)
grp.signal_data.append(None)
# check if it is a CABLOCK or CNBLOCK
stream.seek(component_addr)
blk_id = stream.read(4)
if blk_id == b"##CN":
(
ch_cntr,
ret_composition,
ret_composition_dtype,
) = self._read_channels(
component_addr,
grp,
stream,
dg_cntr,
ch_cntr,
channel,
mapped=mapped,
)
dependencies[index] = ret_composition
channel.dtype_fmt = ret_composition_dtype
else:
# only channel arrays with storage=CN_TEMPLATE are
# supported so far
channel.dtype_fmt = np.dtype(
get_fmt_v4(
channel.data_type,
channel.bit_offset + channel.bit_count,
channel.channel_type,
)
)
ca_dependencies = []
byte_offset_factors: list[int] = []
bit_pos_inval_factors: list[int] = []
dimensions: list[int] = []
total_elem = 1
if channel.data_type == v4c.DATA_TYPE_BYTEARRAY:
# read CA-CN nested structure
ca_block = ChannelArrayBlock(
address=component_addr,
stream=stream,
mapped=mapped,
cc_map=self._cc_map,
file_limit=self.file_limit,
)
if ca_block.storage != v4c.CA_STORAGE_TYPE_CN_TEMPLATE:
logger.warning("Only CN template arrays are supported")
break
(
ch_cntr,
ret_composition,
ret_composition_dtype,
) = self._read_channels(
ca_block.composition_addr,
grp,
stream,
dg_cntr,
ch_cntr,
channel,
mapped=mapped,
)
ret_composition = typing.cast(list[ChannelArrayBlock], ret_composition)
channel.dtype_fmt = ret_composition_dtype
if ret_composition:
ca_dependencies.extend(ret_composition)
else:
while component_addr:
stream.seek(component_addr)
blk_id = stream.read(4)
if blk_id != b"##CA":
logger.warning(f"expected b'##CA' header but found {blk_id}")
break
ca_block = ChannelArrayBlock(
address=component_addr,
stream=stream,
mapped=mapped,
cc_map=self._cc_map,
file_limit=self.file_limit,
)
if ca_block.storage != v4c.CA_STORAGE_TYPE_CN_TEMPLATE:
logger.warning("Only CN template arrays are supported")
break
ca_dependencies.append(ca_block)
component_addr = ca_block.composition_addr
dependencies[index] = ca_dependencies or None
if self._add_array_components:
for ca_blck in ca_dependencies:
# 1D array with dimensions
for i in range(ca_blck.dims):
dim_size = typing.cast(int, ca_blck[f"dim_size_{i}"])
dimensions.append(dim_size)
total_elem *= dim_size
# 1D arrays for byte offset and invalidation bit pos calculations
byte_offset_factors.extend(ca_blck.get_byte_offset_factors())
bit_pos_inval_factors.extend(ca_blck.get_bit_pos_inval_factors())
multipliers = [1] * len(dimensions)
for i in range(len(dimensions) - 2, -1, -1):
multipliers[i] = multipliers[i + 1] * dimensions[i + 1]
def _get_nd_coords(index: int, factors: list[int]) -> list[int]:
"""Convert 1D index to CA's nD coordinates."""
coords = [0] * len(factors)
for i, factor in enumerate(factors):
coords[i] = index // factor
index %= factor
return coords
def _get_name_with_indices(ch_name: str, ch_parent_name: str, indices: list[int]) -> str:
coords = "[" + "][".join(str(coord) for coord in indices) + "]"
m = re.match(ch_parent_name, ch_name)
n = re.search(r"\[\d+\]", ch_name)
if m:
name = ch_name[: m.end()] + coords + ch_name[m.end() :]
elif n:
name = ch_name[: n.start()] + coords + ch_name[n.start() :]
else:
name = ch_name + coords
return name
ch_len = len(channels)
for elem_id in range(total_elem):
for cn_id in range(index, ch_len):
nd_coords = _get_nd_coords(elem_id, multipliers)
# copy composition block
new_block = deepcopy(channels[cn_id])
# update byte offset & position of invalidation bit
byte_offset = bit_offset = 0
for coord, byte_factor, bit_factor in zip(
nd_coords, byte_offset_factors, bit_pos_inval_factors, strict=False
):
byte_offset += coord * byte_factor
bit_offset += coord * bit_factor
new_block.byte_offset += byte_offset
new_block.pos_invalidation_bit += bit_offset
# update channel name
new_block.name = _get_name_with_indices(new_block.name, channel.name, nd_coords)
# append to channel list
channels.append(new_block)
# update channel dependencies
if (deps := dependencies[cn_id]) is not None:
cn_deps: list[tuple[int, int]] = []
for dep in deps:
if not isinstance(dep, ChannelArrayBlock):
dep_entry = (dep[0], dep[1] + (ch_len - index) * elem_id)
cn_deps.append(dep_entry)
if deps:
dependencies.append(cn_deps)
else:
dependencies.append(None)
else:
dependencies.append(None)
grp.signal_data.append(None)
# update channels db
entry = (dg_cntr, ch_cntr)
self.channels_db.add(new_block.name, entry)
ch_cntr += 1
else:
dependencies.append(None)
channel.dtype_fmt = np.dtype(
get_fmt_v4(
channel.data_type,
channel.bit_offset + channel.bit_count,
channel.channel_type,
)
)
# go to next channel of the current channel group
ch_addr = channel.next_ch_addr
if composition_channels is not None and parent_channel:
composition_channels.sort(key=lambda x: x.byte_offset)
padding = 0
dtype_fields: list[DTypeLike] = []
offset = parent_channel.byte_offset
for comp_channel in composition_channels:
if (delta := (comp_channel.byte_offset - offset)) > 0:
dtype_fields.append((f"__padding_{padding}__", f"V{delta}"))
padding += 1
dtype_fields.append((unique_names.get_unique_name(comp_channel.name), comp_channel.dtype_fmt))
offset = comp_channel.byte_offset + comp_channel.dtype_fmt.itemsize
composition_dtype = np.dtype(dtype_fields)
else:
composition_dtype = None
return ch_cntr, composition, composition_dtype
def _load_signal_data(
self,
group: Group | None = None,
index: int | None = None,
offsets: tuple[int, int] | None = None,
) -> bytes:
"""This method is used to get the channel signal data, usually for VLSD
channels.
Returns
-------
data : bytes
Signal data bytes.
"""
if group is not None and index is not None:
info_blocks = group.signal_data[index]
if info_blocks is not None:
stream: tempfile._TemporaryFileWrapper[bytes] | FileLike | mmap.mmap
if offsets is None:
data_list: list[bytes] = []
for info in group.get_signal_data_blocks(index):
address, original_size, compressed_size, block_type, param = (
info.address,
info.original_size,
info.compressed_size,
info.block_type,
info.param,
)
if not info.original_size:
continue
if info.location == v4c.LOCATION_TEMPORARY_FILE:
stream = self._tempfile
else:
if self._file is None:
raise RuntimeError("'_file' is None")
stream = self._file
stream.seek(address)
new_data = stream.read(compressed_size)
if block_type:
decompress = DECOMPRESS_FUNC_MAP[block_type]
new_data = decompress(new_data)
if block_type % 2 == 0:
# tranposed data
cols = typing.cast(int, info.param)
lines = original_size // cols
matrix_size = lines * cols
if matrix_size != original_size:
new_data = (
frombuffer(new_data[:matrix_size], dtype=uint8)
.reshape((cols, lines))
.T.ravel()
.tobytes()
+ new_data[matrix_size:]
)
else:
new_data = (
frombuffer(new_data, dtype=uint8).reshape((cols, lines)).T.ravel().tobytes()
)
data_list.append(new_data)
data = b"".join(data_list)
else:
start_offset, end_offset = offsets
data_array = bytearray()
start_offset = int(start_offset)
end_offset = int(end_offset)
last_sample_start = end_offset - start_offset
current_offset = 0
for info in group.get_signal_data_blocks(index):
address, original_size, compressed_size, block_type, param = (
info.address,
info.original_size,
info.compressed_size,
info.block_type,
info.param,
)
if not info.original_size:
continue
if info.location == v4c.LOCATION_TEMPORARY_FILE:
stream = self._tempfile
else:
if self._file is None:
raise RuntimeError("'_file' is None")
stream = self._file
if current_offset + original_size < start_offset:
current_offset += original_size
continue
stream.seek(address)
new_data = stream.read(compressed_size)
if block_type:
decompress = DECOMPRESS_FUNC_MAP[block_type]
new_data = decompress(new_data)
if block_type % 2 == 0:
# tranposed data
cols = typing.cast(int, info.param)
lines = original_size // cols
matrix_size = lines * cols
if matrix_size != original_size:
new_data = (
frombuffer(new_data[:matrix_size], dtype=uint8)
.reshape((cols, lines))
.T.ravel()
.tobytes()
+ new_data[matrix_size:]
)
else:
new_data = (
frombuffer(new_data, dtype=uint8).reshape((cols, lines)).T.ravel().tobytes()
)
if start_offset > current_offset:
data_array.extend(new_data[start_offset - current_offset :])
else:
data_array.extend(new_data)
current_offset += original_size
if (current_data_size := len(data_array)) >= last_sample_start + 4:
(last_sample_size,) = UINT32_uf(data_array, last_sample_start)
required_size = last_sample_start + 4 + last_sample_size
if required_size <= current_data_size:
data = bytes(data_array[:required_size])
break
else:
data = bytes(data_array)
else:
data = b""
else:
data = b""
return data
def _load_data(
self,
group: Group,
record_offset: int = 0,
record_count: int | None = None,
optimize_read: bool = False,
) -> Iterator[Fragment]:
"""Get group's data block bytes."""
from time import perf_counter
cc = 0
offset = 0
invalidation_offset = 0
has_yielded = False
_count = 0
data_blocks_info_generator = group.data_blocks_info_generator
channel_group = group.channel_group
stream: FileLike | mmap.mmap | tempfile._TemporaryFileWrapper[bytes]
if group.data_location == v4c.LOCATION_ORIGINAL_FILE:
stream = typing.cast(FileLike | mmap.mmap, self._file)
else:
stream = self._tempfile
read = stream.read
seek = stream.seek
if group.uses_ld:
samples_size = channel_group.samples_byte_nr
invalidation_size = channel_group.invalidation_bytes_nr
invalidation_record_offset = record_offset * invalidation_size
rm = True
else:
rm = False
samples_size = channel_group.samples_byte_nr + channel_group.invalidation_bytes_nr
invalidation_size = channel_group.invalidation_bytes_nr
record_offset *= samples_size
finished = False
if record_count is not None:
invalidation_record_count = record_count * invalidation_size
record_count *= samples_size
max_size = record_count + invalidation_record_count
else:
max_size = (invalidation_size + samples_size) * channel_group.cycles_nr
if not samples_size:
if rm:
yield Fragment(b"", offset, _count, b"")
else:
yield Fragment(b"", offset, _count, None)
else:
if group.read_split_count:
split_size = group.read_split_count * samples_size
invalidation_split_size = group.read_split_count * invalidation_size
else:
if self._read_fragment_size:
split_size = self._read_fragment_size // samples_size
invalidation_split_size = split_size * invalidation_size
split_size *= samples_size
else:
channels_nr = len(group.channels)
y_axis = CONVERT
idx = int(searchsorted(CHANNEL_COUNT, channels_nr, side="right") - 1)
idx = max(idx, 0)
split_size = y_axis[idx]
split_size = split_size // samples_size
invalidation_split_size = split_size * invalidation_size
split_size *= samples_size
if split_size == 0:
split_size = samples_size
invalidation_split_size = invalidation_size
split_size = int(split_size)
if split_size > max_size:
invalidation_split_size = (max_size // samples_size) * invalidation_size
split_size = max_size
buffer = bytearray(split_size)
buffer_view = memoryview(buffer)
invalidation_split_size = int(invalidation_split_size)
blocks = iter(group.data_blocks)
cur_size = 0
data: list[object] = []
cur_invalidation_size = 0
invalidation_data: list[bytes] = []
tt = perf_counter()
ss = 0
cc = 0
while True:
try:
info = next(blocks)
(
address,
original_size,
compressed_size,
block_type,
param,
block_limit,
) = (
info.address,
typing.cast(int, info.original_size),
info.compressed_size,
info.block_type,
info.param,
info.block_limit,
)
if rm and invalidation_size:
invalidation_info = info.invalidation_block
else:
invalidation_info = None
except StopIteration:
try:
info = next(data_blocks_info_generator)
(
address,
original_size,
compressed_size,
block_type,
param,
block_limit,
) = (
info.address,
typing.cast(int, info.original_size),
info.compressed_size,
info.block_type,
info.param,
info.block_limit,
)
if rm and invalidation_size:
invalidation_info = info.invalidation_block
else:
invalidation_info = None
group.data_blocks.append(info)
except StopIteration:
break
if offset + original_size < record_offset + 1:
offset += original_size
if rm and invalidation_size:
if invalidation_info is None:
raise RuntimeError(
"'invalidation_info' cannot be None if 'rm and invalidation_size' is True"
)
if invalidation_info.all_valid:
count = original_size // samples_size
invalidation_offset += count * invalidation_size
else:
invalidation_offset += typing.cast(int, invalidation_info.original_size)
continue
seek(address)
new_data: bytes | memoryview[int] = read(typing.cast(int, compressed_size))
cc += 1
ss += original_size
if block_type:
decompress = DECOMPRESS_FUNC_MAP[block_type]
new_data = decompress(new_data)
if block_type % 2 == 0:
# tranposed data
cols = typing.cast(int, param)
lines = original_size // cols
matrix_size = lines * cols
if matrix_size != original_size:
new_data = (
frombuffer(new_data[:matrix_size], dtype=uint8)
.reshape((cols, lines))
.T.ravel()
.tobytes()
+ new_data[matrix_size:]
)
else:
new_data = frombuffer(new_data, dtype=uint8).reshape((cols, lines)).T.ravel().tobytes()
if block_limit is not None:
new_data = new_data[:block_limit]
if len(new_data) > split_size - cur_size:
new_data = memoryview(new_data)
if rm and invalidation_size:
if invalidation_info is None:
raise RuntimeError("'invalidation_info' cannot be None if 'rm and invalidation_size' is True")
if invalidation_info.all_valid:
count = original_size // samples_size
new_invalidation_data = b"\0" * (count * invalidation_size)
else:
seek(invalidation_info.address)
compressed_size = typing.cast(int, invalidation_info.compressed_size)
new_invalidation_data = read(compressed_size)
original_size = typing.cast(int, invalidation_info.original_size)
if invalidation_info.block_type:
decompress = DECOMPRESS_FUNC_MAP[invalidation_info.block_type]
new_invalidation_data = decompress(new_invalidation_data)
if invalidation_info.block_type % 2 == 0:
# tranposed data
cols = typing.cast(int, param)
lines = original_size // cols
matrix_size = lines * cols
if matrix_size != original_size:
new_invalidation_data = (
frombuffer(new_invalidation_data[:matrix_size], dtype=uint8)
.reshape((cols, lines))
.T.ravel()
.tobytes()
+ new_invalidation_data[matrix_size:]
)
else:
new_invalidation_data = (
frombuffer(new_invalidation_data, dtype=uint8)
.reshape((cols, lines))
.T.ravel()
.tobytes()
)
if invalidation_info.block_limit is not None:
new_invalidation_data = new_invalidation_data[: invalidation_info.block_limit]
inv_size = len(new_invalidation_data)
if offset < record_offset:
delta = record_offset - offset
new_data = new_data[delta:]
original_size -= delta
offset = record_offset
if rm and invalidation_size:
delta = invalidation_record_offset - invalidation_offset
new_invalidation_data = new_invalidation_data[delta:]
inv_size -= delta
invalidation_offset = invalidation_record_offset
while original_size >= split_size - cur_size:
if cur_size:
buffer_view[cur_size:] = new_data[: split_size - cur_size]
new_data = new_data[split_size - cur_size :]
if rm and invalidation_size:
invalidation_data.append(
new_invalidation_data[: invalidation_split_size - cur_invalidation_size]
)
new_invalidation_data = new_invalidation_data[
invalidation_split_size - cur_invalidation_size :
]
invalidation_data_ = b"".join(invalidation_data)
if record_count is not None:
if rm and invalidation_size:
__data = buffer[:record_count]
_count = len(__data) // samples_size
yield Fragment(
__data,
offset // samples_size,
_count,
invalidation_data_[:invalidation_record_count],
)
invalidation_record_count -= len(invalidation_data_)
else:
__data = buffer[:record_count]
_count = len(__data) // samples_size
yield Fragment(__data, offset // samples_size, _count, None)
has_yielded = True
record_count -= split_size
if record_count <= 0:
finished = True
break
else:
if rm and invalidation_size:
_count = split_size // samples_size
yield Fragment(buffer, offset // samples_size, _count, invalidation_data_)
else:
_count = split_size // samples_size
yield Fragment(buffer, offset // samples_size, _count, None)
has_yielded = True
else:
buffer_view[:] = new_data[:split_size]
new_data = new_data[split_size:]
if rm and invalidation_size:
invalidation_data_ = new_invalidation_data[:invalidation_split_size]
new_invalidation_data = new_invalidation_data[invalidation_split_size:]
if record_count is not None:
if rm and invalidation_size:
yield Fragment(
buffer[:record_count],
offset // samples_size,
_count,
invalidation_data_[:invalidation_record_count],
)
invalidation_record_count -= len(invalidation_data_)
else:
__data = buffer[:record_count]
_count = len(__data) // samples_size
yield Fragment(__data, offset // samples_size, _count, None)
has_yielded = True
record_count -= split_size
if record_count <= 0:
finished = True
cur_size = 0
break
else:
if rm and invalidation_size:
_count = split_size // samples_size
yield Fragment(buffer, offset // samples_size, _count, invalidation_data_)
else:
_count = split_size // samples_size
yield Fragment(buffer, offset // samples_size, _count, None)
has_yielded = True
offset += split_size
original_size -= split_size - cur_size
data = []
cur_size = 0
if rm and invalidation_size:
invalidation_offset += invalidation_split_size
invalidation_data = []
cur_invalidation_size = 0
inv_size -= invalidation_split_size - cur_invalidation_size
if finished:
cur_size = 0
original_size = 0
if rm and invalidation_size:
invalidation_data = []
break
if original_size > 0:
buffer_view[cur_size : cur_size + original_size] = new_data
cur_size += original_size
if rm and invalidation_size:
invalidation_data.append(new_invalidation_data)
cur_invalidation_size += inv_size
if record_count is not None and cur_size >= record_count:
finished = True
break
if (vv := (perf_counter() - tt)) > 10:
print(f"{ss / 1024 / 1024 / vv:.6f} MB/s {cc=} {vv=}")
cc = 0
ss = 0
tt = perf_counter()
if cur_size:
data_ = buffer[:cur_size]
if rm and invalidation_size:
invalidation_data_ = b"".join(invalidation_data)
if record_count is not None:
if rm and invalidation_size:
__data = data_[:record_count]
_count = len(__data) // samples_size
yield Fragment(
__data, offset // samples_size, _count, invalidation_data_[:invalidation_record_count]
)
invalidation_record_count -= len(invalidation_data_)
else:
__data = data_[:record_count]
_count = len(__data) // samples_size
yield Fragment(__data, offset // samples_size, _count, None)
has_yielded = True
record_count -= len(data_)
else:
if rm and invalidation_size:
_count = len(data_) // samples_size
yield Fragment(data_, offset // samples_size, _count, invalidation_data_)
else:
_count = len(data_) // samples_size
yield Fragment(data_, offset // samples_size, _count, None)
has_yielded = True
if not has_yielded:
if rm and invalidation_size:
yield Fragment(b"", 0, 0, b"")
else:
yield Fragment(b"", 0, 0, None)
def _prepare_record(self, group: Group) -> list[tuple[np.dtype[Any], int, int, int] | None]:
"""Compute record.
Parameters
----------
group : dict
MDF group dict.
Returns
-------
record : list
Mapping of channels to records fields, records fields dtype.
"""
if group.record is None:
channels = group.channels
record: list[tuple[np.dtype[Any], int, int, int] | None] = []
for idx, new_ch in enumerate(channels):
start_offset = new_ch.byte_offset
bit_offset = new_ch.bit_offset
data_type = new_ch.data_type
bit_count = new_ch.bit_count
ch_type = new_ch.channel_type
dependency_list = group.channel_dependencies[idx]
if ch_type not in v4c.VIRTUAL_TYPES:
# adjust size to 1, 2, 4 or 8 bytes
size = bit_offset + bit_count
byte_size, rem = divmod(size, 8)
if rem:
byte_size += 1
bit_size = byte_size * 8
if data_type in (
v4c.DATA_TYPE_SIGNED_MOTOROLA,
v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
):
if size > 32:
bit_offset += 64 - bit_size
elif size > 16:
bit_offset += 32 - bit_size
elif size > 8:
bit_offset += 16 - bit_size
if new_ch.dtype_fmt == np.dtype(np.void):
new_ch.dtype_fmt = np.dtype(get_fmt_v4(data_type, size, ch_type))
if (
bit_offset
or dependency_list
or (new_ch.dtype_fmt.kind in "ui" and size < 64 and size not in (8, 16, 32))
):
new_ch.standard_C_size = False
record.append(
(
new_ch.dtype_fmt,
new_ch.dtype_fmt.itemsize,
start_offset,
bit_offset,
)
)
else:
record.append(None)
group.record = record
return group.record
def _uses_ld(
self,
address: int,
stream: FileLike | mmap.mmap,
mapped: bool = False,
) -> bool:
mapped = mapped or not is_file_like(stream)
uses_ld = False
if mapped:
if address:
if address + COMMON_SHORT_SIZE > self.file_limit:
handle_incomplete_block(address, self.file_limit, self.original_name)
return False
id_string, block_len = COMMON_SHORT_uf(stream, address)
if id_string == b"##LD":
uses_ld = True
# or a header list
elif id_string == b"##HL":
hl = HeaderList(address=address, stream=stream, mapped=mapped, file_limit=self.file_limit)
address = hl.first_dl_addr
uses_ld = self._uses_ld(
address,
stream,
mapped,
)
else:
if address:
if address + COMMON_SHORT_SIZE > self.file_limit:
handle_incomplete_block(address, self.file_limit, self.original_name)
return False
stream.seek(address)
id_string, block_len = COMMON_SHORT_u(stream.read(COMMON_SHORT_SIZE))
# can be a DataBlock
if id_string == b"##LD":
uses_ld = True
# or a header list
elif id_string == b"##HL":
hl = HeaderList(address=address, stream=stream, file_limit=self.file_limit)
address = hl.first_dl_addr
uses_ld = self._uses_ld(
address,
stream,
mapped,
)
return uses_ld
def _get_data_blocks_info(
self,
address: int,
stream: FileLike | mmap.mmap,
mapped: bool = False,
total_size: int = 0,
inval_total_size: int = 0,
record_size: int = 0,
) -> Iterator[DataBlockInfo]:
mapped = mapped or not is_file_like(stream)
if record_size > 4 * 1024 * 1024:
READ_CHUNK_SIZE = record_size
elif record_size:
READ_CHUNK_SIZE = 4 * 1024 * 1024 // record_size * record_size
else:
READ_CHUNK_SIZE = 4 * 1024 * 1024
READ_CHUNK_SIZE = min(READ_CHUNK_SIZE, total_size)
if mapped:
if original_address := address:
if address + COMMON_SHORT_SIZE > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
id_string, block_len = COMMON_SHORT_uf(stream, address)
if address + block_len > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
# can be a DataBlock
if id_string in (b"##DT", b"##DV"):
size = block_len - 24
if size:
size = min(size, total_size)
address = address + COMMON_SIZE
# split the DTBLOCK into chucks of up to 32MB
while True:
if size > READ_CHUNK_SIZE:
total_size -= READ_CHUNK_SIZE
size -= READ_CHUNK_SIZE
yield DataBlockInfo(
address=address,
block_type=v4c.DT_BLOCK,
original_size=READ_CHUNK_SIZE,
compressed_size=READ_CHUNK_SIZE,
param=0,
block_limit=None,
)
address += READ_CHUNK_SIZE
else:
if total_size < size:
block_limit = total_size
else:
block_limit = None
yield DataBlockInfo(
address=address,
block_type=v4c.DT_BLOCK,
original_size=size,
compressed_size=size,
param=0,
block_limit=block_limit,
)
break
# or a DataZippedBlock
elif id_string == b"##DZ":
(
zip_type,
param,
original_size,
zip_size,
) = v4c.DZ_COMMON_INFO_uf(stream, address + v4c.DZ_INFO_COMMON_OFFSET)
if original_size:
block_type_ = v4c.DZ_FLAG_TO_TYPE[zip_type]
if total_size < original_size:
block_limit = total_size
else:
block_limit = None
total_size -= original_size
yield DataBlockInfo(
address=address + v4c.DZ_COMMON_SIZE,
block_type=block_type_,
original_size=original_size,
compressed_size=zip_size,
param=param,
block_limit=block_limit,
)
# or a DataList
elif id_string == b"##DL":
while address:
dl = DataList(address=address, stream=stream, mapped=mapped, file_limit=self.file_limit)
for i in range(dl.data_block_nr):
original_address = addr = getattr(dl, f"data_block_addr{i}")
if original_address + COMMON_SHORT_SIZE > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
id_string, block_len = COMMON_SHORT_uf(stream, addr)
if original_address + block_len > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
# can be a DataBlock
if id_string != b"##DZ":
size = block_len - 24
if size:
size = min(size, total_size)
addr += COMMON_SIZE
# split the DTBLOCK into chucks of up to 32MB
while True:
if size > READ_CHUNK_SIZE:
total_size -= READ_CHUNK_SIZE
size -= READ_CHUNK_SIZE
yield DataBlockInfo(
address=addr,
block_type=v4c.DT_BLOCK,
original_size=READ_CHUNK_SIZE,
compressed_size=READ_CHUNK_SIZE,
param=0,
block_limit=None,
)
addr += READ_CHUNK_SIZE
else:
if total_size < size:
block_limit = total_size
else:
block_limit = None
total_size -= size
yield DataBlockInfo(
address=addr,
block_type=v4c.DT_BLOCK,
original_size=size,
compressed_size=size,
param=0,
block_limit=block_limit,
)
break
# or a DataZippedBlock
else:
(
zip_type,
param,
original_size,
zip_size,
) = v4c.DZ_COMMON_INFO_uf(stream, addr + v4c.DZ_INFO_COMMON_OFFSET)
if original_size:
block_type_ = v4c.DZ_FLAG_TO_TYPE[zip_type]
if total_size < original_size:
block_limit = total_size
else:
block_limit = None
total_size -= original_size
yield DataBlockInfo(
address=addr + v4c.DZ_COMMON_SIZE,
block_type=block_type_,
original_size=original_size,
compressed_size=zip_size,
param=param,
block_limit=block_limit,
)
address = dl.next_dl_addr
# or a ListData
elif id_string == b"##LD":
uses_ld = True
while address:
ld = ListData(address=address, stream=stream, mapped=mapped, file_limit=self.file_limit)
has_invalidation = ld.flags_ext & v4c.FLAG_LD_EXT_INVALIDATION_PRESENT
for i in range(ld.data_block_nr):
original_address = addr = getattr(ld, f"data_block_addr_{i}")
if original_address + COMMON_SHORT_SIZE > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
id_string, block_len = COMMON_SHORT_uf(stream, addr)
if original_address + block_len > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
# can be a DataBlock
if id_string == b"##DV":
size = block_len - 24
if size:
if total_size < size:
block_limit = total_size
else:
block_limit = None
total_size -= size
data_info = DataBlockInfo(
address=addr + COMMON_SIZE,
block_type=v4c.DT_BLOCK,
original_size=size,
compressed_size=size,
param=0,
block_limit=block_limit,
)
# or a DataZippedBlock
elif id_string == b"##DZ":
(
zip_type,
param,
original_size,
zip_size,
) = v4c.DZ_COMMON_INFO_uf(stream, addr + v4c.DZ_INFO_COMMON_OFFSET)
if original_size:
block_type_ = v4c.DZ_FLAG_TO_TYPE[zip_type]
if total_size < original_size:
block_limit = total_size
else:
block_limit = None
total_size -= original_size
data_info = DataBlockInfo(
address=addr + v4c.DZ_COMMON_SIZE,
block_type=block_type_,
original_size=original_size,
compressed_size=zip_size,
param=param,
block_limit=block_limit,
)
if has_invalidation:
inval_addr = typing.cast(int, ld[f"invalidation_bits_addr_{i}"])
if original_address := inval_addr:
if original_address + COMMON_SHORT_SIZE > self.file_limit:
return handle_incomplete_block(
original_address, self.file_limit, self.original_name
)
id_string, block_len = COMMON_SHORT_uf(stream, inval_addr)
if original_address + block_len > self.file_limit:
return handle_incomplete_block(
original_address, self.file_limit, self.original_name
)
if id_string == b"##DI":
size = block_len - 24
if size:
if inval_total_size < size:
block_limit = inval_total_size
else:
block_limit = None
inval_total_size -= size
data_info.invalidation_block = InvalidationBlockInfo(
address=inval_addr + COMMON_SIZE,
block_type=v4c.DT_BLOCK,
original_size=size,
compressed_size=size,
param=0,
block_limit=block_limit,
)
else:
(
zip_type,
param,
original_size,
zip_size,
) = v4c.DZ_COMMON_INFO_uf(
stream,
inval_addr + v4c.DZ_INFO_COMMON_OFFSET,
)
if original_size:
block_type_ = v4c.DZ_FLAG_TO_TYPE[zip_type]
if inval_total_size < original_size:
block_limit = inval_total_size
else:
block_limit = None
inval_total_size -= original_size
data_info.invalidation_block = InvalidationBlockInfo(
address=inval_addr + v4c.DZ_COMMON_SIZE,
block_type=block_type_,
original_size=original_size,
compressed_size=zip_size,
param=param,
block_limit=block_limit,
)
else:
data_info.invalidation_block = InvalidationBlockInfo(
address=0,
block_type=v4c.DT_BLOCK,
original_size=None,
compressed_size=None,
param=None,
all_valid=True,
)
yield data_info
address = ld.next_ld_addr
# or a header list
elif id_string == b"##HL":
hl = HeaderList(address=address, stream=stream, mapped=mapped, file_limit=self.file_limit)
address = hl.first_dl_addr
yield from self._get_data_blocks_info(
address,
stream,
mapped,
total_size,
inval_total_size,
record_size,
)
else:
if original_address := address:
if original_address + COMMON_SHORT_SIZE > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
stream.seek(address)
id_string, block_len = COMMON_SHORT_u(stream.read(COMMON_SHORT_SIZE))
if original_address + block_len > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
# can be a DataBlock
if id_string in (b"##DT", b"##DV"):
size = block_len - 24
if size:
size = min(size, total_size)
address = address + COMMON_SIZE
# split the DTBLOCK into chucks of up to 32MB
while True:
if size > READ_CHUNK_SIZE:
total_size -= READ_CHUNK_SIZE
size -= READ_CHUNK_SIZE
yield DataBlockInfo(
address=address,
block_type=v4c.DT_BLOCK,
original_size=READ_CHUNK_SIZE,
compressed_size=READ_CHUNK_SIZE,
param=0,
block_limit=None,
)
address += READ_CHUNK_SIZE
else:
if total_size < size:
block_limit = total_size
else:
block_limit = None
yield DataBlockInfo(
address=address,
block_type=v4c.DT_BLOCK,
original_size=size,
compressed_size=size,
param=0,
block_limit=block_limit,
)
break
# or a DataZippedBlock
elif id_string == b"##DZ":
stream.seek(address + v4c.DZ_INFO_COMMON_OFFSET)
(
zip_type,
param,
original_size,
zip_size,
) = v4c.DZ_COMMON_INFO_u(stream.read(v4c.DZ_COMMON_INFO_SIZE))
if original_size:
block_type_ = v4c.DZ_FLAG_TO_TYPE[zip_type]
if total_size < original_size:
block_limit = total_size
else:
block_limit = None
total_size -= original_size
yield DataBlockInfo(
address=address + v4c.DZ_COMMON_SIZE,
block_type=block_type_,
original_size=original_size,
compressed_size=zip_size,
param=param,
block_limit=block_limit,
)
# or a DataList
elif id_string == b"##DL":
while address:
dl = DataList(address=address, stream=stream, file_limit=self.file_limit)
for i in range(dl.data_block_nr):
original_address = addr = getattr(dl, f"data_block_addr{i}")
if original_address + COMMON_SHORT_SIZE > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
stream.seek(addr)
id_string, block_len = COMMON_SHORT_u(stream.read(COMMON_SHORT_SIZE))
if original_address + block_len > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
# can be a DataBlock
if id_string != b"##DZ":
size = block_len - 24
if size:
addr = addr + COMMON_SIZE
# split the DTBLOCK into chucks of up to 32MB
while True:
if size > READ_CHUNK_SIZE:
total_size -= READ_CHUNK_SIZE
size -= READ_CHUNK_SIZE
yield DataBlockInfo(
address=addr,
block_type=v4c.DT_BLOCK,
original_size=READ_CHUNK_SIZE,
compressed_size=READ_CHUNK_SIZE,
param=0,
block_limit=None,
)
addr += READ_CHUNK_SIZE
else:
if total_size < size:
block_limit = total_size
else:
block_limit = None
total_size -= size
yield DataBlockInfo(
address=addr,
block_type=v4c.DT_BLOCK,
original_size=size,
compressed_size=size,
param=0,
block_limit=block_limit,
)
break
# or a DataZippedBlock
else:
stream.seek(addr + v4c.DZ_INFO_COMMON_OFFSET)
(
zip_type,
param,
original_size,
zip_size,
) = v4c.DZ_COMMON_INFO_u(stream.read(v4c.DZ_COMMON_INFO_SIZE))
if original_size:
block_type_ = v4c.DZ_FLAG_TO_TYPE[zip_type]
if total_size < original_size:
block_limit = total_size
else:
block_limit = None
total_size -= original_size
yield DataBlockInfo(
address=addr + v4c.DZ_COMMON_SIZE,
block_type=block_type_,
original_size=original_size,
compressed_size=zip_size,
param=param,
block_limit=block_limit,
)
address = dl.next_dl_addr
# or a DataList
elif id_string == b"##LD":
uses_ld = True
while address:
ld = ListData(address=address, stream=stream, file_limit=self.file_limit)
has_invalidation = ld.flags_ext & v4c.FLAG_LD_EXT_INVALIDATION_PRESENT
for i in range(ld.data_block_nr):
original_address = addr = getattr(ld, f"data_block_addr{i}")
if original_address + COMMON_SHORT_SIZE > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
stream.seek(addr)
id_string, block_len = COMMON_SHORT_u(stream.read(COMMON_SHORT_SIZE))
if original_address + block_len > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
# can be a DataBlock
if id_string == b"##DV":
size = block_len - 24
if size:
if total_size < size:
block_limit = total_size
else:
block_limit = None
total_size -= size
data_info = DataBlockInfo(
address=addr + COMMON_SIZE,
block_type=v4c.DT_BLOCK,
original_size=size,
compressed_size=size,
param=0,
block_limit=block_limit,
)
# or a DataZippedBlock
elif id_string == b"##DZ":
stream.seek(addr + v4c.DZ_INFO_COMMON_OFFSET)
(
zip_type,
param,
original_size,
zip_size,
) = v4c.DZ_COMMON_INFO_u(stream.read(v4c.DZ_COMMON_INFO_SIZE))
if original_size:
block_type_ = v4c.DZ_FLAG_TO_TYPE[zip_type]
if total_size < original_size:
block_limit = total_size
else:
block_limit = None
total_size -= original_size
data_info = DataBlockInfo(
address=addr + v4c.DZ_COMMON_SIZE,
block_type=block_type_,
original_size=original_size,
compressed_size=zip_size,
param=param,
block_limit=block_limit,
)
if has_invalidation:
inval_addr = typing.cast(int, ld[f"invalidation_bits_addr_{i}"])
if original_address := inval_addr:
if original_address + COMMON_SHORT_SIZE > self.file_limit:
return handle_incomplete_block(
original_address, self.file_limit, self.original_name
)
stream.seek(inval_addr)
id_string, block_len = COMMON_SHORT_u(stream.read(COMMON_SHORT_SIZE))
if original_address + block_len > self.file_limit:
return handle_incomplete_block(
original_address, self.file_limit, self.original_name
)
if id_string == b"##DI":
size = block_len - 24
if size:
if inval_total_size < size:
block_limit = inval_total_size
else:
block_limit = None
inval_total_size -= size
data_info.invalidation_block = InvalidationBlockInfo(
address=inval_addr + COMMON_SIZE,
block_type=v4c.DT_BLOCK,
original_size=size,
compressed_size=size,
param=0,
block_limit=block_limit,
)
else:
stream.seek(inval_addr + v4c.DZ_INFO_COMMON_OFFSET)
(
zip_type,
param,
original_size,
zip_size,
) = v4c.DZ_COMMON_INFO_u(stream.read(v4c.DZ_COMMON_INFO_SIZE))
if original_size:
block_type_ = v4c.DZ_FLAG_TO_TYPE[zip_type]
if inval_total_size < original_size:
block_limit = inval_total_size
else:
block_limit = None
inval_total_size -= original_size
data_info.invalidation_block = InvalidationBlockInfo(
address=inval_addr + v4c.DZ_COMMON_SIZE,
block_type=block_type_,
original_size=original_size,
compressed_size=zip_size,
param=param,
block_limit=block_limit,
)
else:
data_info.invalidation_block = InvalidationBlockInfo(
address=0,
block_type=v4c.DT_BLOCK,
original_size=0,
compressed_size=0,
param=0,
all_valid=True,
)
yield data_info
address = ld.next_ld_addr
# or a header list
elif id_string == b"##HL":
hl = HeaderList(address=address, stream=stream, file_limit=self.file_limit)
address = hl.first_dl_addr
yield from self._get_data_blocks_info(
address,
stream,
mapped,
total_size,
inval_total_size,
record_size,
)
def _get_signal_data_blocks_info(
self,
address: int,
stream: FileLike | mmap.mmap,
) -> Iterator[SignalDataBlockInfo]:
if not (original_address := address):
raise MdfException(f"Expected non-zero SDBLOCK address but got 0x{address:X}")
if original_address + COMMON_SHORT_SIZE > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
stream.seek(address)
id_string, block_len = COMMON_SHORT_u(stream.read(COMMON_SHORT_SIZE))
if original_address + block_len > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
# can be a DataBlock
if id_string == b"##SD":
size = block_len - 24
if size:
yield SignalDataBlockInfo(
address=address + COMMON_SIZE,
compressed_size=size,
original_size=size,
block_type=v4c.DT_BLOCK,
)
# or a DataZippedBlock
elif id_string == b"##DZ":
stream.seek(address + v4c.DZ_INFO_COMMON_OFFSET)
(
zip_type,
param,
original_size,
zip_size,
) = v4c.DZ_COMMON_INFO_u(stream.read(v4c.DZ_COMMON_INFO_SIZE))
if original_size:
block_type_ = v4c.DZ_FLAG_TO_TYPE[zip_type]
yield SignalDataBlockInfo(
address=address + v4c.DZ_COMMON_SIZE,
block_type=block_type_,
original_size=original_size,
compressed_size=zip_size,
param=param,
)
# or a DataList
elif id_string == b"##DL":
while address:
dl = DataList(address=address, stream=stream, file_limit=self.file_limit)
for i in range(dl.data_block_nr):
original_address = addr = typing.cast(int, dl[f"data_block_addr{i}"])
if original_address + COMMON_SHORT_SIZE > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
stream.seek(addr)
id_string, block_len = COMMON_SHORT_u(stream.read(COMMON_SHORT_SIZE))
if original_address + block_len > self.file_limit:
return handle_incomplete_block(original_address, self.file_limit, self.original_name)
# can be a DataBlock
if id_string == b"##SD":
size = block_len - 24
if size:
yield SignalDataBlockInfo(
address=addr + COMMON_SIZE,
compressed_size=size,
original_size=size,
block_type=v4c.DT_BLOCK,
)
# or a DataZippedBlock
elif id_string == b"##DZ":
stream.seek(addr + v4c.DZ_INFO_COMMON_OFFSET)
(
zip_type,
param,
original_size,
zip_size,
) = v4c.DZ_COMMON_INFO_u(stream.read(v4c.DZ_COMMON_INFO_SIZE))
if original_size:
block_type_ = v4c.DZ_FLAG_TO_TYPE[zip_type]
yield SignalDataBlockInfo(
address=addr + v4c.DZ_COMMON_SIZE,
block_type=block_type_,
original_size=original_size,
compressed_size=zip_size,
param=param,
)
address = dl.next_dl_addr
# or a header list
elif id_string == b"##HL":
hl = HeaderList(address=address, stream=stream, file_limit=self.file_limit)
address = hl.first_dl_addr
yield from self._get_signal_data_blocks_info(
address,
stream,
)
def _filter_occurrences(
self,
occurrences: Iterator[tuple[int, int]],
source_name: str | None = None,
source_path: str | None = None,
acq_name: str | None = None,
) -> Iterator[tuple[int, int]]:
if source_name is not None:
occurrences = (
(gp_idx, cn_idx)
for gp_idx, cn_idx in occurrences
if ((source := self.groups[gp_idx].channels[cn_idx].source) is not None and source.name == source_name)
or (
(acq_source := self.groups[gp_idx].channel_group.acq_source) is not None
and acq_source.name == source_name
)
)
if source_path is not None:
occurrences = (
(gp_idx, cn_idx)
for gp_idx, cn_idx in occurrences
if ((source := self.groups[gp_idx].channels[cn_idx].source) is not None and source.path == source_path)
or (
(acq_source := self.groups[gp_idx].channel_group.acq_source) is not None
and acq_source.path == source_path
)
)
if acq_name is not None:
occurrences = (
(gp_idx, cn_idx)
for gp_idx, cn_idx in occurrences
if self.groups[gp_idx].channel_group.acq_name == acq_name
)
return occurrences
[docs]
def get_invalidation_bits(
self,
group_index: int,
pos_invalidation_bit: int,
fragment: Fragment,
one_piece: bool = False,
) -> InvalidationArray | None:
"""Get invalidation indexes of the channels in the given group.
Parameters
----------
group_index : int
Group index.
pos_invalidation_bit : int
Channel invalidation bit position.
fragment : Fragment
Data bytes as a Fragment.
one_piece : bool
onley one piece was given in the get call
Returns
-------
invalidation_bits : iterable
Iterable of valid channel indexes; if all are valid `None` is
returned.
"""
group = self.groups[group_index]
data_bytes, offset, _count, invalidation_bytes = (
fragment.data,
fragment.record_offset,
fragment.record_count,
fragment.invalidation_data,
)
if invalidation_bytes is None:
invalidation_bytes_nr = group.channel_group.invalidation_bytes_nr
samples_byte_nr = group.channel_group.samples_byte_nr
record = group.record
if record is None:
self._prepare_record(group)
invalidation_bytes = get_channel_raw_bytes(
data_bytes,
samples_byte_nr + invalidation_bytes_nr,
samples_byte_nr,
invalidation_bytes_nr,
)
key = (group_index, offset, _count, pos_invalidation_bit)
if key not in self._invalidation_cache:
inv = get_invalidation_bits_array(
invalidation_bytes, group.channel_group.invalidation_bytes_nr, pos_invalidation_bit, _count, one_piece
)
if inv is None:
self._invalidation_cache[key] = None
else:
self._invalidation_cache[key] = InvalidationArray(
inv,
(group_index, pos_invalidation_bit),
)
return self._invalidation_cache[key]
@overload
def append(
self,
signals: DataFrame,
acq_name: str | None = None,
acq_source: Source | None = None,
comment: str = "Python",
common_timebase: bool = False,
units: dict[str, str] | None = None,
) -> None: ...
@overload
def append(
self,
signals: list[Signal] | Signal,
acq_name: str | None = None,
acq_source: Source | None = None,
comment: str = "Python",
common_timebase: bool = False,
units: dict[str, str] | None = None,
) -> int: ...
@overload
def append(
self,
signals: list[Signal] | Signal | DataFrame,
acq_name: str | None = ...,
acq_source: Source | None = ...,
comment: str = ...,
common_timebase: bool = ...,
units: dict[str, str] | None = ...,
) -> int | None: ...
[docs]
def append(
self,
signals: list[Signal] | Signal | DataFrame,
acq_name: str | None = None,
acq_source: Source | None = None,
comment: str = "Python",
common_timebase: bool = False,
units: dict[str, str] | None = None,
) -> int | None:
"""Append a new data group.
For channel dependencies type Signals, the `samples` attribute must be
a np.recarray.
Parameters
----------
signals : list | Signal | pandas.DataFrame
List of `Signal` objects, or a single `Signal` object, or a pandas
DataFrame object. All bytes columns in the DataFrame must be
*utf-8* encoded.
acq_name : str, optional
Channel group acquisition name.
acq_source : Source, optional
Channel group acquisition source.
comment : str, default 'Python'
Channel group comment.
common_timebase : bool, default False
Flag to hint that the signals have the same timebase. Only set this
if you know for sure that all appended channels share the same time
base.
units : dict, optional
Will contain the signal units mapped to the signal names when
appending a pandas DataFrame.
Examples
--------
>>> from asammdf import MDF, Signal
>>> import numpy as np
>>> import pandas as pd
Case 1: Conversion type None.
>>> s1 = np.array([1, 2, 3, 4, 5])
>>> s2 = np.array([-1, -2, -3, -4, -5])
>>> s3 = np.array([0.1, 0.04, 0.09, 0.16, 0.25])
>>> t = np.array([0.001, 0.002, 0.003, 0.004, 0.005])
>>> s1 = Signal(samples=s1, timestamps=t, unit='+', name='Positive')
>>> s2 = Signal(samples=s2, timestamps=t, unit='-', name='Negative')
>>> s3 = Signal(samples=s3, timestamps=t, unit='flts', name='Floats')
>>> mdf = MDF(version='4.10')
>>> mdf.append([s1, s2, s3], comment='created by asammdf')
Case 2: VTAB conversions from channels inside another file.
>>> mdf1 = MDF('in.mf4')
>>> ch1 = mdf1.get("Channel1_VTAB")
>>> ch2 = mdf1.get("Channel2_VTABR")
>>> mdf2 = MDF('out.mf4')
>>> mdf2.append([ch1, ch2], comment='created by asammdf')
>>> mdf2.append(ch1, comment='just a single channel')
>>> df = pd.DataFrame.from_dict({'s1': np.array([1, 2, 3, 4, 5]), 's2': np.array([-1, -2, -3, -4, -5])})
>>> units = {'s1': 'V', 's2': 'A'}
>>> mdf2.append(df, units=units)
"""
source_block = SourceInformation.from_common_source(acq_source) if acq_source else None
if isinstance(signals, Signal):
signals = [signals]
elif isinstance(signals, DataFrame):
self._append_dataframe(
signals,
acq_name=acq_name,
acq_source=source_block,
comment=comment,
units=units,
)
return None
if not signals:
return None
# check if the signals have a common timebase
# if not interpolate the signals using the union of all timebases
supports_virtual_channels = self.version >= "4.10"
virtual_master = False
virtual_master_conversion = None
if signals:
t_ = signals[0].timestamps
if (
supports_virtual_channels
and all(sig.flags & sig.Flags.virtual_master for sig in signals)
and all(np.array_equal(sig.timestamps, t_) for sig in signals)
):
virtual_master = True
virtual_master_conversion = signals[0].virtual_master_conversion
t = t_
else:
if not common_timebase:
for s in signals[1:]:
if not array_equal(s.timestamps, t_):
different = True
break
else:
different = False
if different:
times = [s.timestamps for s in signals]
t = unique(concatenate(times))
if t.dtype != float64:
t = t.astype(float64)
signals = [
s.interp(
t,
integer_interpolation_mode=self._integer_interpolation,
float_interpolation_mode=self._float_interpolation,
)
for s in signals
]
else:
t = t_
else:
t = t_
else:
t = np.array([])
if self.version >= "4.20" and self._column_storage:
return self._append_column_oriented(signals, acq_name=acq_name, acq_source=source_block, comment=comment)
dg_cntr = len(self.groups)
gp = Group(DataGroup())
gp_sdata = gp.signal_data = []
gp_channels = gp.channels = []
gp_dep = gp.channel_dependencies = []
gp_sig_types = gp.signal_types = []
cycles_nr = len(t)
# channel group
cg_kwargs: ChannelGroupKwargs = {"cycles_nr": cycles_nr, "samples_byte_nr": 0}
gp.channel_group = ChannelGroup(**cg_kwargs)
gp.channel_group.acq_name = acq_name
gp.channel_group.acq_source = source_block
gp.channel_group.comment = comment
record = gp.record = []
inval_bits: dict[tuple[int, int], list[InvalidationArray] | InvalidationArray] = {
InvalidationArray.ORIGIN_UNKNOWN: []
}
if any(sig.invalidation_bits is not None for sig in signals):
invalidation_bytes_nr = 1
gp.channel_group.invalidation_bytes_nr = invalidation_bytes_nr
else:
invalidation_bytes_nr = 0
self.groups.append(gp)
fields: list[tuple[bytes | NDArray[Any], int]] = []
ch_cntr = 0
offset = 0
defined_texts: dict[str, int] = {}
si_map = self._si_map
# setup all blocks related to the time master channel
file = self._tempfile
tell = file.tell
seek = file.seek
seek(0, 2)
if signals:
master_metadata = signals[0].master_metadata
else:
master_metadata = None
if master_metadata:
time_name, sync_type = master_metadata
match sync_type:
case 0 | 1:
time_unit = "s"
case 2:
time_unit = "deg"
case 3:
time_unit = "m"
case 4:
time_unit = "index"
else:
time_name, sync_type = "time", v4c.SYNC_TYPE_TIME
time_unit = "s"
gp.channel_group.acq_source = source_block
if signals:
# time channel
if virtual_master:
cn_kwargs: ChannelKwargs = {
"channel_type": v4c.CHANNEL_TYPE_VIRTUAL_MASTER,
"data_type": v4c.DATA_TYPE_UNSIGNED_INTEL,
"sync_type": sync_type,
"byte_offset": 0,
"bit_offset": 0,
"bit_count": 0,
}
ch = Channel(**cn_kwargs)
ch.unit = time_unit
ch.name = time_name
ch.source = source_block
ch.dtype_fmt = t.dtype
ch.conversion = conversion_transfer(virtual_master_conversion, version=4)
name = time_name
gp_channels.append(ch)
gp_sdata.append(None)
self.channels_db.add(name, (dg_cntr, ch_cntr))
self.masters_db[dg_cntr] = 0
# time channel doesn't have channel dependencies
gp_dep.append(None)
ch_cntr += 1
gp_sig_types.append((v4c.SIGNAL_TYPE_VIRTUAL, 0))
else:
t_type, t_size = fmt_to_datatype_v4(t.dtype, t.shape)
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_MASTER,
"data_type": t_type,
"sync_type": sync_type,
"byte_offset": 0,
"bit_offset": 0,
"bit_count": t_size,
}
ch = Channel(**cn_kwargs)
ch.unit = time_unit
ch.name = time_name
ch.source = source_block
ch.dtype_fmt = t.dtype
name = time_name
gp_channels.append(ch)
gp_sdata.append(None)
self.channels_db.add(name, (dg_cntr, ch_cntr))
self.masters_db[dg_cntr] = 0
record.append(
(
t.dtype,
t.dtype.itemsize,
0,
0,
)
)
# time channel doesn't have channel dependencies
gp_dep.append(None)
if not t.flags["C_CONTIGUOUS"]:
t = np.ascontiguousarray(t)
fields.append((t, t.itemsize))
offset += t_size // 8
ch_cntr += 1
gp_sig_types.append((v4c.SIGNAL_TYPE_SCALAR, t_size // 8))
for signal in signals:
sig = signal
samples = sig.samples
sig_dtype = samples.dtype
sig_shape = samples.shape
names = sig_dtype.names
name = signal.name
dt_fields = samples.dtype.fields
if names is None:
if supports_virtual_channels and sig.flags & sig.Flags.virtual:
sig_type = v4c.SIGNAL_TYPE_VIRTUAL
else:
sig_type = v4c.SIGNAL_TYPE_SCALAR
if sig_dtype.kind in "SV":
sig_type = v4c.SIGNAL_TYPE_STRING
else:
if names in (v4c.CANOPEN_TIME_FIELDS, v4c.CANOPEN_DATE_FIELDS):
sig_type = v4c.SIGNAL_TYPE_CANOPEN
elif sig.name not in names:
sig_type = v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION
else:
sig_type = v4c.SIGNAL_TYPE_ARRAY
# first add the signals in the simple signal list
if sig_type == v4c.SIGNAL_TYPE_SCALAR:
# compute additional byte offset for large records size
s_type, s_size = fmt_to_datatype_v4(sig_dtype, sig_shape)
if (s_type, s_size) == (v4c.DATA_TYPE_BYTEARRAY, 0):
offsets = typing.cast(NDArray[np.uint64], arange(len(samples), dtype=uint64) * (sig_shape[1] + 4))
values = [
full(len(samples), sig_shape[1], dtype=uint32),
samples,
]
types_ = [("o", uint32), ("s", sig_dtype, sig_shape[1:])]
records = np.rec.fromarrays(values, dtype=types_)
data_size = len(records) * records.itemsize
if data_size:
data_addr = tell()
info = SignalDataBlockInfo(
address=data_addr,
compressed_size=data_size,
original_size=data_size,
location=v4c.LOCATION_TEMPORARY_FILE,
)
gp_sdata.append(
(
[info],
iter(EMPTY_TUPLE),
)
)
records.tofile(file)
else:
data_addr = 0
gp_sdata.append(
(
[],
iter(EMPTY_TUPLE),
)
)
byte_size = 8
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VLSD,
"bit_count": 64,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if invalidation_bytes_nr and signal.invalidation_bits is not None:
if (origin := signal.invalidation_bits.origin) == InvalidationArray.ORIGIN_UNKNOWN:
invalidation_arrays = typing.cast(list[InvalidationArray], inval_bits[origin])
invalidation_arrays.append(signal.invalidation_bits)
else:
inval_bits[origin] = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = origin[1]
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
ch.dtype_fmt = np.dtype("<u8")
# conversions for channel
conversion = conversion_transfer(signal.conversion, version=4)
ch.conversion = conversion
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
gp_channels.append(ch)
record.append(
(
np.dtype(uint64),
8,
offset,
0,
)
)
gp_sig_types.append((sig_type, 8))
offset += byte_size
entry = (dg_cntr, ch_cntr)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
if not offsets.flags["C_CONTIGUOUS"]:
offsets = np.ascontiguousarray(offsets)
fields.append((offsets, 8))
ch_cntr += 1
# simple channels don't have channel dependencies
gp_dep.append(None)
else:
byte_size = s_size // 8 or 1
data_block_addr = 0
gp_sig_types.append((sig_type, byte_size))
if sig_dtype.kind == "u" and signal.bit_count <= 4:
s_size = signal.bit_count
if signal.flags & signal.Flags.stream_sync:
channel_type = v4c.CHANNEL_TYPE_SYNC
if signal.attachment:
at_data, at_name, hash_sum, *_ = typing.cast(tuple[bytes, Path, bytes], signal.attachment)
attachment_index = self.attach(
at_data,
at_name,
hash_sum,
mime="video/avi",
embedded=False,
)
attachment = attachment_index
else:
attachment = None
sync_type = v4c.SYNC_TYPE_TIME
else:
channel_type = v4c.CHANNEL_TYPE_VALUE
sync_type = v4c.SYNC_TYPE_NONE
match signal.attachment:
case at_data, at_name, hash_sum, compression_type:
attachment_index = self.attach(
at_data, at_name, hash_sum, compression_type=compression_type
)
attachment = attachment_index
case at_data, at_name, hash_sum:
attachment_index = self.attach(at_data, at_name, hash_sum)
attachment = attachment_index
case None:
attachment = None
cn_kwargs = {
"channel_type": channel_type,
"sync_type": sync_type,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if attachment is not None:
cn_kwargs["attachment_addr"] = 0
if invalidation_bytes_nr and signal.invalidation_bits is not None:
if (origin := signal.invalidation_bits.origin) == InvalidationArray.ORIGIN_UNKNOWN:
invalidation_arrays = typing.cast(list[InvalidationArray], inval_bits[origin])
invalidation_arrays.append(signal.invalidation_bits)
else:
inval_bits[origin] = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = origin[1]
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
if len(sig_shape) > 1:
ch.dtype_fmt = np.dtype((sig_dtype, sig_shape[1:]))
else:
ch.dtype_fmt = sig_dtype
ch.attachment = attachment
# conversions for channel
ch.conversion = conversion_transfer(signal.conversion, version=4)
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
gp_channels.append(ch)
record.append(
(
ch.dtype_fmt,
ch.dtype_fmt.itemsize,
offset,
0,
)
)
offset += byte_size
if not samples.flags["C_CONTIGUOUS"]:
samples = np.ascontiguousarray(samples)
fields.append((samples, byte_size))
gp_sdata.append(None)
entry = (dg_cntr, ch_cntr)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
ch_cntr += 1
# simple channels don't have channel dependencies
gp_dep.append(None)
elif sig_type == v4c.SIGNAL_TYPE_VIRTUAL:
channel_type = v4c.CHANNEL_TYPE_VIRTUAL
sync_type = v4c.SYNC_TYPE_NONE
cn_kwargs = {
"channel_type": channel_type,
"sync_type": sync_type,
"bit_count": 0,
"byte_offset": offset,
"bit_offset": 0,
"data_type": v4c.DATA_TYPE_UNSIGNED_INTEL,
"flags": 0,
}
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
# conversions for channel
ch.conversion = conversion_transfer(signal.virtual_conversion, version=4)
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
gp_channels.append(ch)
gp_sig_types.append((sig_type, 0))
gp_sdata.append(None)
entry = (dg_cntr, ch_cntr)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
ch_cntr += 1
# virtual channels don't have channel dependencies
gp_dep.append(None)
elif sig_type == v4c.SIGNAL_TYPE_CANOPEN:
vals: bytes | NDArray[Any]
if names == v4c.CANOPEN_TIME_FIELDS:
record.append(
(
np.dtype("V6"),
6,
offset,
0,
)
)
if not signal.samples.flags["C_CONTIGUOUS"]:
vals = np.ascontiguousarray(signal.samples)
else:
vals = signal.samples
fields.append((vals, 6))
byte_size = 6
s_type = v4c.DATA_TYPE_CANOPEN_TIME
s_dtype = np.void(6)
gp_sig_types.append((sig_type, 6))
else:
record.append(
(
np.dtype("V7"),
7,
offset,
0,
)
)
arrays: list[NDArray[Any]] = []
for field in ("ms", "min", "hour", "day", "month", "year"):
if field == "hour":
arrays.append(signal.samples[field] + (signal.samples["summer_time"] << 7))
elif field == "day":
arrays.append(signal.samples[field] + (signal.samples["day_of_week"] << 4))
else:
arrays.append(signal.samples[field])
vals = np.rec.fromarrays(arrays)
if not vals.flags["C_CONTIGUOUS"]:
vals = np.ascontiguousarray(vals)
fields.append((vals, 7))
byte_size = 7
s_type = v4c.DATA_TYPE_CANOPEN_DATE
s_dtype = np.void(7)
gp_sig_types.append((sig_type, 7))
s_size = byte_size * 8
# there is no channel dependency
gp_dep.append(None)
# add channel block
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if invalidation_bytes_nr and signal.invalidation_bits is not None:
if (origin := signal.invalidation_bits.origin) == InvalidationArray.ORIGIN_UNKNOWN:
invalidation_arrays = typing.cast(list[InvalidationArray], inval_bits[origin])
invalidation_arrays.append(signal.invalidation_bits)
else:
inval_bits[origin] = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = origin[1]
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
ch.dtype_fmt = np.dtype(s_dtype)
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
gp_channels.append(ch)
offset += byte_size
entry = (dg_cntr, ch_cntr)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
gp_sdata.append(None)
ch_cntr += 1
elif sig_type == v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION:
(
offset,
dg_cntr,
ch_cntr,
struct_self,
new_fields,
) = self._append_structure_composition(
gp,
signal,
offset,
dg_cntr,
ch_cntr,
defined_texts,
invalidation_bytes_nr,
inval_bits,
)
fields.extend(new_fields)
gp_sig_types.append((sig_type, signal.samples.dtype.itemsize))
elif sig_type == v4c.SIGNAL_TYPE_ARRAY:
if names is None:
raise RuntimeError("'names' is None")
array_name = signal.name
samples = signal.samples[array_name]
shape = samples.shape[1:]
dt_fields = [(name, *val) for name, val in signal.samples.dtype.fields.items()]
dt_fields.sort(key=lambda x: x[-1]) # sort by offset
array_dtype = signal.samples.dtype
dims_nr = len(shape)
names_nr = len(names)
metadata = array_dtype[array_name].metadata or array_dtype[array_name].base.metadata or {}
if "axes" in metadata:
array_axes = metadata["axes"]
else:
if len(names) == 1:
array_axes = [{"type": "NO_AXIS", "conversion": None, "size": size} for size in shape[::-1]]
else:
names = [name for name in names if name != array_name][::-1]
array_axes = [
{
"type": "REF_AXIS",
"conversion": None,
"size": size,
"dtype_field_name": name,
}
for size, name in zip(shape[::-1], names, strict=False)
]
# A2l axes are defined in the XYZ oreder
# MDF axes are stored in ZYX order
array_axes = array_axes[::-1]
ref_names = {ax.get("dtype_field_name", ""): i for i, ax in enumerate(array_axes)}
if len(names) == 1 and all(axis["type"] == "NO_AXIS" for axis in array_axes):
# add channel dependency block for composed parent channel
ca_kwargs = {
"dims": dims_nr,
"ca_type": v4c.CA_TYPE_ARRAY,
"flags": 0,
"byte_offset_base": samples.dtype.itemsize,
}
for i, size in enumerate(shape):
ca_kwargs[f"dim_size_{i}"] = size # type: ignore[literal-required]
parent_deps = [ChannelArrayBlock(**ca_kwargs)]
elif len(array_axes) == 1 and array_axes[0]["type"] == "SCALE_AXIS":
ca_kwargs = {
"dims": 1,
"ca_type": v4c.CA_TYPE_SCALE_AXIS,
"flags": 0,
"byte_offset_base": samples.dtype.itemsize,
"dim_size_0": shape[0],
}
parent_deps = [ChannelArrayBlock(**ca_kwargs)]
else:
parent_dep_with_refs = None
if all(axis["type"] == "REF_AXIS" for axis in array_axes):
ca_kwargs = {
"dims": dims_nr,
"ca_type": v4c.CA_TYPE_LOOKUP,
"flags": v4c.FLAG_CA_AXIS,
"byte_offset_base": samples.dtype.itemsize,
}
for i, size in enumerate(shape):
conv = array_axes[i]["conversion"]
if isinstance(conv, dict):
conv = from_dict(conv)
ca_kwargs[f"axis_conversion_{i}"] = conv
ca_kwargs[f"dim_size_{i}"] = size # type: ignore[literal-required]
parent_dep_with_refs = ChannelArrayBlock(**ca_kwargs)
parent_deps = [parent_dep_with_refs]
elif all(axis["type"] == "FIXED_AXIS" for axis in array_axes):
ca_kwargs = {
"dims": dims_nr,
"ca_type": v4c.CA_TYPE_LOOKUP,
"flags": v4c.FLAG_CA_AXIS | v4c.FLAG_CA_FIXED_AXIS,
"byte_offset_base": samples.dtype.itemsize,
}
for i, size in enumerate(shape):
conv = array_axes[i]["conversion"]
if isinstance(conv, dict):
conv = from_dict(conv)
ca_kwargs[f"axis_conversion_{i}"] = conv
ca_kwargs[f"dim_size_{i}"] = size # type: ignore[literal-required]
for j in range(size):
ca_kwargs[f"axis_{i}_value_{j}"] = array_axes[i]["values"][j]
parent_dep_with_refs = None
parent_deps = [ChannelArrayBlock(**ca_kwargs)]
else:
parent_deps = []
byte_offset_base = samples.dtype.itemsize
for i, (axis, shp) in enumerate(zip(array_axes, shape, strict=False)):
conv = axis["conversion"]
if isinstance(conv, dict):
conv = from_dict(conv)
if axis["type"] == "REF_AXIS":
ca_kwargs = {
"dims": 1,
"ca_type": v4c.CA_TYPE_LOOKUP,
"flags": v4c.FLAG_CA_AXIS,
"byte_offset_base": byte_offset_base,
"axis_conversion_0": conv,
"dim_size_0": shp,
}
parent_dep_with_refs = dep = ChannelArrayBlock(**ca_kwargs)
else:
ca_kwargs = {
"dims": 1,
"ca_type": v4c.CA_TYPE_LOOKUP,
"flags": v4c.FLAG_CA_AXIS | v4c.FLAG_CA_FIXED_AXIS,
"byte_offset_base": byte_offset_base,
"axis_conversion_0": conv,
"dim_size_0": shp,
}
for j in range(shp):
ca_kwargs[f"axis_0_value_{j}"] = axis["values"][j]
dep = ChannelArrayBlock(**ca_kwargs)
byte_offset_base *= shp
parent_deps.insert(0, dep)
current_dt_offset = 0
for name, dt_dtype, dt_offset in dt_fields:
samples = signal.samples[name]
shape = samples.shape[1:]
if delta := (dt_offset - current_dt_offset):
padding = np.zeros((samples.shape[0], delta), dtype="u1")
fields.append((padding, delta))
offset += delta
current_dt_offset = dt_offset + dt_dtype.itemsize
if name == array_name:
metadata = array_dtype[name].metadata or array_dtype[name].base.metadata or {}
conversion = signal.conversion or metadata.get("conversion", None)
if isinstance(conversion, dict):
conversion = from_dict(conversion)
gp_dep.append(parent_deps)
# first we add the structure channel
s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape, True)
# add channel block
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if invalidation_bytes_nr and signal.invalidation_bits is not None:
if (origin := signal.invalidation_bits.origin) == InvalidationArray.ORIGIN_UNKNOWN:
invalidation_arrays = typing.cast(list[InvalidationArray], inval_bits[origin])
invalidation_arrays.append(signal.invalidation_bits)
else:
inval_bits[origin] = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = origin[1]
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
ch.dtype_fmt = samples.dtype
ch.conversion = conversion
record.append(
(
samples.dtype,
samples.dtype.itemsize,
offset,
0,
)
)
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
gp_channels.append(ch)
size = s_size // 8
for dim in shape:
size *= dim
offset += size
if not samples.flags["C_CONTIGUOUS"]:
samples = np.ascontiguousarray(samples)
fields.append((samples, size))
gp_sig_types.append((sig_type, size))
gp_sdata.append(None)
entry = (dg_cntr, ch_cntr)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
ch_cntr += 1
elif name not in ref_names:
itemsize = samples.dtype.itemsize
fields.append((samples, itemsize))
offset += itemsize
else:
metadata = array_dtype[name].metadata or array_dtype[name].base.metadata or {}
idx = ref_names[name]
axis = array_axes[idx]
if axis["type"] == "FIXED_AXIS":
itemsize = samples.dtype.itemsize
fields.append((samples, itemsize))
offset += itemsize
else:
# add channel dependency block
ca_kwargs = {
"dims": 1,
"ca_type": v4c.CA_TYPE_SCALE_AXIS,
"flags": 0,
"byte_offset_base": samples.dtype.itemsize,
"dim_size_0": shape[0],
}
dep = ChannelArrayBlock(**ca_kwargs)
gp_dep.append([dep])
# add components channel
s_type, s_size = fmt_to_datatype_v4(samples.dtype, ())
byte_size = s_size // 8 or 1
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if invalidation_bytes_nr and signal.invalidation_bits is not None:
if (origin := signal.invalidation_bits.origin) == InvalidationArray.ORIGIN_UNKNOWN:
invalidation_arrays = typing.cast(list[InvalidationArray], inval_bits[origin])
invalidation_arrays.append(signal.invalidation_bits)
else:
inval_bits[origin] = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = origin[1]
ch = Channel(**cn_kwargs)
ch.name = axis.get("ref_name", "") or name
ch.unit = metadata.get("unit", "")
ch.comment = f"{array_name} axis {idx}"
ch.dtype_fmt = samples.dtype
record.append(
(
samples.dtype,
samples.dtype.itemsize,
offset,
0,
)
)
gp_channels.append(ch)
entry = dg_cntr, ch_cntr
parent_dep_with_refs.axis_channels.append(entry)
for dim in shape:
byte_size *= dim
offset += byte_size
if not samples.flags["C_CONTIGUOUS"]:
samples = np.ascontiguousarray(samples)
fields.append((samples, byte_size))
gp_sdata.append(None)
self.channels_db.add(name, entry)
ch_cntr += 1
if delta := (signal.samples.dtype.itemsize - current_dt_offset):
padding = np.zeros((signal.samples.shape[0], delta), dtype="u1")
fields.append((padding, delta))
else:
encoding = signal.encoding
samples = signal.samples
sig_dtype = samples.dtype
match encoding:
case "utf-8":
data_type = v4c.DATA_TYPE_STRING_UTF_8
case "latin-1":
data_type = v4c.DATA_TYPE_STRING_LATIN_1
case "utf-16-be":
data_type = v4c.DATA_TYPE_STRING_UTF_16_BE
case "utf-16-le":
data_type = v4c.DATA_TYPE_STRING_UTF_16_LE
case _:
raise MdfException(f'wrong encoding "{encoding}" for string signal')
if self.compact_vlsd:
buffers: list[bytes] = []
offsets_list: list[int] = []
off = 0
if encoding == "utf-16-le":
for elem in samples:
offsets_list.append(off)
size = len(elem)
if size % 2:
size += 1
elem = elem + b"\0"
buffers.append(UINT32_p(size))
buffers.append(elem)
off += size + 4
else:
for elem in samples:
offsets_list.append(off)
size = len(elem)
buffers.append(UINT32_p(size))
buffers.append(elem)
off += size + 4
data_size = off
offsets = array(offsets_list, dtype=uint64)
if data_size:
data_addr = tell()
info = SignalDataBlockInfo(
address=data_addr,
compressed_size=data_size,
original_size=data_size,
location=v4c.LOCATION_TEMPORARY_FILE,
)
gp_sdata.append(
(
[info],
iter(EMPTY_TUPLE),
)
)
file.seek(0, 2)
file.write(b"".join(buffers))
else:
data_addr = 0
gp_sdata.append(
(
[],
iter(EMPTY_TUPLE),
)
)
else:
offsets = typing.cast(
NDArray[np.uint64], arange(len(samples), dtype=uint64) * (signal.samples.itemsize + 4)
)
values = [
full(len(samples), samples.itemsize, dtype=uint32),
samples,
]
types_ = [("o", uint32), ("s", sig_dtype)]
records = np.rec.fromarrays(values, dtype=types_)
data_size = len(records) * records.itemsize
if data_size:
data_addr = tell()
info = SignalDataBlockInfo(
address=data_addr,
compressed_size=data_size,
original_size=data_size,
location=v4c.LOCATION_TEMPORARY_FILE,
)
gp_sdata.append(
(
[info],
iter(EMPTY_TUPLE),
)
)
records.tofile(file)
else:
data_addr = 0
gp_sdata.append(
(
[],
iter(EMPTY_TUPLE),
)
)
# compute additional byte offset for large records size
byte_size = 8
gp_sig_types.append((sig_type, 8))
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VLSD,
"bit_count": 64,
"byte_offset": offset,
"bit_offset": 0,
"data_type": data_type,
"flags": 0,
}
if invalidation_bytes_nr:
if signal.invalidation_bits is not None:
if (origin := signal.invalidation_bits.origin) == InvalidationArray.ORIGIN_UNKNOWN:
invalidation_arrays = typing.cast(list[InvalidationArray], inval_bits[origin])
invalidation_arrays.append(signal.invalidation_bits)
else:
inval_bits[origin] = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = origin[1]
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
ch.dtype_fmt = np.dtype("<u8")
# conversions for channel
conversion = conversion_transfer(signal.conversion, version=4)
ch.conversion = conversion
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
gp_channels.append(ch)
record.append(
(
np.dtype(uint64),
8,
offset,
0,
)
)
offset += byte_size
entry = (dg_cntr, ch_cntr)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
if not offsets.flags["C_CONTIGUOUS"]:
offsets = np.ascontiguousarray(offsets)
fields.append((offsets, 8))
ch_cntr += 1
# simple channels don't have channel dependencies
gp_dep.append(None)
if invalidation_bytes_nr:
unknown_origin = typing.cast(list[InvalidationArray], inval_bits.pop(InvalidationArray.ORIGIN_UNKNOWN))
inval_arrays = typing.cast(dict[tuple[int, int], InvalidationArray], inval_bits)
_pos_map = {key: idx for idx, (_, key) in enumerate(inval_arrays)}
_unknown_pos_map = deque(list(range(len(inval_arrays), len(inval_arrays) + len(unknown_origin))))
invalidation_bits_list = typing.cast(list[NDArray[np.bool]], list(inval_arrays.values()) + unknown_origin)
invalidation_bytes_nr = len(invalidation_bits_list)
for _ in range(8 - invalidation_bytes_nr % 8):
invalidation_bits_list.append(zeros(cycles_nr, dtype=bool))
invalidation_bits_list.reverse()
invalidation_bytes_nr = len(invalidation_bits_list) // 8
gp.channel_group.invalidation_bytes_nr = invalidation_bytes_nr
bytes_array = np.fliplr(
np.packbits(array(invalidation_bits_list).T).reshape((cycles_nr, invalidation_bytes_nr))
).ravel()
if not self._use_ld_blocks:
fields.append((bytes_array, invalidation_bytes_nr))
for ch in gp.channels:
if ch.flags & v4c.FLAG_CN_INVALIDATION_PRESENT:
if (pos_invalidation_bit := ch.pos_invalidation_bit) == InvalidationArray.ORIGIN_UNKNOWN[1]:
ch.pos_invalidation_bit = _unknown_pos_map.popleft()
else:
ch.pos_invalidation_bit = _pos_map[pos_invalidation_bit]
else:
bytes_array = None
gp.channel_group.cycles_nr = cycles_nr
gp.channel_group.samples_byte_nr = offset
virtual_group = VirtualChannelGroup()
self.virtual_groups[dg_cntr] = virtual_group
self.virtual_groups_map[dg_cntr] = dg_cntr
virtual_group.groups.append(dg_cntr)
virtual_group.record_size = offset + invalidation_bytes_nr
virtual_group.cycles_nr = cycles_nr
# data group
gp.sorted = True
data_block = data_block_from_arrays(fields, cycles_nr, THREAD_COUNT)
size = len(data_block)
data_block_view = memoryview(data_block)
del fields
record_size = offset + invalidation_bytes_nr
if size:
if not self._use_ld_blocks:
block_size = 32 * 1024 * 1024 // record_size * record_size
count = ceil(size / block_size)
for i in range(count):
data_ = data_block_view[i * block_size : (i + 1) * block_size]
raw_size = len(data_)
data_ = lz_compress(data_, store_size=True)
size = len(data_)
data_address = self._tempfile.tell()
self._tempfile.write(data_)
gp.data_blocks.append(
DataBlockInfo(
address=data_address,
block_type=v4c.DZ_BLOCK_LZ,
original_size=raw_size,
compressed_size=size,
param=0,
location=v4c.LOCATION_TEMPORARY_FILE,
)
)
else:
gp.uses_ld = True
data_address = tell()
raw_size = len(data_block_view)
data = lz_compress(data_block_view, store_size=True)
size = len(data)
self._tempfile.write(data)
gp.data_blocks.append(
DataBlockInfo(
address=data_address,
block_type=v4c.DZ_BLOCK_LZ,
original_size=raw_size,
compressed_size=size,
param=0,
location=v4c.LOCATION_TEMPORARY_FILE,
)
)
if bytes_array is not None:
addr = tell()
data = bytes_array.tobytes()
raw_size = len(data)
data = lz_compress(data, store_size=True)
size = len(data)
self._tempfile.write(data)
gp.data_blocks[-1].invalidation_block = InvalidationBlockInfo(
address=addr,
block_type=v4c.DZ_BLOCK_LZ,
original_size=raw_size,
compressed_size=size,
param=None,
)
gp.data_location = v4c.LOCATION_TEMPORARY_FILE
return dg_cntr
def _append_column_oriented(
self,
signals: list[Signal],
acq_name: str | None = None,
acq_source: SourceInformation | None = None,
comment: str = "",
) -> int:
defined_texts: dict[str, int] = {}
si_map = self._si_map
# setup all blocks related to the time master channel
file = self._tempfile
tell = file.tell
seek = file.seek
write = file.write
seek(0, 2)
dg_cntr = initial_dg_cntr = len(self.groups)
# add the master group
gp = Group(DataGroup())
gp_sdata = gp.signal_data = []
gp_channels = gp.channels = []
gp_dep = gp.channel_dependencies = []
gp_sig_types: list[int] = []
gp.signal_types = gp_sig_types
gp.uses_ld = True
gp.sorted = True
record = gp.record = []
timestamps = signals[0].timestamps
cycles_nr = len(timestamps)
# channel group
cg_kwargs: ChannelGroupKwargs = {"cycles_nr": cycles_nr, "samples_byte_nr": 0}
gp.channel_group = remote_master_channel_group = ChannelGroup(**cg_kwargs)
gp.channel_group.acq_name = acq_name
gp.channel_group.acq_source = acq_source
gp.channel_group.comment = comment
self.groups.append(gp)
ch_cntr = 0
types: list[tuple[str, np.dtype[Any], tuple[int, ...]] | DTypeLike] = []
ch_cntr = 0
offset = 0
source_block = acq_source
master_metadata = signals[0].master_metadata
if master_metadata:
time_name, sync_type = master_metadata
match sync_type:
case 0 | 1:
time_unit = "s"
case 2:
time_unit = "deg"
case 3:
time_unit = "m"
case 4:
time_unit = "index"
case _:
raise RuntimeError(f"unexpected sync_type '{sync_type}'")
else:
time_name, sync_type = "time", v4c.SYNC_TYPE_TIME
time_unit = "s"
gp.channel_group.acq_source = source_block
# time channel
t_type, t_size = fmt_to_datatype_v4(timestamps.dtype, timestamps.shape)
cn_kwargs: ChannelKwargs = {
"channel_type": v4c.CHANNEL_TYPE_MASTER,
"data_type": t_type,
"sync_type": sync_type,
"byte_offset": 0,
"bit_offset": 0,
"bit_count": t_size,
}
ch = Channel(**cn_kwargs)
ch.unit = time_unit
ch.name = time_name
ch.source = source_block
ch.dtype_fmt = timestamps.dtype
name = time_name
gp_channels.append(ch)
gp_sdata.append(None)
self.channels_db.add(name, (dg_cntr, ch_cntr))
self.masters_db[dg_cntr] = 0
record.append(
(
timestamps.dtype,
timestamps.dtype.itemsize,
offset,
0,
)
)
# time channel doesn't have channel dependencies
gp_dep.append(None)
types.append((name, timestamps.dtype))
offset += t_size // 8
ch_cntr += 1
gp_sig_types.append(0)
gp.channel_group.samples_byte_nr = offset
# data block
gp.sorted = True
size = cycles_nr * timestamps.itemsize
cg_master_index = dg_cntr
virtual_group = VirtualChannelGroup()
self.virtual_groups[cg_master_index] = virtual_group
self.virtual_groups_map[dg_cntr] = dg_cntr
virtual_group.groups.append(dg_cntr)
virtual_group.record_size = offset
virtual_group.cycles_nr = cycles_nr
dg_cntr += 1
if size:
data_address = tell()
gp.data_location = v4c.LOCATION_TEMPORARY_FILE
write(timestamps.tobytes())
chunk = self._write_fragment_size // timestamps.itemsize
chunk *= timestamps.itemsize
while size:
if size > chunk:
gp.data_blocks.append(
DataBlockInfo(
address=data_address,
block_type=v4c.DT_BLOCK,
original_size=chunk,
compressed_size=chunk,
param=0,
location=v4c.LOCATION_TEMPORARY_FILE,
)
)
data_address += chunk
size -= chunk
else:
gp.data_blocks.append(
DataBlockInfo(
address=data_address,
block_type=v4c.DT_BLOCK,
original_size=size,
compressed_size=size,
param=0,
location=v4c.LOCATION_TEMPORARY_FILE,
)
)
size = 0
else:
gp.data_location = v4c.LOCATION_TEMPORARY_FILE
for signal in signals:
gp = Group(DataGroup())
gp_sdata = gp.signal_data = []
gp_channels = gp.channels = []
gp_dep = gp.channel_dependencies = []
gp.signal_types = gp_sig_types = []
gp.sorted = True
gp.uses_ld = True
record = gp.record = []
# channel group
cg_kwargs = {
"cycles_nr": cycles_nr,
"samples_byte_nr": 0,
"flags": v4c.FLAG_CG_REMOTE_MASTER,
}
gp.channel_group = ChannelGroup(**cg_kwargs)
gp.channel_group.acq_name = acq_name
gp.channel_group.acq_source = acq_source
gp.channel_group.comment = remote_master_channel_group.comment
gp.channel_group.cg_master_index = cg_master_index
self.groups.append(gp)
types = []
ch_cntr = 0
offset = 0
field_names = UniqueDB()
sig = signal
samples = sig.samples
sig_dtype = samples.dtype
sig_shape = samples.shape
names = sig_dtype.names
name = signal.name
if names is None:
sig_type = v4c.SIGNAL_TYPE_SCALAR
if sig_dtype.kind in "SV":
sig_type = v4c.SIGNAL_TYPE_STRING
else:
if names in (v4c.CANOPEN_TIME_FIELDS, v4c.CANOPEN_DATE_FIELDS):
sig_type = v4c.SIGNAL_TYPE_CANOPEN
elif sig.name not in names:
sig_type = v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION
else:
sig_type = v4c.SIGNAL_TYPE_ARRAY
gp_sig_types.append(sig_type)
axes = signal.axes or {}
conversions = signal.conversions or {}
units = signal.units or {}
# first add the signals in the simple signal list
if sig_type == v4c.SIGNAL_TYPE_SCALAR:
# compute additional byte offset for large records size
s_type, s_size = fmt_to_datatype_v4(sig_dtype, sig_shape)
byte_size = s_size // 8 or 1
if sig_dtype.kind == "u" and signal.bit_count <= 4:
s_size = signal.bit_count
if signal.flags & signal.Flags.stream_sync:
channel_type = v4c.CHANNEL_TYPE_SYNC
if signal.attachment:
at_data, at_name, hash_sum, *_ = typing.cast(tuple[bytes, Path, bytes], signal.attachment)
attachment_addr = self.attach(at_data, at_name, hash_sum, mime="video/avi", embedded=False)
data_block_addr = attachment_addr
else:
data_block_addr = 0
sync_type = v4c.SYNC_TYPE_TIME
else:
channel_type = v4c.CHANNEL_TYPE_VALUE
data_block_addr = 0
sync_type = v4c.SYNC_TYPE_NONE
cn_kwargs = {
"channel_type": channel_type,
"sync_type": sync_type,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if signal.invalidation_bits is not None:
invalidation_bits = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = 0
else:
invalidation_bits = None
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
# conversions for channel
ch.conversion = conversion_transfer(signal.conversion, version=4)
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
gp_channels.append(ch)
gp_sdata.append(None)
entry = (dg_cntr, ch_cntr)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
_shape = sig_shape[1:]
types.append((name, sig_dtype, _shape))
gp.single_channel_dtype = ch.dtype_fmt = np.dtype((sig_dtype, _shape))
record.append(
(
ch.dtype_fmt,
ch.dtype_fmt.itemsize,
0,
0,
)
)
offset = byte_size
# simple channels don't have channel dependencies
gp_dep.append(None)
elif sig_type == v4c.SIGNAL_TYPE_CANOPEN:
if names == v4c.CANOPEN_TIME_FIELDS:
record.append(
(
np.dtype("V6"),
6,
0,
0,
)
)
types.append((name, "V6"))
gp.single_channel_dtype = np.dtype("V6")
byte_size = 6
s_type = v4c.DATA_TYPE_CANOPEN_TIME
else:
record.append(
(
np.dtype("V7"),
7,
0,
0,
)
)
vals: list[Any] = []
for field in ("ms", "min", "hour", "day", "month", "year"):
if field == "hour":
vals.append(signal.samples[field] + (signal.samples["summer_time"] << 7))
elif field == "day":
vals.append(signal.samples[field] + (signal.samples["day_of_week"] << 4))
else:
vals.append(signal.samples[field])
samples = np.rec.fromarrays(vals)
types.append((name, "V7"))
gp.single_channel_dtype = np.dtype("V7")
byte_size = 7
s_type = v4c.DATA_TYPE_CANOPEN_DATE
s_size = byte_size * 8
# there is no channel dependency
gp_dep.append(None)
# add channel block
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if signal.invalidation_bits is not None:
invalidation_bits = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = 0
else:
invalidation_bits = None
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
ch.dtype_fmt = gp.single_channel_dtype
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
gp_channels.append(ch)
offset = byte_size
entry = (dg_cntr, ch_cntr)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
gp_sdata.append(None)
elif sig_type == v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION:
(
offset,
dg_cntr,
ch_cntr,
struct_self,
new_fields,
new_types,
) = self._append_structure_composition_column_oriented(
gp,
signal,
field_names,
offset,
dg_cntr,
ch_cntr,
defined_texts,
)
if signal.invalidation_bits is not None:
invalidation_bits = signal.invalidation_bits
else:
invalidation_bits = None
gp.signal_types = np.dtype(new_types)
offset = gp.signal_types.itemsize
samples = signal.samples
elif sig_type == v4c.SIGNAL_TYPE_ARRAY:
fields: list[NDArray[Any]] = []
if names is None:
raise RuntimeError("'names' is None")
array_name = signal.name
# here we have channel arrays or mdf v3 channel dependencies
samples = signal.samples[array_name]
shape = samples.shape[1:]
if len(names) > 1 or len(shape) > 1:
# add channel dependency block for composed parent channel
dims_nr = len(shape)
names_nr = len(names)
if names_nr == 0:
ca_kwargs: ChannelArrayBlockKwargs = {
"dims": dims_nr,
"ca_type": v4c.CA_TYPE_LOOKUP,
"flags": v4c.FLAG_CA_FIXED_AXIS,
"byte_offset_base": samples.dtype.itemsize,
}
for i in range(dims_nr):
ca_kwargs[f"dim_size_{i}"] = shape[i] # type: ignore[literal-required]
elif len(names) == 1:
ca_kwargs = {
"dims": dims_nr,
"ca_type": v4c.CA_TYPE_ARRAY,
"flags": 0,
"byte_offset_base": samples.dtype.itemsize,
}
for i in range(dims_nr):
ca_kwargs[f"dim_size_{i}"] = shape[i] # type: ignore[literal-required]
else:
ca_kwargs = {
"dims": dims_nr,
"ca_type": v4c.CA_TYPE_LOOKUP,
"flags": v4c.FLAG_CA_AXIS,
"byte_offset_base": samples.dtype.itemsize,
}
for i in range(dims_nr):
ca_kwargs[f"dim_size_{i}"] = shape[i] # type: ignore[literal-required]
parent_dep = ChannelArrayBlock(**ca_kwargs)
else:
# add channel dependency block for composed parent channel
ca_kwargs = {
"dims": 1,
"ca_type": v4c.CA_TYPE_SCALE_AXIS,
"flags": 0,
"byte_offset_base": samples.dtype.itemsize,
"dim_size_0": shape[0],
}
parent_dep = ChannelArrayBlock(**ca_kwargs)
for name in names:
samples = signal[name]
shape = samples.shape[1:]
if name == array_name:
gp_dep.append([parent_dep])
field_name = field_names.get_unique_name(name)
if not samples.flags["C_CONTIGUOUS"]:
samples = np.ascontiguousarray(samples)
fields.append(samples)
dtype_pair = field_name, samples.dtype, shape
types.append(dtype_pair)
record.append(
(
samples.dtype,
samples.dtype.itemsize,
offset,
0,
)
)
# first we add the structure channel
s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape, True)
# add channel block
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if signal.invalidation_bits is not None:
invalidation_bits = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = 0
else:
invalidation_bits = None
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
ch.dtype_fmt = samples.dtype
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
gp_channels.append(ch)
size = s_size // 8
for dim in shape:
size *= dim
offset += size
gp_sdata.append(None)
entry = (dg_cntr, ch_cntr)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
ch_cntr += 1
else:
field_name = field_names.get_unique_name(name)
if not samples.flags["C_CONTIGUOUS"]:
samples = np.ascontiguousarray(samples)
fields.append(samples)
types.append((field_name, samples.dtype, shape))
record.append(
(
samples.dtype,
samples.dtype.itemsize,
offset,
0,
)
)
# add channel dependency block
ca_kwargs = {
"dims": 1,
"ca_type": v4c.CA_TYPE_SCALE_AXIS,
"flags": 0,
"byte_offset_base": samples.dtype.itemsize,
"dim_size_0": shape[0],
}
dep = ChannelArrayBlock(**ca_kwargs)
gp_dep.append([dep])
# add components channel
s_type, s_size = fmt_to_datatype_v4(samples.dtype, ())
byte_size = s_size // 8 or 1
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if signal.invalidation_bits is not None:
invalidation_bits = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = 0
else:
invalidation_bits = None
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
ch.dtype_fmt = samples.dtype
gp_channels.append(ch)
entry = dg_cntr, ch_cntr
parent_dep.axis_channels.append(entry)
for dim in shape:
byte_size *= dim
offset += byte_size
gp_sdata.append(None)
self.channels_db.add(name, entry)
ch_cntr += 1
gp.signal_types = np.dtype(types)
samples = signal.samples
else:
encoding = signal.encoding
samples = signal.samples
sig_dtype = samples.dtype
match encoding:
case "utf-8":
data_type = v4c.DATA_TYPE_STRING_UTF_8
case "latin-1":
data_type = v4c.DATA_TYPE_STRING_LATIN_1
case "utf-16-be":
data_type = v4c.DATA_TYPE_STRING_UTF_16_BE
case "utf-16-le":
data_type = v4c.DATA_TYPE_STRING_UTF_16_LE
case _:
raise MdfException(f'wrong encoding "{encoding}" for string signal')
offsets = arange(len(samples), dtype=uint64) * (signal.samples.itemsize + 4)
values = [full(len(samples), samples.itemsize, dtype=uint32), samples]
types_ = [("o", uint32), ("s", sig_dtype)]
array = np.rec.fromarrays(values, dtype=types_)
data_size = len(array) * array.itemsize
if data_size:
data_addr = tell()
info = SignalDataBlockInfo(
address=data_addr,
compressed_size=data_size,
original_size=data_size,
location=v4c.LOCATION_TEMPORARY_FILE,
)
gp_sdata.append(
(
[info],
iter(EMPTY_TUPLE),
)
)
array.tofile(file)
else:
data_addr = 0
gp_sdata.append(
(
[],
iter(EMPTY_TUPLE),
)
)
# compute additional byte offset for large records size
byte_size = 8
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VLSD,
"bit_count": 64,
"byte_offset": offset,
"bit_offset": 0,
"data_type": data_type,
"flags": 0,
}
if signal.invalidation_bits is not None:
invalidation_bits = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = 0
else:
invalidation_bits = None
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
# conversions for channel
conversion = conversion_transfer(signal.conversion, version=4)
ch.conversion = conversion
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
gp_channels.append(ch)
record.append(
(
np.dtype(uint64),
8,
offset,
0,
)
)
offset = byte_size
entry = (dg_cntr, ch_cntr)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
types.append((name, uint64))
gp.single_channel_dtype = ch.dtype_fmt = np.dtype(uint64)
samples = offsets
# simple channels don't have channel dependencies
gp_dep.append(None)
gp.channel_group.samples_byte_nr = offset
if invalidation_bits is not None:
gp.channel_group.invalidation_bytes_nr = 1
virtual_group.groups.append(dg_cntr)
self.virtual_groups_map[dg_cntr] = cg_master_index
virtual_group.record_size += offset
if signal.invalidation_bits:
virtual_group.record_size += 1
dg_cntr += 1
size = cycles_nr * samples.itemsize
if size:
data_address = tell()
data = samples.tobytes()
raw_size = len(data)
data = lz_compress(data, store_size=True)
size = len(data)
write(data)
gp.data_blocks.append(
DataBlockInfo(
address=data_address,
block_type=v4c.DZ_BLOCK_LZ,
original_size=raw_size,
compressed_size=size,
param=0,
location=v4c.LOCATION_TEMPORARY_FILE,
)
)
if invalidation_bits is not None:
addr = tell()
data = invalidation_bits.tobytes()
raw_size = len(data)
data = lz_compress(data, store_size=True)
size = len(data)
write(data)
gp.data_blocks[-1].invalidation_block = InvalidationBlockInfo(
address=addr,
block_type=v4c.DZ_BLOCK_LZ,
original_size=raw_size,
compressed_size=size,
param=None,
)
gp.data_location = v4c.LOCATION_TEMPORARY_FILE
return initial_dg_cntr
def _append_dataframe(
self,
df: DataFrame,
acq_name: str | None = None,
acq_source: SourceInformation | None = None,
comment: str = "",
units: dict[str, str] | None = None,
) -> None:
"""Append a new data group from a pandas DataFrame."""
units = units or {}
if df.shape == (0, 0):
return
t: NDArray[Any] = df.index.values
index_name = df.index.name
time_name = index_name if isinstance(index_name, str) and index_name else "time"
sync_type = v4c.SYNC_TYPE_TIME
time_unit = "s"
dg_cntr = len(self.groups)
gp = Group(DataGroup())
gp_sdata = gp.signal_data = []
gp_channels = gp.channels = []
gp_dep = gp.channel_dependencies = []
gp_sig_types = gp.signal_types = []
record = gp.record = []
cycles_nr = len(t)
# channel group
cg_kwargs: ChannelGroupKwargs = {"cycles_nr": cycles_nr, "samples_byte_nr": 0}
gp.channel_group = ChannelGroup(**cg_kwargs)
gp.channel_group.acq_name = acq_name
gp.channel_group.acq_source = acq_source
gp.channel_group.comment = comment
self.groups.append(gp)
fields: list[NDArray[Any]] = []
types: list[DTypeLike | tuple[str, np.dtype[Any], tuple[int, ...]]] = []
ch_cntr = 0
offset = 0
field_names = UniqueDB()
# setup all blocks related to the time master channel
file = self._tempfile
tell = file.tell
seek = file.seek
seek(0, 2)
virtual_group = VirtualChannelGroup()
self.virtual_groups[dg_cntr] = virtual_group
self.virtual_groups_map[dg_cntr] = dg_cntr
virtual_group.groups.append(dg_cntr)
virtual_group.cycles_nr = cycles_nr
# time channel
t_type, t_size = fmt_to_datatype_v4(t.dtype, t.shape)
cn_kwargs: ChannelKwargs = {
"channel_type": v4c.CHANNEL_TYPE_MASTER,
"data_type": t_type,
"sync_type": sync_type,
"byte_offset": 0,
"bit_offset": 0,
"bit_count": t_size,
"min_raw_value": t[0] if cycles_nr else 0,
"max_raw_value": t[-1] if cycles_nr else 0,
"lower_limit": t[0] if cycles_nr else 0,
"upper_limit": t[-1] if cycles_nr else 0,
"flags": v4c.FLAG_PHY_RANGE_OK | v4c.FLAG_VAL_RANGE_OK,
}
ch = Channel(**cn_kwargs)
ch.unit = time_unit
ch.name = time_name
ch.dtype_fmt = t.dtype
name = time_name
gp_channels.append(ch)
gp_sdata.append(None)
self.channels_db.add(name, (dg_cntr, ch_cntr))
self.masters_db[dg_cntr] = 0
record.append(
(
t.dtype,
t.dtype.itemsize,
offset,
0,
)
)
# time channel doesn't have channel dependencies
gp_dep.append(None)
fields.append(t)
types.append((name, t.dtype))
field_names.get_unique_name(name)
offset += t_size // 8
ch_cntr += 1
gp_sig_types.append(0)
for signal in df.columns:
if index_name == signal:
continue
sig: Series[Any] = df[signal]
name = signal
sig_type = v4c.SIGNAL_TYPE_SCALAR
if sig.dtype.kind in "SV":
sig_type = v4c.SIGNAL_TYPE_STRING
gp_sig_types.append(sig_type)
# first add the signals in the simple signal list
if sig_type == v4c.SIGNAL_TYPE_SCALAR:
# compute additional byte offset for large records size
if sig.dtype.kind == "O":
array = encode(sig.array.astype(str), "utf-8")
else:
array = sig.to_numpy()
s_type, s_size = fmt_to_datatype_v4(array.dtype, array.shape)
byte_size = s_size // 8 or 1
channel_type = v4c.CHANNEL_TYPE_VALUE
data_block_addr = 0
sync_type = v4c.SYNC_TYPE_NONE
cn_kwargs = {
"channel_type": channel_type,
"sync_type": sync_type,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
}
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = units.get(name, "")
ch.dtype_fmt = np.dtype((array.dtype, array.shape[1:]))
record.append(
(
ch.dtype_fmt,
ch.dtype_fmt.itemsize,
offset,
0,
)
)
gp_channels.append(ch)
offset += byte_size
gp_sdata.append(None)
self.channels_db.add(name, (dg_cntr, ch_cntr))
field_name = field_names.get_unique_name(name)
fields.append(array)
types.append((field_name, array.dtype, array.shape[1:]))
ch_cntr += 1
# simple channels don't have channel dependencies
gp_dep.append(None)
elif sig_type == v4c.SIGNAL_TYPE_STRING:
array = sig.to_numpy()
offsets = arange(len(array), dtype=uint64) * (array.dtype.itemsize + 4)
values = [full(len(array), array.dtype.itemsize, dtype=uint32), array]
types_: list[DTypeLike] = [("", uint32), ("", array.dtype)]
data = np.rec.fromarrays(values, dtype=types_)
data_size = len(data) * data.itemsize
if data_size:
data_addr = tell()
info = SignalDataBlockInfo(
address=data_addr,
compressed_size=data_size,
original_size=data_size,
location=v4c.LOCATION_TEMPORARY_FILE,
)
gp_sdata.append(
(
[info],
iter(EMPTY_TUPLE),
)
)
data.tofile(file)
else:
data_addr = 0
gp_sdata.append(
(
[],
iter(EMPTY_TUPLE),
)
)
# compute additional byte offset for large records size
byte_size = 8
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VLSD,
"bit_count": 64,
"byte_offset": offset,
"bit_offset": 0,
"data_type": v4c.DATA_TYPE_STRING_UTF_8,
"min_raw_value": 0,
"max_raw_value": 0,
"lower_limit": 0,
"upper_limit": 0,
"flags": 0,
}
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = units.get(name, "")
ch.dtype_fmt = np.dtype("<u8")
gp_channels.append(ch)
record.append(
(
np.dtype(np.uint64),
8,
offset,
0,
)
)
offset += byte_size
self.channels_db.add(name, (dg_cntr, ch_cntr))
field_name = field_names.get_unique_name(name)
fields.append(offsets)
types.append((field_name, uint64))
ch_cntr += 1
# simple channels don't have channel dependencies
gp_dep.append(None)
virtual_group.record_size = offset
virtual_group.cycles_nr = cycles_nr
gp.channel_group.cycles_nr = cycles_nr
gp.channel_group.samples_byte_nr = offset
# data block
gp.sorted = True
samples: NDArray[Any]
if df.shape[0]:
samples = np.rec.fromarrays(fields, dtype=np.dtype(types))
else:
samples = np.array([])
size = len(samples) * samples.itemsize
if size:
data_address = self._tempfile.tell()
gp.data_location = v4c.LOCATION_TEMPORARY_FILE
samples.tofile(self._tempfile)
self._tempfile.write(samples.tobytes())
gp.data_blocks.append(
DataBlockInfo(
address=data_address,
block_type=v4c.DT_BLOCK,
original_size=size,
compressed_size=size,
param=0,
location=v4c.LOCATION_TEMPORARY_FILE,
)
)
else:
gp.data_location = v4c.LOCATION_TEMPORARY_FILE
def _append_structure_composition(
self,
grp: Group,
signal: Signal,
offset: int,
dg_cntr: int,
ch_cntr: int,
defined_texts: dict[str, int],
invalidation_bytes_nr: int,
inval_bits: dict[tuple[int, int], list[InvalidationArray] | InvalidationArray],
) -> tuple[int, int, int, tuple[int, int], list[tuple[NDArray[Any], int]]]:
si_map = self._si_map
fields: list[tuple[NDArray[Any], int]] = []
file = self._tempfile
seek = file.seek
seek(0, 2)
gp = grp
gp_sdata = gp.signal_data
gp_channels = gp.channels
gp_dep = gp.channel_dependencies
record = self._prepare_record(gp)
name = signal.name
names = signal.samples.dtype.names
if names is None:
raise RuntimeError("'names' is None")
# first we add the structure channel
match signal.attachment:
case at_data, at_name, hash_sum, compression_type:
attachment_index = self.attach(at_data, at_name, hash_sum, compression_type=compression_type)
attachment = attachment_index
case at_data, at_name, hash_sum:
attachment_index = self.attach(at_data, at_name, hash_sum)
attachment = attachment_index
case None:
attachment = None
# add channel block
cn_kwargs: ChannelKwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": signal.samples.dtype.itemsize * 8,
"byte_offset": offset,
"bit_offset": 0,
"data_type": v4c.DATA_TYPE_BYTEARRAY,
"precision": 0,
}
if attachment is not None:
cn_kwargs["attachment_addr"] = 0
def source_bus(source: Source | None) -> TypeIs[Source]:
return source is not None and source.source_type == v4c.SOURCE_BUS
if source_bus(signal.source):
cn_kwargs["flags"] = v4c.FLAG_CN_BUS_EVENT
flags_ = v4c.FLAG_CN_BUS_EVENT
grp.channel_group.flags |= v4c.FLAG_CG_BUS_EVENT | v4c.FLAG_CG_PLAIN_BUS_EVENT
else:
cn_kwargs["flags"] = 0
flags_ = 0
if invalidation_bytes_nr and signal.invalidation_bits is not None:
if (origin := signal.invalidation_bits.origin) == InvalidationArray.ORIGIN_UNKNOWN:
invalidation_arrays = typing.cast(list[InvalidationArray], inval_bits[origin])
invalidation_arrays.append(signal.invalidation_bits)
else:
inval_bits[origin] = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = origin[1]
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
ch.attachment = attachment
ch.dtype_fmt = signal.samples.dtype
record.append((ch.dtype_fmt, ch.dtype_fmt.itemsize, offset, 0))
if source_bus(signal.source) and grp.channel_group.acq_source is None:
grp.channel_group.acq_source = SourceInformation.from_common_source(signal.source)
match signal.source.bus_type:
case v4c.BUS_TYPE_CAN:
grp.channel_group.path_separator = 46
grp.channel_group.acq_name = "CAN"
case v4c.BUS_TYPE_FLEXRAY:
grp.channel_group.path_separator = 46
grp.channel_group.acq_name = "FLEXRAY"
case v4c.BUS_TYPE_ETHERNET:
grp.channel_group.path_separator = 46
grp.channel_group.acq_name = "ETHERNET"
case v4c.BUS_TYPE_K_LINE:
grp.channel_group.path_separator = 46
grp.channel_group.acq_name = "K_LINE"
case v4c.BUS_TYPE_MOST:
grp.channel_group.path_separator = 46
grp.channel_group.acq_name = "MOST"
case v4c.BUS_TYPE_LIN:
grp.channel_group.path_separator = 46
grp.channel_group.acq_name = "LIN"
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
entry = dg_cntr, ch_cntr
gp_channels.append(ch)
struct_self = entry
gp_sdata.append(None)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
ch_cntr += 1
dep_list: list[tuple[int, int]] = []
gp_dep.append(dep_list)
# then we add the fields
for name in names:
samples = signal.samples[name]
fld_names = samples.dtype.names
dtype = samples.dtype
if fld_names is None:
sig_type = v4c.SIGNAL_TYPE_SCALAR
if samples.dtype.kind in "SV":
sig_type = v4c.SIGNAL_TYPE_STRING
else:
if fld_names in (v4c.CANOPEN_TIME_FIELDS, v4c.CANOPEN_DATE_FIELDS):
sig_type = v4c.SIGNAL_TYPE_CANOPEN
elif name not in fld_names:
sig_type = v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION
else:
sig_type = v4c.SIGNAL_TYPE_ARRAY
metadata = dtype.metadata or dtype.base.metadata or {}
if sig_type in (v4c.SIGNAL_TYPE_SCALAR, v4c.SIGNAL_TYPE_STRING):
s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape)
byte_size = s_size // 8 or 1
# add channel block
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": flags_,
}
if invalidation_bytes_nr:
if signal.invalidation_bits is not None:
if (origin := signal.invalidation_bits.origin) == InvalidationArray.ORIGIN_UNKNOWN:
invalidation_arrays = typing.cast(list[InvalidationArray], inval_bits[origin])
invalidation_arrays.append(signal.invalidation_bits)
else:
inval_bits[origin] = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = origin[1]
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = metadata.get("unit", "")
ch.conversion = metadata.get("conversion", None)
ch.dtype_fmt = np.dtype((samples.dtype, samples.shape[1:]))
if dtype.metadata:
ch.conversion = dtype.metadata.get("conversion", None)
ch.unit = dtype.metadata.get("unit", "")
record.append(
(
ch.dtype_fmt,
ch.dtype_fmt.itemsize,
offset,
0,
)
)
entry = (dg_cntr, ch_cntr)
gp_channels.append(ch)
dep_list.append(entry)
offset += byte_size
if not samples.flags["C_CONTIGUOUS"]:
samples = np.ascontiguousarray(samples)
fields.append((samples, byte_size))
gp_sdata.append(None)
self.channels_db.add(name, entry)
ch_cntr += 1
gp_dep.append(None)
elif sig_type == v4c.SIGNAL_TYPE_ARRAY:
# here we have channel arrays or mdf v3 channel dependencies
array_samples = samples
names = samples.dtype.names
array_name = name
if names is None:
raise RuntimeError("'names' is None")
samples = array_samples[array_name]
shape = samples.shape[1:]
array_dtype = array_samples.dtype
dt_fields = [(name, *val) for name, val in array_dtype.fields.items()]
dt_fields.sort(key=lambda x: x[-1]) # sort by offset
dims_nr = len(shape)
names_nr = len(names)
metadata = array_dtype[array_name].metadata or array_dtype[array_name].base.metadata or {}
if metadata:
array_axes = metadata["axes"]
else:
if len(names) == 1:
array_axes = [{"type": "NO_AXIS", "conversion": None, "size": size} for size in shape]
else:
array_axes = [{"type": "REF_AXIS", "conversion": None, "size": size} for size in shape]
if len(names) == 1 and all(axis["type"] == "NO_AXIS" for axis in array_axes):
# add channel dependency block for composed parent channel
ca_kwargs = {
"dims": dims_nr,
"ca_type": v4c.CA_TYPE_ARRAY,
"flags": 0,
"byte_offset_base": samples.dtype.itemsize,
}
for i in range(dims_nr):
ca_kwargs[f"dim_size_{i}"] = shape[i] # type: ignore[literal-required]
parent_deps = [ChannelArrayBlock(**ca_kwargs)]
elif len(array_axes) == 1 and array_axes[0]["type"] == "SCALE_AXIS":
ca_kwargs = {
"dims": 1,
"ca_type": v4c.CA_TYPE_SCALE_AXIS,
"flags": 0,
"byte_offset_base": samples.dtype.itemsize,
"dim_size_0": shape[0],
}
parent_deps = [ChannelArrayBlock(**ca_kwargs)]
else:
parent_dep_with_refs = None
if all(axis["type"] == "REF_AXIS" for axis in array_axes):
ca_kwargs = {
"dims": dims_nr,
"ca_type": v4c.CA_TYPE_LOOKUP,
"flags": v4c.FLAG_CA_AXIS,
"byte_offset_base": samples.dtype.itemsize,
}
for i in range(dims_nr):
conv = array_axes[i]["conversion"]
if isinstance(conv, dict):
conv = from_dict(conv)
ca_kwargs[f"axis_conversion_{i}"] = conv
ca_kwargs[f"dim_size_{i}"] = shape[i] # type: ignore[literal-required]
parent_dep_with_refs = ChannelArrayBlock(**ca_kwargs)
parent_deps = [parent_dep_with_refs]
elif all(axis["type"] == "FIXED_AXIS" for axis in array_axes):
ca_kwargs = {
"dims": dims_nr,
"ca_type": v4c.CA_TYPE_LOOKUP,
"flags": v4c.FLAG_CA_AXIS | v4c.FLAG_CA_FIXED_AXIS,
"byte_offset_base": samples.dtype.itemsize,
}
for i in range(dims_nr):
conv = array_axes[i]["conversion"]
if isinstance(conv, dict):
conv = from_dict(conv)
ca_kwargs[f"axis_conversion_{i}"] = conv
ca_kwargs[f"dim_size_{i}"] = shape[i] # type: ignore[literal-required]
for j in range(shape[i]):
ca_kwargs[f"axis_{i}_value_{j}"] = array_axes[i]["values"][j]
parent_dep_with_refs = None
parent_deps = [ChannelArrayBlock(**ca_kwargs)]
else:
parent_deps = []
for i, axis in enumerate(array_axes):
conv = axis["conversion"]
if isinstance(conv, dict):
conv = from_dict(conv)
if axis["type"] == "REF_AXIS":
ca_kwargs[f"axis_conversion_{i}"] = conv
ca_kwargs = {
"dims": 1,
"ca_type": v4c.CA_TYPE_LOOKUP,
"flags": v4c.FLAG_CA_AXIS,
"byte_offset_base": samples.dtype.itemsize,
"axis_conversion_0": conv,
"dim_size_0": shape[i],
}
parent_dep_with_refs = dep = ChannelArrayBlock(**ca_kwargs)
else:
ca_kwargs = {
"dims": 1,
"ca_type": v4c.CA_TYPE_LOOKUP,
"flags": v4c.FLAG_CA_AXIS | v4c.FLAG_CA_FIXED_AXIS,
"byte_offset_base": samples.dtype.itemsize,
"axis_conversion_0": conv,
"dim_size_0": shape[i],
}
for j in range(shape[i]):
ca_kwargs[f"axis_0_value_{j}"] = axis["values"][j]
dep = ChannelArrayBlock(**ca_kwargs)
parent_deps.append(dep)
current_dt_offset = 0
for name, dt_dtype, dt_offset in dt_fields:
samples = array_samples[name]
shape = samples.shape[1:]
if delta := (dt_offset - current_dt_offset):
padding = np.zeros((samples.shape[0], delta), dtype="u1")
fields.append((padding, delta))
offset += delta
current_dt_offset = dt_offset + dt_dtype.itemsize
if name == array_name:
gp_dep.append(parent_deps)
# first we add the structure channel
s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape, True)
# add channel block
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if invalidation_bytes_nr and signal.invalidation_bits is not None:
if (origin := signal.invalidation_bits.origin) == InvalidationArray.ORIGIN_UNKNOWN:
invalidation_arrays = typing.cast(list[InvalidationArray], inval_bits[origin])
invalidation_arrays.append(signal.invalidation_bits)
else:
inval_bits[origin] = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = origin[1]
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
ch.dtype_fmt = samples.dtype
record.append(
(
samples.dtype,
samples.dtype.itemsize,
offset,
0,
)
)
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
gp_channels.append(ch)
size = s_size // 8
for dim in shape:
size *= dim
offset += size
if not samples.flags["C_CONTIGUOUS"]:
samples = np.ascontiguousarray(samples)
fields.append((samples, size))
gp_sdata.append(None)
entry = (dg_cntr, ch_cntr)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
ch_cntr += 1
elif not name.startswith("axis_"):
itemsize = samples.dtype.itemsize
fields.append((samples, itemsize))
offset += itemsize
else:
metadata = array_dtype[name].metadata or array_dtype[name].base.metadata or {}
axes = metadata.get("axes", [])
idx = int(name.split("_")[-1])
if array_axes[idx]["type"] == "FIXED_AXIS":
itemsize = samples.dtype.itemsize
fields.append((samples, itemsize))
offset += itemsize
else:
# add channel dependency block
ca_kwargs = {
"dims": 1,
"ca_type": v4c.CA_TYPE_SCALE_AXIS,
"flags": 0,
"byte_offset_base": samples.dtype.itemsize,
"dim_size_0": shape[0],
}
dep = ChannelArrayBlock(**ca_kwargs)
gp_dep.append([dep])
# add components channel
s_type, s_size = fmt_to_datatype_v4(samples.dtype, ())
byte_size = s_size // 8 or 1
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if invalidation_bytes_nr and signal.invalidation_bits is not None:
if (origin := signal.invalidation_bits.origin) == InvalidationArray.ORIGIN_UNKNOWN:
invalidation_arrays = typing.cast(list[InvalidationArray], inval_bits[origin])
invalidation_arrays.append(signal.invalidation_bits)
else:
inval_bits[origin] = signal.invalidation_bits
cn_kwargs["flags"] = v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = origin[1]
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = metadata.get("unit", "")
ch.comment = f"{array_name} axis {idx}"
ch.dtype_fmt = samples.dtype
record.append(
(
samples.dtype,
samples.dtype.itemsize,
offset,
0,
)
)
gp_channels.append(ch)
entry = dg_cntr, ch_cntr
parent_dep_with_refs.axis_channels.append(entry)
for dim in shape:
byte_size *= dim
offset += byte_size
if not samples.flags["C_CONTIGUOUS"]:
samples = np.ascontiguousarray(samples)
fields.append((samples, byte_size))
gp_sdata.append(None)
self.channels_db.add(name, entry)
ch_cntr += 1
if delta := (array_samples.dtype.itemsize - current_dt_offset):
padding = np.zeros((array_samples.shape[0], delta), dtype="u1")
fields.append((padding, delta))
elif sig_type == v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION:
struct = Signal(
samples,
samples,
name=name,
invalidation_bits=signal.invalidation_bits,
)
(
offset,
dg_cntr,
ch_cntr,
sub_structure,
new_fields,
) = self._append_structure_composition(
grp,
struct,
offset,
dg_cntr,
ch_cntr,
defined_texts,
invalidation_bytes_nr,
inval_bits,
)
dep_list.append(sub_structure)
fields.extend(new_fields)
return offset, dg_cntr, ch_cntr, struct_self, fields
def _append_structure_composition_column_oriented(
self,
grp: Group,
signal: Signal,
field_names: UniqueDB,
offset: int,
dg_cntr: int,
ch_cntr: int,
defined_texts: dict[str, int],
) -> tuple[
int,
int,
int,
tuple[int, int],
list[NDArray[Any]],
list[tuple[str, np.dtype[Any], tuple[int, ...]]],
]:
si_map = self._si_map
fields: list[NDArray[Any]] = []
types: list[tuple[str, np.dtype[Any], tuple[int, ...]]] = []
file = self._tempfile
seek = file.seek
seek(0, 2)
gp = grp
gp_sdata = gp.signal_data
gp_channels = gp.channels
gp_dep = gp.channel_dependencies
record = self._prepare_record(gp)
name = signal.name
names = signal.samples.dtype.names
if names is None:
raise RuntimeError("'names' is None")
field_name = field_names.get_unique_name(name)
# first we add the structure channel
match signal.attachment:
case at_data, at_name, hash_sum, compression_type:
attachment_index = self.attach(at_data, at_name, hash_sum, compression_type=compression_type)
attachment = attachment_index
case at_data, at_name, hash_sum:
attachment_index = self.attach(at_data, at_name, hash_sum)
attachment = attachment_index
case None:
attachment = None
# add channel block
cn_kwargs: ChannelKwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": signal.samples.dtype.itemsize * 8,
"byte_offset": offset,
"bit_offset": 0,
"data_type": v4c.DATA_TYPE_BYTEARRAY,
"precision": 0,
}
if attachment is not None:
cn_kwargs["attachment_addr"] = 0
def source_bus(source: Source | None) -> TypeIs[Source]:
return source is not None and source.source_type == v4c.SOURCE_BUS
if source_bus(signal.source):
cn_kwargs["flags"] = v4c.FLAG_CN_BUS_EVENT
flags_ = v4c.FLAG_CN_BUS_EVENT
grp.channel_group.flags |= v4c.FLAG_CG_BUS_EVENT
else:
cn_kwargs["flags"] = 0
flags_ = 0
if signal.invalidation_bits is not None:
cn_kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = 0
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
ch.attachment = attachment
ch.dtype_fmt = signal.samples.dtype
if source_bus(signal.source):
grp.channel_group.acq_source = SourceInformation.from_common_source(signal.source)
match signal.source.bus_type:
case v4c.BUS_TYPE_CAN:
grp.channel_group.path_separator = 46
grp.channel_group.acq_name = "CAN"
case v4c.BUS_TYPE_FLEXRAY:
grp.channel_group.path_separator = 46
grp.channel_group.acq_name = "FLEXRAY"
case v4c.BUS_TYPE_ETHERNET:
grp.channel_group.path_separator = 46
grp.channel_group.acq_name = "ETHERNET"
case v4c.BUS_TYPE_K_LINE:
grp.channel_group.path_separator = 46
grp.channel_group.acq_name = "K_LINE"
case v4c.BUS_TYPE_MOST:
grp.channel_group.path_separator = 46
grp.channel_group.acq_name = "MOST"
case v4c.BUS_TYPE_LIN:
grp.channel_group.path_separator = 46
grp.channel_group.acq_name = "LIN"
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
entry = dg_cntr, ch_cntr
gp_channels.append(ch)
struct_self = entry
gp_sdata.append(None)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
ch_cntr += 1
dep_list: list[tuple[int, int]] = []
gp_dep.append(dep_list)
record.append((ch.dtype_fmt, ch.dtype_fmt.itemsize, offset, 0))
# then we add the fields
for name in names:
field_name = field_names.get_unique_name(name)
samples = signal.samples[name]
fld_names = samples.dtype.names
if fld_names is None:
sig_type = v4c.SIGNAL_TYPE_SCALAR
if samples.dtype.kind in "SV":
sig_type = v4c.SIGNAL_TYPE_STRING
else:
if fld_names in (v4c.CANOPEN_TIME_FIELDS, v4c.CANOPEN_DATE_FIELDS):
sig_type = v4c.SIGNAL_TYPE_CANOPEN
elif name not in fld_names:
sig_type = v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION
else:
sig_type = v4c.SIGNAL_TYPE_ARRAY
if sig_type in (v4c.SIGNAL_TYPE_SCALAR, v4c.SIGNAL_TYPE_STRING):
s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape)
byte_size = s_size // 8 or 1
fields.append(samples)
types.append((field_name, samples.dtype, samples.shape[1:]))
# add channel block
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": flags_,
}
if signal.invalidation_bits is not None:
cn_kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = 0
ch = Channel(**cn_kwargs)
ch.name = name
ch.dtype_fmt = np.dtype((samples.dtype, samples.shape[1:]))
record.append(
(
ch.dtype_fmt,
ch.dtype_fmt.itemsize,
offset,
0,
)
)
entry = (dg_cntr, ch_cntr)
gp_channels.append(ch)
dep_list.append(entry)
offset += byte_size
gp_sdata.append(None)
self.channels_db.add(name, entry)
ch_cntr += 1
gp_dep.append(None)
elif sig_type == v4c.SIGNAL_TYPE_ARRAY:
# here we have channel arrays or mdf v3 channel dependencies
array_samples = samples
names = samples.dtype.names
if names is None:
raise RuntimeError("'names' is None")
array_name = name
samples = array_samples[array_name]
shape = samples.shape[1:]
if len(names) > 1:
# add channel dependency block for composed parent channel
dims_nr = len(shape)
names_nr = len(names)
if names_nr == 0:
ca_kwargs: ChannelArrayBlockKwargs = {
"dims": dims_nr,
"ca_type": v4c.CA_TYPE_LOOKUP,
"flags": v4c.FLAG_CA_FIXED_AXIS,
"byte_offset_base": samples.dtype.itemsize,
}
for i in range(dims_nr):
ca_kwargs[f"dim_size_{i}"] = shape[i] # type: ignore[literal-required]
elif len(names) == 1:
ca_kwargs = {
"dims": dims_nr,
"ca_type": v4c.CA_TYPE_ARRAY,
"flags": 0,
"byte_offset_base": samples.dtype.itemsize,
}
for i in range(dims_nr):
ca_kwargs[f"dim_size_{i}"] = shape[i] # type: ignore[literal-required]
else:
ca_kwargs = {
"dims": dims_nr,
"ca_type": v4c.CA_TYPE_LOOKUP,
"flags": v4c.FLAG_CA_AXIS,
"byte_offset_base": samples.dtype.itemsize,
}
for i in range(dims_nr):
ca_kwargs[f"dim_size_{i}"] = shape[i] # type: ignore[literal-required]
parent_dep = ChannelArrayBlock(**ca_kwargs)
else:
# add channel dependency block for composed parent channel
ca_kwargs = {
"dims": 1,
"ca_type": v4c.CA_TYPE_SCALE_AXIS,
"flags": 0,
"byte_offset_base": samples.dtype.itemsize,
"dim_size_0": shape[0],
}
parent_dep = ChannelArrayBlock(**ca_kwargs)
gp_dep.append([parent_dep])
for name in names:
samples = array_samples[name]
shape = samples.shape[1:]
if name == array_name:
gp_dep.append([parent_dep])
record.append(
(
samples.dtype,
samples.dtype.itemsize,
offset,
0,
)
)
field_name = field_names.get_unique_name(name)
fields.append(samples)
dtype_pair = field_name, samples.dtype, shape
types.append(dtype_pair)
# first we add the structure channel
s_type, s_size = fmt_to_datatype_v4(samples.dtype, samples.shape, True)
# add channel block
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if signal.invalidation_bits is not None:
cn_kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = 0
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
ch.dtype_fmt = samples.dtype
# source for channel
source = signal.source
if source:
if source in si_map:
ch.source = si_map[source]
else:
new_source = SourceInformation(source_type=source.source_type, bus_type=source.bus_type)
new_source.name = source.name
new_source.path = source.path
new_source.comment = source.comment
si_map[source] = new_source
ch.source = new_source
gp_channels.append(ch)
size = s_size // 8
for dim in shape:
size *= dim
offset += size
gp_sdata.append(None)
entry = (dg_cntr, ch_cntr)
self.channels_db.add(name, entry)
for _name in ch.display_names:
self.channels_db.add(_name, entry)
ch_cntr += 1
else:
field_name = field_names.get_unique_name(name)
fields.append(samples)
types.append((field_name, samples.dtype, shape))
record.append(
(
samples.dtype,
samples.dtype.itemsize,
offset,
0,
)
)
# add channel dependency block
ca_kwargs = {
"dims": 1,
"ca_type": v4c.CA_TYPE_SCALE_AXIS,
"flags": 0,
"byte_offset_base": samples.dtype.itemsize,
"dim_size_0": shape[0],
}
dep = ChannelArrayBlock(**ca_kwargs)
gp_dep.append([dep])
# add components channel
s_type, s_size = fmt_to_datatype_v4(samples.dtype, ())
byte_size = s_size // 8 or 1
cn_kwargs = {
"channel_type": v4c.CHANNEL_TYPE_VALUE,
"bit_count": s_size,
"byte_offset": offset,
"bit_offset": 0,
"data_type": s_type,
"flags": 0,
}
if signal.invalidation_bits is not None:
cn_kwargs["flags"] |= v4c.FLAG_CN_INVALIDATION_PRESENT
cn_kwargs["pos_invalidation_bit"] = 0
ch = Channel(**cn_kwargs)
ch.name = name
ch.unit = signal.unit
ch.comment = signal.comment
ch.display_names = signal.display_names
ch.dtype_fmt = samples.dtype
gp_channels.append(ch)
entry = dg_cntr, ch_cntr
parent_dep.axis_channels.append(entry)
for dim in shape:
byte_size *= dim
offset += byte_size
gp_sdata.append(None)
self.channels_db.add(name, entry)
ch_cntr += 1
elif sig_type == v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION:
struct = Signal(
samples,
samples,
name=name,
invalidation_bits=signal.invalidation_bits,
)
(
offset,
dg_cntr,
ch_cntr,
sub_structure,
new_fields,
new_types,
) = self._append_structure_composition_column_oriented(
grp,
struct,
field_names,
offset,
dg_cntr,
ch_cntr,
defined_texts,
)
dep_list.append(sub_structure)
fields.extend(new_fields)
types.extend(new_types)
return offset, dg_cntr, ch_cntr, struct_self, fields, types
[docs]
def extend(self, index: int, signals: Sequence[tuple[NDArray[Any], NDArray[np.bool] | None]]) -> None:
"""Extend a group with new samples.
`signals` contains (values, invalidation_bits) pairs for each extended
signal. The first pair is the master channel's pair, and the next pairs
must respect the same order in which the signals were appended. The
samples must have raw or physical values according to the signals used
for the initial append.
Parameters
----------
index : int
Group index.
signals : sequence
Sequence of (np.ndarray, np.ndarray) tuples.
Examples
--------
>>> from asammdf import MDF, Signal
>>> import numpy as np
>>> s1 = np.array([1, 2, 3, 4, 5])
>>> s2 = np.array([-1, -2, -3, -4, -5])
>>> s3 = np.array([0.1, 0.04, 0.09, 0.16, 0.25])
>>> t = np.array([0.001, 0.002, 0.003, 0.004, 0.005])
>>> s1 = Signal(samples=s1, timestamps=t, unit='+', name='Positive')
>>> s2 = Signal(samples=s2, timestamps=t, unit='-', name='Negative')
>>> s3 = Signal(samples=s3, timestamps=t, unit='flts', name='Floats')
>>> mdf = MDF(version='4.10')
>>> mdf.append([s1, s2, s3], comment='created by asammdf')
>>> t = np.array([0.006, 0.007, 0.008, 0.009, 0.010])
Extend without invalidation bits.
>>> mdf.extend(0, [(t, None), (s1.samples, None), (s2.samples, None), (s3.samples, None)])
Extend with some invalidation bits.
>>> s1_inv = np.array([0, 0, 0, 1, 1], dtype=bool)
>>> mdf.extend(0, [(t, None), (s1.samples, s1_inv), (s2.samples, None), (s3.samples, None)])
"""
if self.version >= "4.20" and self._column_storage:
return self._extend_column_oriented(index, signals)
gp = self.groups[index]
if not signals:
message = '"append" requires a non-empty list of Signal objects'
raise MdfException(message)
stream = self._tempfile
fields: list[tuple[bytes | NDArray[Any], int]] = []
inval_bits_map: dict[tuple[int, int], list[InvalidationArray] | InvalidationArray] = {
InvalidationArray.ORIGIN_UNKNOWN: []
}
added_cycles = len(signals[0][0])
invalidation_bytes_nr = gp.channel_group.invalidation_bytes_nr
signal_types = typing.cast(list[tuple[int, int]], gp.signal_types)
for i, ((signal, invalidation_bits), (sig_type, sig_size)) in enumerate(
zip(signals, signal_types, strict=False)
):
if invalidation_bytes_nr:
if invalidation_bits is not None:
if not isinstance(invalidation_bits, InvalidationArray):
invalidation_array = InvalidationArray(invalidation_bits)
else:
invalidation_array = invalidation_bits
if (origin := invalidation_array.origin) == InvalidationArray.ORIGIN_UNKNOWN:
invalidation_arrays = typing.cast(list[InvalidationArray], inval_bits_map[origin])
invalidation_arrays.append(invalidation_array)
else:
inval_bits_map[origin] = invalidation_array
# first add the signals in the simple signal list
match sig_type:
case v4c.SIGNAL_TYPE_SCALAR:
if not signal.flags["C_CONTIGUOUS"]:
signal = np.ascontiguousarray(signal)
fields.append((signal, sig_size))
case v4c.SIGNAL_TYPE_CANOPEN:
names = signal.dtype.names
if names == v4c.CANOPEN_TIME_FIELDS:
if not signal.flags["C_CONTIGUOUS"]:
signal = np.ascontiguousarray(signal)
fields.append((signal, sig_size))
else:
arrays: list[Any] = []
for field in ("ms", "min", "hour", "day", "month", "year"):
arrays.append(signal[field])
vals = np.rec.fromarrays(arrays).tobytes()
fields.append((vals, sig_size))
case v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION:
if not signal.flags["C_CONTIGUOUS"]:
signal = np.ascontiguousarray(signal)
fields.append((signal, sig_size))
case v4c.SIGNAL_TYPE_ARRAY:
if signal.dtype.fields is None:
raise RuntimeError("'names' is None")
dt_fields = [(name, *val) for name, val in signal.dtype.fields.items()]
dt_fields.sort(key=lambda x: x[-1]) # sort by offset
current_dt_offset = 0
for name, dt_dtype, dt_offset in dt_fields:
samples = signal[name]
if delta := (dt_offset - current_dt_offset):
padding = np.zeros((samples.shape[0], delta), dtype="u1")
fields.append((padding, delta))
size = dt_dtype.itemsize
current_dt_offset = dt_offset + dt_dtype.itemsize
if not samples.flags["C_CONTIGUOUS"]:
samples = np.ascontiguousarray(samples)
fields.append((samples, size))
if delta := (signal.dtype.itemsize - current_dt_offset):
padding = np.zeros((signal.shape[0], delta), dtype="u1")
fields.append((padding, delta))
case _:
if self.compact_vlsd:
cur_offset = sum(blk.original_size for blk in gp.get_signal_data_blocks(i))
pairs: list[bytes] = []
offsets: list[int] = []
off = 0
if gp.channels[i].data_type == v4c.DATA_TYPE_STRING_UTF_16_LE:
for elem in signal:
offsets.append(off)
size = len(elem)
if size % 2:
size += 1
elem = elem + b"\0"
pairs.extend((UINT32_p(size), elem))
off += size + 4
else:
for elem in signal:
offsets.append(off)
size = len(elem)
pairs.extend((UINT32_p(size), elem))
off += size + 4
offsets_arr: NDArray[np.integer[Any]] = array(offsets, dtype=uint64)
stream.seek(0, 2)
addr = stream.tell()
data_size = off
if data_size:
info = SignalDataBlockInfo(
address=addr,
compressed_size=data_size,
original_size=data_size,
location=v4c.LOCATION_TEMPORARY_FILE,
)
signal_data = typing.cast(
list[tuple[list[SignalDataBlockInfo], Iterator[SignalDataBlockInfo]]], gp.signal_data
)
signal_data[i][0].append(info)
stream.write(b"".join(pairs))
offsets_arr += cur_offset # type: ignore[misc, unused-ignore]
if not offsets_arr.flags["C_CONTIGUOUS"]:
offsets_arr = np.ascontiguousarray(offsets_arr)
fields.append((offsets_arr, 8))
else:
cur_offset = sum(blk.original_size for blk in gp.get_signal_data_blocks(i))
offsets_arr = arange(len(signal), dtype=uint64) * (signal.itemsize + 4)
values = [full(len(signal), signal.itemsize, dtype=uint32), signal]
types_ = [("", uint32), ("", signal.dtype)]
values_arr = np.rec.fromarrays(values, dtype=types_)
stream.seek(0, 2)
addr = stream.tell()
block_size = len(values_arr) * values_arr.itemsize
if block_size:
info = SignalDataBlockInfo(
address=addr,
compressed_size=block_size,
original_size=block_size,
location=v4c.LOCATION_TEMPORARY_FILE,
)
signal_data = typing.cast(
list[tuple[list[SignalDataBlockInfo], Iterator[SignalDataBlockInfo]]], gp.signal_data
)
signal_data[i][0].append(info)
values_arr.tofile(stream)
offsets_arr += cur_offset
if not offsets_arr.flags["C_CONTIGUOUS"]:
offsets_arr = np.ascontiguousarray(offsets_arr)
fields.append((offsets_arr, 8))
if invalidation_bytes_nr:
unknown_origin = typing.cast(list[InvalidationArray], inval_bits_map.pop(InvalidationArray.ORIGIN_UNKNOWN))
inval_array_map = typing.cast(dict[tuple[int, int], InvalidationArray], inval_bits_map)
inval_bits = typing.cast(list[NDArray[np.bool]], list(inval_array_map.values()) + unknown_origin)
cycles_nr = len(inval_bits[0])
new_invalidation_bytes_nr = len(inval_bits)
for _ in range(8 - new_invalidation_bytes_nr % 8):
inval_bits.append(zeros(cycles_nr, dtype=bool))
inval_bits.reverse()
new_invalidation_bytes_nr = len(inval_bits) // 8
if new_invalidation_bytes_nr != invalidation_bytes_nr:
raise MdfException(
"The invalidation bytes number in the extend methods differs from the one from the append"
)
inval_bits_vals = np.fliplr(
np.packbits(array(inval_bits).T).reshape((cycles_nr, invalidation_bytes_nr))
).ravel()
if not self._use_ld_blocks:
fields.append((inval_bits_vals, invalidation_bytes_nr))
samples_bytes = data_block_from_arrays(fields, added_cycles, THREAD_COUNT)
size = len(samples_bytes)
samples_view = memoryview(samples_bytes)
del fields
stream.seek(0, 2)
addr = stream.tell()
record_size = gp.channel_group.samples_byte_nr + gp.channel_group.invalidation_bytes_nr
if size:
if not self._use_ld_blocks:
block_size = 32 * 1024 * 1024 // record_size * record_size
count = ceil(size / block_size)
for i in range(count):
data_ = samples_view[i * block_size : (i + 1) * block_size]
raw_size = len(data_)
data_ = lz_compress(data_, store_size=True)
size = len(data_)
data_address = self._tempfile.tell()
self._tempfile.write(data_)
gp.data_blocks.append(
DataBlockInfo(
address=data_address,
block_type=v4c.DZ_BLOCK_LZ,
original_size=raw_size,
compressed_size=size,
param=0,
location=v4c.LOCATION_TEMPORARY_FILE,
)
)
gp.channel_group.cycles_nr += added_cycles
self.virtual_groups[index].cycles_nr += added_cycles
else:
raw_size = size
data = lz_compress(samples_bytes, store_size=True)
size = len(data)
stream.write(data)
gp.data_blocks.append(
DataBlockInfo(
address=addr,
block_type=v4c.DZ_BLOCK_LZ,
original_size=raw_size,
compressed_size=size,
param=0,
location=v4c.LOCATION_TEMPORARY_FILE,
)
)
gp.channel_group.cycles_nr += added_cycles
self.virtual_groups[index].cycles_nr += added_cycles
if invalidation_bytes_nr:
addr = stream.tell()
data = inval_bits_vals.tobytes()
raw_size = len(data)
data = lz_compress(data, store_size=True)
size = len(data)
stream.write(data)
gp.data_blocks[-1].invalidation_block = InvalidationBlockInfo(
address=addr,
block_type=v4c.DZ_BLOCK_LZ,
original_size=raw_size,
compressed_size=size,
param=0,
)
def _extend_column_oriented(
self, index: int, signals: Sequence[tuple[NDArray[Any], NDArray[np.bool] | None]]
) -> None:
"""Extend a group with new samples.
`signals` contains (values, invalidation_bits) pairs for each extended
signal. The first pair is the master channel's pair, and the next pairs
must respect the same order in which the signals were appended. The
samples must have raw or physical values according to the signals used
for the initial append.
Parameters
----------
index : int
Group index.
signals : sequence
Sequence of (np.ndarray, np.ndarray) tuples.
Examples
--------
>>> from asammdf import MDF, Signal
>>> import numpy as np
>>> s1 = np.array([1, 2, 3, 4, 5])
>>> s2 = np.array([-1, -2, -3, -4, -5])
>>> s3 = np.array([0.1, 0.04, 0.09, 0.16, 0.25])
>>> t = np.array([0.001, 0.002, 0.003, 0.004, 0.005])
>>> s1 = Signal(samples=s1, timestamps=t, unit='+', name='Positive')
>>> s2 = Signal(samples=s2, timestamps=t, unit='-', name='Negative')
>>> s3 = Signal(samples=s3, timestamps=t, unit='flts', name='Floats')
>>> mdf = MDF(version='4.10')
>>> mdf.append([s1, s2, s3], comment='created by asammdf')
>>> t = np.array([0.006, 0.007, 0.008, 0.009, 0.010])
Extend without invalidation bits.
>>> mdf.extend(0, [(t, None), (s1.samples, None), (s2.samples, None), (s3.samples, None)])
Extend with some invalidation bits.
>>> s1_inv = np.array([0, 0, 0, 1, 1], dtype=bool)
>>> mdf.extend(0, [(t, None), (s1.samples, s1_inv), (s2.samples, None), (s3.samples, None)])
"""
gp = self.groups[index]
if not signals:
message = '"append" requires a non-empty list of Signal objects'
raise MdfException(message)
stream = self._tempfile
stream.seek(0, 2)
write = stream.write
tell = stream.tell
added_cycles = len(signals[0][0])
self.virtual_groups[index].cycles_nr += added_cycles
for i, (signal, invalidation_bits) in enumerate(signals):
gp = self.groups[index + i]
sig_type = typing.cast(int, gp.signal_types[0])
# first add the signals in the simple signal list
match sig_type:
case v4c.SIGNAL_TYPE_SCALAR:
samples = signal
case v4c.SIGNAL_TYPE_CANOPEN:
names = signal.dtype.names
if names == v4c.CANOPEN_TIME_FIELDS:
samples = signal
else:
vals = []
for field in ("ms", "min", "hour", "day", "month", "year"):
vals.append(signal[field])
samples = np.rec.fromarrays(vals)
case v4c.SIGNAL_TYPE_STRUCTURE_COMPOSITION:
samples = signal
case v4c.SIGNAL_TYPE_ARRAY:
samples = signal
case _:
cur_offset = sum(blk.original_size for blk in gp.get_signal_data_blocks(0))
offsets = arange(len(signal), dtype=uint64) * (signal.itemsize + 4)
arrays = [full(len(signal), signal.itemsize, dtype=uint32), signal]
types_ = [("", uint32), ("", signal.dtype)]
values = np.rec.fromarrays(arrays, dtype=types_)
addr = tell()
block_size = len(values) * values.itemsize
if block_size:
info = SignalDataBlockInfo(
address=addr,
compressed_size=block_size,
original_size=block_size,
location=v4c.LOCATION_TEMPORARY_FILE,
)
sd_block_infos = typing.cast(
tuple[list[SignalDataBlockInfo], Iterator[SignalDataBlockInfo]], gp.signal_data[i]
)
sd_block_infos[0].append(info)
write(values.tobytes())
offsets += cur_offset
samples = offsets
addr = tell()
if added_cycles:
data = samples.tobytes()
raw_size = len(data)
data = lz_compress(data, store_size=True)
size = len(data)
write(data)
gp.data_blocks.append(
DataBlockInfo(
address=addr,
block_type=v4c.DZ_BLOCK_LZ,
original_size=raw_size,
compressed_size=size,
param=0,
location=v4c.LOCATION_TEMPORARY_FILE,
)
)
gp.channel_group.cycles_nr += added_cycles
if invalidation_bits is not None:
addr = tell()
data = invalidation_bits.tobytes()
raw_size = len(data)
data = lz_compress(data, store_size=True)
size = len(data)
write(data)
gp.data_blocks[-1].invalidation_block = InvalidationBlockInfo(
address=addr,
block_type=v4c.DZ_BLOCK_LZ,
original_size=raw_size,
compressed_size=size,
param=None,
)
[docs]
def attach(
self,
data: bytes,
file_name: StrPath | None = None,
hash_sum: bytes | None = None,
comment: str = "",
compression: bool = True,
mime: str = r"application/octet-stream",
embedded: bool = True,
password: str | bytes | None = None,
compression_type: str = "deflate",
) -> int:
"""Attach embedded attachment as application/octet-stream.
Parameters
----------
data : bytes
Data to be attached.
file_name : str | path-like, optional
File name.
hash_sum : bytes, optional
MD5 of the data.
comment : str, optional
Attachment comment.
compression : bool, default True
Use compression for embedded attachment data.
mime : str, default 'application/octet-stream'
Mime type.
embedded : bool, default True
Attachment is embedded in the file.
password : str | bytes, optional
Password used to encrypt the data using AES256 encryption.
.. versionadded:: 7.0.0
compression_type : str, default "deflate"
compression type starting with MDF v4.30. Can be "deflate",
"lz4" or "zstd"
.. versionadded:: 8.8.0
Returns
-------
index : int
New attachment index.
"""
if self._force_attachment_encryption:
password = password or self._password
if password and not CRYPTOGRAPHY_AVAILABLE:
raise MdfException("cryptography must be installed for attachment encryption")
hash_digest: bytes | str
if hash_sum is None:
worker = md5()
worker.update(data)
hash_digest = worker.hexdigest()
else:
hash_digest = hash_sum
if hash_digest in self._attachments_cache:
return self._attachments_cache[hash_digest]
hash_sum_encrypted: bytes | str
if password:
if isinstance(password, str):
password = password.encode("utf-8")
size = len(password)
if size < 32:
password = password + bytes(32 - size)
else:
password = password[:32]
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(password), modes.CBC(iv))
encryptor = cipher.encryptor()
original_size = len(data)
rem = original_size % 16
if rem:
data += os.urandom(16 - rem)
data = iv + encryptor.update(data) + encryptor.finalize()
worker = md5()
worker.update(data)
hash_sum_encrypted = worker.hexdigest()
comment = f"""<ATcomment>
<TX>{comment}</TX>
<extensions>
<extension>
<encrypted>true</encrypted>
<algorithm>AES256</algorithm>
<original_md5_sum>{hash_sum_encrypted}</original_md5_sum>
<original_size>{original_size}</original_size>
</extension>
</extensions>
</ATcomment>
"""
else:
hash_sum_encrypted = hash_digest
if hash_sum_encrypted in self._attachments_cache:
return self._attachments_cache[hash_digest]
creator_index = len(self.file_history)
fh = FileHistory()
fh.comment = """<FHcomment>
<TX>Added new embedded attachment from {file_name}</TX>
<tool_id>{tool}</tool_id>
<tool_vendor>{vendor}</tool_vendor>
<tool_version>{version}</tool_version>
</FHcomment>""".format(
file_name=file_name if file_name else "bin.bin",
version=tool.__version__,
tool=tool.__tool__,
vendor=tool.__vendor__,
)
self.file_history.append(fh)
file_name = file_name or "bin.bin"
at_block = AttachmentBlock(
data=data,
compression=compression,
embedded=embedded,
file_name=file_name,
comment=comment,
compression_type=compression_type,
)
at_block.comment = comment
at_block.creator_index = creator_index
self.attachments.append(at_block)
file_path = Path(file_name)
mime_type, _ = guess_type(Path(file_path))
if mime_type is None:
suffix = file_path.suffix.lower().strip(".")
if suffix == "a2l":
mime = "application/A2L"
elif not mime:
mime = f"application/x-{suffix}"
else:
mime = mime_type
at_block.mime = mime
index = len(self.attachments) - 1
self._attachments_cache[hash_digest] = index
self._attachments_cache[hash_sum_encrypted] = index
return index
[docs]
def close(self) -> None:
"""Call this just before the object is not used anymore to clean up the
temporary file and close the file object.
"""
if self._closed:
return
else:
self._closed = True
self._parent = None
if self._tempfile is not None:
self._tempfile.close()
if not self._from_filelike and self._file is not None:
self._file.close()
if self._mapped_file is not None:
self._mapped_file.close()
if self._delete_on_close:
try:
Path(self.name).unlink()
except:
pass
if self.original_name is not None:
if Path(self.original_name).suffix.lower() in (
".bz2",
".gzip",
".mf4z",
".zip",
):
try:
Path(self.name).unlink()
except:
pass
for gp in self.groups:
gp.clear()
self.groups.clear()
del self.header
del self.identification
self.file_history.clear()
self.channels_db.clear()
self.masters_db.clear()
self.attachments.clear()
self._attachments_cache.clear()
self.events.clear()
self._ch_map.clear()
self._master_channel_metadata.clear()
self._invalidation_cache.clear()
self._external_dbc_cache.clear()
self._si_map.clear()
self._cc_map.clear()
self._cg_map.clear()
self._cn_data_map.clear()
self._dbc_cache.clear()
self.virtual_groups.clear()
def _extract_attachment(
self,
index: int | None = None,
password: str | bytes | None = None,
) -> tuple[bytes | str, Path, bytes | str]:
"""Extract attachment data by index. If it is an embedded attachment,
then this method creates the new file according to the attachment file
name information.
Parameters
----------
index : int, optional
Attachment index.
password : str | bytes, optional
Password used to encrypt the data using AES256 encryption.
.. versionadded:: 7.0.0
Returns
-------
data : (bytes, pathlib.Path, bytes)
Tuple of attachment data and path.
"""
password = password or self._password
if index is None:
return b"", Path(""), md5().digest()
attachment = self.attachments[index]
current_path = Path.cwd()
file_path = Path(attachment.file_name or "embedded")
data: bytes | str
md5_sum: bytes | str
try:
os.chdir(self.name.resolve().parent)
flags = attachment.flags
# for embedded attachments extract data and create new files
if flags & v4c.FLAG_AT_EMBEDDED:
data = attachment.extract()
md5_worker = md5()
md5_worker.update(data)
md5_sum = md5_worker.digest()
encryption_info = extract_encryption_information(attachment.comment)
if encryption_info.get("encrypted", False):
if not password:
raise MdfException("the password must be provided for encrypted attachments")
if isinstance(password, str):
password = password.encode("utf-8")
size = len(password)
if size < 32:
password = password + bytes(32 - size)
else:
password = password[:32]
if encryption_info["algorithm"] == "aes256":
md5_worker = md5()
md5_worker.update(data)
md5_sum = md5_worker.hexdigest().lower()
if md5_sum != encryption_info["original_md5_sum"]:
raise MdfException(
f"MD5 sum mismatch for encrypted attachment: original={encryption_info['original_md5_sum']} and computed={md5_sum}"
)
iv, data = data[:16], data[16:]
cipher = Cipher(algorithms.AES(password), modes.CBC(iv))
decryptor = cipher.decryptor()
data = decryptor.update(data) + decryptor.finalize()
data = data[: encryption_info["original_size"]]
else:
raise MdfException(
f"not implemented attachment encryption algorithm <{encryption_info['algorithm']}>"
)
else:
# for external attachments read the file and return the content
if file_path.exists() and file_path.is_file():
data = file_path.read_bytes()
else:
if self.original_name is None:
raise RuntimeError("'original_name' is None")
file_path = Path(self.original_name).parent / file_path.name
if file_path.exists() and file_path.is_file():
data = file_path.read_bytes()
else:
raise Exception(f"External attachment file {attachment.file_name} was not found")
md5_worker = md5()
md5_worker.update(data)
md5_sum = md5_worker.digest()
if attachment.mime.startswith("text"):
data = data.decode("utf-8", errors="replace")
if flags & v4c.FLAG_AT_MD5_VALID and attachment.md5_sum != md5_sum:
message = (
f'ATBLOCK md5sum="{attachment.md5_sum!r}" '
f"and external attachment data ({file_path}) "
f'md5sum="{md5_sum!r}"'
)
logger.warning(message)
except Exception as err:
os.chdir(current_path)
message = f'Exception during attachment "{attachment.file_name}" extraction: {err!r}'
logger.warning(message)
data = b""
md5_sum = md5().digest()
finally:
os.chdir(current_path)
return data, file_path, md5_sum
@overload
def get(
self,
name: str | None = ...,
group: int | None = ...,
index: int | None = ...,
raster: RasterType | None = ...,
samples_only: Literal[False] = ...,
data: Fragment | None = ...,
raw: bool = ...,
ignore_invalidation_bits: bool = ...,
record_offset: int = ...,
record_count: int | None = ...,
skip_channel_validation: bool = ...,
) -> Signal: ...
@overload
def get(
self,
name: str | None = ...,
group: int | None = ...,
index: int | None = ...,
raster: RasterType | None = ...,
*,
samples_only: Literal[True],
data: Fragment | None = ...,
raw: bool = ...,
ignore_invalidation_bits: Literal[False] = ...,
record_offset: int = ...,
record_count: int | None = ...,
skip_channel_validation: bool = ...,
) -> tuple[NDArray[Any], NDArray[np.bool] | None]: ...
@overload
def get(
self,
name: str | None = ...,
group: int | None = ...,
index: int | None = ...,
raster: RasterType | None = ...,
*,
samples_only: Literal[True],
data: Fragment | None = ...,
raw: bool = ...,
ignore_invalidation_bits: Literal[True],
record_offset: int = ...,
record_count: int | None = ...,
skip_channel_validation: bool = ...,
) -> tuple[NDArray[Any], None]: ...
@overload
def get(
self,
name: str | None = ...,
group: int | None = ...,
index: int | None = ...,
raster: RasterType | None = ...,
*,
samples_only: Literal[True],
data: Fragment | None = ...,
raw: bool = ...,
ignore_invalidation_bits: bool = ...,
record_offset: int = ...,
record_count: int | None = ...,
skip_channel_validation: bool = ...,
) -> tuple[NDArray[Any], NDArray[np.bool] | None]: ...
@overload
def get(
self,
name: str | None = ...,
group: int | None = ...,
index: int | None = ...,
raster: RasterType | None = ...,
samples_only: bool = ...,
data: Fragment | None = ...,
raw: bool = ...,
ignore_invalidation_bits: bool = ...,
record_offset: int = ...,
record_count: int | None = ...,
skip_channel_validation: bool = ...,
) -> Signal | tuple[NDArray[Any], NDArray[np.bool] | None]: ...
[docs]
def get(
self,
name: str | None = None,
group: int | None = None,
index: int | None = None,
raster: RasterType | None = None,
samples_only: bool = False,
data: Fragment | None = None,
raw: bool = False,
ignore_invalidation_bits: bool = False,
record_offset: int = 0,
record_count: int | None = None,
skip_channel_validation: bool = False,
) -> Signal | tuple[NDArray[Any], NDArray[np.bool] | None]:
"""Get channel samples. The raw data group samples are not loaded to
memory so it is advised to use `filter` or `select` instead of
performing several `get` calls.
The channel can be specified in two ways:
* Using the first positional argument `name`.
* If there are multiple occurrences for this channel, then the `group`
and `index` arguments can be used to select a specific group.
* If there are multiple occurrences for this channel and either the
`group` or `index` arguments is None, then a warning is issued.
* Using the group number (keyword argument `group`) and the channel
number (keyword argument `index`). Use `info` method for group and
channel numbers.
If the `raster` keyword argument is not None, the output is interpolated
accordingly.
Parameters
----------
name : str, optional
Name of channel.
group : int, optional
0-based group index.
index : int, optional
0-based channel index.
raster : float, optional
Time raster in seconds.
samples_only : bool, default False
If True, return only the channel samples as np.ndarray; if False,
return a `Signal` object.
data : Fragment, optional
Prevent redundant data read by providing the raw data group samples.
raw : bool, default False
Return channel samples without applying the conversion rule.
ignore_invalidation_bits : bool, default False
Option to ignore invalidation bits.
record_offset : int, optional
If `data=None`, use this to select the record offset from which the
group data should be loaded.
record_count : int, optional
Number of records to read; default is None and in this case all
available records are used.
skip_channel_validation : bool, default False
Skip validation of channel name, group index and channel index. If
True, the caller has to make sure that the `group` and `index`
arguments are provided and are correct.
.. versionadded:: 7.0.0
Returns
-------
res : (np.ndarray, np.ndarray) | Signal
Returns `Signal` if `samples_only=False` (default option),
otherwise returns a (np.ndarray, np.ndarray) tuple of samples and
invalidation bits. If invalidation bits are not used or if
`ignore_invalidation_bits` is False, then the second item will be
None.
The `Signal` samples are:
* np.recarray for channels that have composition/channel
array address or for channels of type CANOPENDATE, CANOPENTIME
* np.ndarray for all the rest
Raises
------
MdfException
* if the channel name is not found
* if the group index is out of range
* if the channel index is out of range
* if there are multiple channel occurrences in the file and the
arguments `name`, `group`, `index` are ambiguous. This behaviour
can be turned off by setting `raise_on_multiple_occurrences` to
False.
Examples
--------
>>> from asammdf import MDF, Signal
>>> import numpy as np
>>> t = np.arange(5)
>>> s = np.ones(5)
>>> mdf = MDF(version='4.10')
>>> for i in range(4):
... sigs = [Signal(s * (i * 10 + j), t, name='Sig') for j in range(1, 4)]
... mdf.append(sigs)
Specifying only the channel name is not enough when there are multiple
channels with that name.
>>> mdf.get('Sig')
MdfException: Multiple occurrences for channel "Sig": ((0, 1), (0, 2),
(0, 3), (1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2),
(3, 3)). Provide both "group" and "index" arguments to select another
data group
In this case, adding the group number is also not enough since there
are multiple channels with that name in that group.
>>> mdf.get('Sig', 1)
MdfException: Multiple occurrences for channel "Sig": ((1, 1), (1, 2),
(1, 3)). Provide both "group" and "index" arguments to select another
data group
Get the channel named "Sig" from group 1, channel index 2.
>>> mdf.get('Sig', 1, 2)
<Signal Sig:
samples=[ 12. 12. 12. 12. 12.]
timestamps=[ 0. 1. 2. 3. 4.]
unit=""
comment="">
Get the channel from group 2 and channel index 1.
>>> mdf.get(None, 2, 1)
<Signal Sig:
samples=[ 21. 21. 21. 21. 21.]
timestamps=[ 0. 1. 2. 3. 4.]
unit=""
comment="">
>>> mdf.get(group=2, index=1)
<Signal Sig:
samples=[ 21. 21. 21. 21. 21.]
timestamps=[ 0. 1. 2. 3. 4.]
unit=""
comment="">
"""
if skip_channel_validation:
if group is None:
raise RuntimeError("'group' cannot be None if 'skip_channel_validation' is True")
if index is None:
raise RuntimeError("'index' cannot be None if 'skip_channel_validation' is True")
gp_nr, ch_nr = group, index
else:
gp_nr, ch_nr = self._validate_channel_selection(name, group, index)
grp = self.groups[gp_nr]
# get the channel object
channel = grp.channels[ch_nr]
dependency_list = grp.channel_dependencies[ch_nr]
master_is_required = not samples_only or bool(raster)
master_index = self.masters_db.get(gp_nr, None)
vals: NDArray[Any] | None = None
all_invalid = False
if channel.byte_offset + (channel.bit_offset + channel.bit_count) / 8 > grp.channel_group.samples_byte_nr:
all_invalid = True
logger.warning(
"\n\t".join(
[
f"Channel {channel.name} byte offset too high:",
f"byte offset = {channel.byte_offset}",
f"bit offset = {channel.bit_offset}",
f"bit count = {channel.bit_count}",
f"group record size = {grp.channel_group.samples_byte_nr}",
f"group index = {gp_nr}",
f"channel index = {ch_nr}",
]
)
)
if (channel.bit_offset + channel.bit_count) / 8 > grp.channel_group.samples_byte_nr:
vals = np.array([])
else:
channel = deepcopy(channel)
channel.byte_offset = 0
if vals is None:
if dependency_list:
if not isinstance(dependency_list[0], ChannelArrayBlock):
dependency_list = typing.cast(list[tuple[int, int]], dependency_list)
samples, timestamps, invalidation_bits, encoding = self._get_structure(
channel=channel,
group=grp,
group_index=gp_nr,
channel_index=ch_nr,
dependency_list=dependency_list,
raster=raster,
data=data,
ignore_invalidation_bits=ignore_invalidation_bits,
record_offset=record_offset,
record_count=record_count,
master_is_required=master_is_required,
raw=raw,
)
else:
dependency_list = typing.cast(list[ChannelArrayBlock], dependency_list)
samples, timestamps, invalidation_bits, encoding = self._get_array(
channel=channel,
group=grp,
group_index=gp_nr,
channel_index=ch_nr,
dependency_list=dependency_list,
raster=raster,
data=data,
ignore_invalidation_bits=ignore_invalidation_bits,
record_offset=record_offset,
record_count=record_count,
master_is_required=master_is_required,
)
else:
grp.load_all_data_blocks()
blocks = grp.data_blocks
record_size = grp.channel_group.samples_byte_nr + grp.channel_group.invalidation_bytes_nr
cycles_nr = grp.channel_group.cycles_nr
if (
data is None
and validate_blocks(blocks, record_size)
and not self._from_filelike
and not grp.signal_data[ch_nr]
and grp.record[ch_nr]
and (
not master_is_required
or (master_is_required and master_index is not None and grp.record[master_index])
)
):
if grp.data_location == v4c.LOCATION_ORIGINAL_FILE:
file_name = self._mapped_file.name
else:
file_name = self._tempfile.name
signals = []
channels = []
info_rec = []
if master_is_required:
master_channel = grp.channels[master_index]
channel_dtype, byte_size, byte_offset, bit_offset = info = typing.cast(
tuple[np.dtype[Any], int, int, int], grp.record[master_index]
)
signals.append((byte_offset, byte_size, master_channel.pos_invalidation_bit))
channels.append(master_channel)
info_rec.append(info)
channel_dtype, byte_size, byte_offset, bit_offset = info = grp.record[ch_nr]
signals.append((byte_offset, byte_size, channel.pos_invalidation_bit))
channels.append(channel)
info_rec.append(info)
raw_and_invalidation = get_channel_raw_bytes_complete(
blocks,
signals,
file_name,
cycles_nr,
record_size,
grp.channel_group.invalidation_bytes_nr,
gp_nr,
THREAD_COUNT,
)
extracted_signals = []
for info, channel, (raw_data, invalidation_bits) in zip(
info_rec, channels, raw_and_invalidation, strict=False
):
channel_dtype, byte_size, byte_offset, bit_offset = info
vals = np.frombuffer(raw_data, dtype=channel_dtype)
data_type = channel.data_type
if not channel.standard_C_size:
size = byte_size
if channel_dtype.byteorder == "=" and data_type in (
v4c.DATA_TYPE_SIGNED_MOTOROLA,
v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
):
view = np.dtype(f">u{vals.itemsize}")
else:
view = np.dtype(f"{channel_dtype.byteorder}u{vals.itemsize}")
if view != vals.dtype:
vals = vals.view(view)
if bit_offset:
vals >>= bit_offset
if channel.bit_count != size * 8:
if data_type in v4c.SIGNED_INT:
vals = as_non_byte_sized_signed_int(vals, channel.bit_count)
else:
mask = (1 << channel.bit_count) - 1
vals &= mask
elif data_type in v4c.SIGNED_INT:
view = f"{channel_dtype.byteorder}i{vals.itemsize}"
if np.dtype(view) != vals.dtype:
vals = vals.view(view)
extracted_signals.append((vals, invalidation_bits))
if master_is_required:
master, signal = extracted_signals
if master_channel.conversion:
master = master_channel.conversion.convert(master[0])
else:
master = master[0]
samples, timestamps, invalidation_bits, encoding = signal[0], master, signal[1], None
else:
signal = extracted_signals[0]
samples, timestamps, invalidation_bits, encoding = signal[0], None, signal[1], None
if record_offset:
if record_count is None:
samples = samples[record_offset:]
timestamps = timestamps[record_offset:]
if invalidation_bits is not None:
invalidation_bits = invalidation_bits[record_offset:]
else:
end = record_offset + record_count
samples = samples[record_offset:end]
timestamps = timestamps[record_offset:end]
if invalidation_bits is not None:
invalidation_bits = invalidation_bits[record_offset:end]
else:
if record_count is not None:
samples = samples[:record_count]
timestamps = timestamps[:record_count]
if invalidation_bits is not None:
invalidation_bits = invalidation_bits[:record_count]
else:
if (
(fast_path := channel.fast_path) is not None
and not master_is_required
and ignore_invalidation_bits
and not raster
):
samples, timestamps, invalidation_bits, encoding = self._fast_scalar_path(*fast_path, data)
else:
samples, timestamps, invalidation_bits, encoding = self._get_scalar(
channel=channel,
group=grp,
group_index=gp_nr,
channel_index=ch_nr,
dependency_list=dependency_list,
raster=raster,
data=data,
ignore_invalidation_bits=ignore_invalidation_bits,
record_offset=record_offset,
record_count=record_count,
master_is_required=master_is_required,
)
else:
samples = vals
timestamps = np.array([], dtype=np.float64)
invalidation_bits = None
encoding = None
if all_invalid:
invalidation_bits = np.ones(len(samples), dtype=bool)
conversion = channel.conversion
res: tuple[NDArray[Any], NDArray[np.bool] | None] | Signal
if samples_only:
if not raw:
if conversion:
samples = conversion.convert(samples)
samples = samples.view(
np.dtype(
samples.dtype,
metadata={"conversion": conversion, "unit": (conversion and conversion.unit) or channel.unit},
)
)
res = samples, invalidation_bits
else:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None if 'samples_only' is False")
if not raw:
if conversion:
samples = conversion.convert(samples)
conversion = None
if samples.dtype.kind == "S":
encoding = "utf-8"
channel_type = channel.channel_type
if name is None:
name = channel.name
unit = (conversion and conversion.unit) or channel.unit
samples = samples.view(
np.dtype(
samples.dtype,
metadata={"conversion": conversion, "unit": (conversion and conversion.unit) or channel.unit},
)
)
comment = channel.comment
source_information = channel.source
if source_information:
source = Source.from_source(source_information)
else:
cg_source = grp.channel_group.acq_source
if cg_source:
source = Source.from_source(cg_source)
else:
source = None
if channel.attachment is not None:
try:
attachment = self.extract_attachment(
channel.attachment,
)
except:
print(format_exc())
attachment = None
else:
attachment = None
master_metadata = self._master_channel_metadata.get(gp_nr, None)
if channel_type == v4c.CHANNEL_TYPE_SYNC:
flags = Signal.Flags.stream_sync
else:
flags = Signal.Flags.no_flags
try:
res = Signal(
samples=samples,
timestamps=timestamps,
unit=unit,
name=name,
comment=comment,
conversion=conversion,
master_metadata=master_metadata,
attachment=attachment,
source=source,
display_names=channel.display_names,
bit_count=channel.bit_count,
flags=flags,
invalidation_bits=invalidation_bits,
encoding=encoding,
group_index=gp_nr,
channel_index=ch_nr,
)
except:
debug_channel(self, grp, channel, dependency_list)
raise
if data is None:
self._invalidation_cache.clear()
return res
@overload
def _get_structure(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[tuple[int, int]],
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: Literal[False],
record_offset: int,
record_count: int | None,
master_is_required: Literal[False],
raw: bool,
) -> tuple[NDArray[Any], None, NDArray[np.bool] | None, None]: ...
@overload
def _get_structure(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[tuple[int, int]],
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: Literal[True],
record_offset: int,
record_count: int | None,
master_is_required: Literal[False],
raw: bool,
) -> tuple[NDArray[Any], None, None, None]: ...
@overload
def _get_structure(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[tuple[int, int]],
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: Literal[False],
record_offset: int,
record_count: int | None,
master_is_required: Literal[True],
raw: bool,
) -> tuple[NDArray[Any], NDArray[Any], NDArray[np.bool] | None, None]: ...
@overload
def _get_structure(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[tuple[int, int]],
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: Literal[True],
record_offset: int,
record_count: int | None,
master_is_required: Literal[True],
raw: bool,
) -> tuple[NDArray[Any], NDArray[Any], None, None]: ...
@overload
def _get_structure(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[tuple[int, int]],
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: bool,
record_offset: int,
record_count: int | None,
master_is_required: bool,
raw: bool,
) -> tuple[NDArray[Any], NDArray[Any] | None, NDArray[np.bool], None]: ...
def _get_structure(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[tuple[int, int]],
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: bool,
record_offset: int,
record_count: int | None,
master_is_required: bool,
raw: bool,
) -> tuple[NDArray[Any], NDArray[Any] | None, NDArray[np.bool] | None, None]:
grp = group
gp_nr = group_index
# get data group record
self._prepare_record(grp)
# get group data
if data is None:
fragments = self._load_data(grp, record_offset=record_offset, record_count=record_count)
one_piece = False
else:
fragments = iter((data,))
one_piece = True
groups = self.groups
channel_invalidation_present = (not self._ignore_invalidation_bits) and channel.flags & (
v4c.FLAG_CN_ALL_INVALID | v4c.FLAG_CN_INVALIDATION_PRESENT
)
_dtype = np.dtype(channel.dtype_fmt)
conditions = [
_dtype.itemsize == channel.bit_count // 8,
all(
groups[dg_nr].channels[ch_nr].channel_type != v4c.CHANNEL_TYPE_VLSD
for (dg_nr, ch_nr) in dependency_list
),
]
channel_values: list[NDArray[Any]] | list[list[NDArray[Any]]]
if all(conditions):
fast_path = True
flat_channel_values: list[NDArray[Any]] = []
masters: list[NDArray[Any]] = []
invalidation_arrays: list[InvalidationArray | None] = []
byte_offset = channel.byte_offset
record_size = grp.channel_group.samples_byte_nr + grp.channel_group.invalidation_bytes_nr
count = 0
for fragment in fragments:
bts = fragment.data
buffer = get_channel_raw_bytes(bts, record_size, byte_offset, _dtype.itemsize)
flat_channel_values.append(frombuffer(buffer, dtype=_dtype))
if master_is_required:
masters.append(self.get_master(gp_nr, fragment, one_piece=True))
if channel_invalidation_present:
invalidation_arrays.append(
self.get_invalidation_bits(gp_nr, channel.pos_invalidation_bit, fragment, one_piece=one_piece)
)
count += 1
channel_values = flat_channel_values
else:
unique_names = UniqueDB()
fast_path = False
names = [unique_names.get_unique_name(grp.channels[ch_nr].name) for _, ch_nr in dependency_list]
channel_values_list: list[list[NDArray[Any]]] = [[] for _ in dependency_list]
masters = []
invalidation_arrays = []
count = 0
for fragment in fragments:
for i, (dg_nr, ch_nr) in enumerate(dependency_list):
vals = self.get(
group=dg_nr,
index=ch_nr,
samples_only=True,
data=fragment,
ignore_invalidation_bits=ignore_invalidation_bits,
record_offset=record_offset,
record_count=record_count,
raw=raw,
)[0]
channel_values_list[i].append(vals)
if master_is_required:
masters.append(self.get_master(gp_nr, fragment, one_piece=True))
if channel_invalidation_present:
invalidation_arrays.append(
self.get_invalidation_bits(gp_nr, channel.pos_invalidation_bit, fragment, one_piece=one_piece)
)
count += 1
channel_values = channel_values_list
if fast_path:
flat_channel_values = typing.cast(list[NDArray[Any]], channel_values)
total_size = sum(len(_) for _ in flat_channel_values)
shape = (total_size, *flat_channel_values[0].shape[1:])
if count > 1:
out = empty(shape, dtype=flat_channel_values[0].dtype)
vals = concatenate(flat_channel_values, out=out)
else:
vals = flat_channel_values[0]
else:
channel_values_list = typing.cast(list[list[NDArray[Any]]], channel_values)
total_size = sum(len(_) for _ in channel_values_list[0])
if count > 1:
arrays: list[NDArray[Any]] = []
for lst in channel_values_list:
shape_len = len(lst[0].shape)
# fix bytearray signals if the length changes between the chunks
if shape_len == 2:
shape = (total_size,)
vlsd_max_size = max(l.shape[1] for l in lst)
shape = (*shape, vlsd_max_size)
max_vlsd_arrs = []
for arr in lst:
if arr.shape[1] < vlsd_max_size:
arr = np.hstack(
(
arr,
np.zeros(
(
arr.shape[0],
vlsd_max_size - arr.shape[1],
),
dtype=arr.dtype,
),
)
)
max_vlsd_arrs.append(arr)
arr = concatenate(
max_vlsd_arrs,
out=empty(shape, dtype=max_vlsd_arrs[0].dtype),
)
arrays.append(arr)
elif shape_len == 1:
arr = concatenate(
lst,
out=empty((total_size,), dtype=lst[0].dtype),
)
arrays.append(arr)
else:
arrays.append(
concatenate(
lst,
out=empty((total_size, *lst[0].shape[1:]), dtype=lst[0].dtype),
)
)
else:
arrays = [lst[0] for lst in channel_values_list]
types: DTypeLike = [(name_, arr.dtype, arr.shape[1:]) for name_, arr in zip(names, arrays, strict=False)]
types = np.dtype(types)
vals = np.rec.fromarrays(arrays, dtype=types)
if master_is_required:
if count > 1:
out = empty(total_size, dtype=masters[0].dtype)
timestamps = concatenate(masters, out=out)
else:
timestamps = masters[0]
else:
timestamps = None
if channel_invalidation_present:
if one_piece:
invalidation_array = invalidation_arrays[0]
if invalidation_array is None:
invalidation_bits = None
elif not ignore_invalidation_bits:
vals = vals[nonzero(~invalidation_array)[0]]
if master_is_required:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None if 'master_is_required' is True")
timestamps = timestamps[nonzero(~invalidation_array)[0]]
invalidation_bits = None
else:
invalidation_bits = invalidation_array
else:
if count > 1:
out = empty(total_size, dtype=invalidation_arrays[0].dtype)
invalidation_array = concatenate(invalidation_arrays, out=out)
else:
invalidation_array = invalidation_arrays[0]
if not ignore_invalidation_bits:
vals = vals[nonzero(~invalidation_array)[0]]
if master_is_required:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None if 'master_is_required' is True")
timestamps = timestamps[nonzero(~invalidation_array)[0]]
invalidation_bits = None
else:
invalidation_bits = invalidation_array
else:
invalidation_bits = None
if raster:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None if 'raster' is not None")
if len(timestamps) > 1:
t = arange(timestamps[0], timestamps[-1], raster, dtype=np.float64)
signal = Signal(vals, timestamps, name="_", invalidation_bits=invalidation_bits).interp(
t,
integer_interpolation_mode=self._integer_interpolation,
float_interpolation_mode=self._float_interpolation,
)
vals, timestamps, invalidation_bits = (
signal.samples,
signal.timestamps,
signal.invalidation_bits,
)
return vals, timestamps, invalidation_bits, None
@overload
def _get_array(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[ChannelArrayBlock],
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: Literal[False],
record_offset: int,
record_count: int | None,
master_is_required: Literal[False],
) -> tuple[NDArray[Any], None, NDArray[np.bool] | None, None]: ...
@overload
def _get_array(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[ChannelArrayBlock],
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: Literal[True],
record_offset: int,
record_count: int | None,
master_is_required: Literal[False],
) -> tuple[NDArray[Any], None, None, None]: ...
@overload
def _get_array(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[ChannelArrayBlock],
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: Literal[False],
record_offset: int,
record_count: int | None,
master_is_required: Literal[True],
) -> tuple[NDArray[Any], NDArray[Any], NDArray[np.bool] | None, None]: ...
@overload
def _get_array(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[ChannelArrayBlock],
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: Literal[True],
record_offset: int,
record_count: int | None,
master_is_required: Literal[True],
) -> tuple[NDArray[Any], NDArray[Any], None, None]: ...
@overload
def _get_array(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[ChannelArrayBlock],
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: bool,
record_offset: int,
record_count: int | None,
master_is_required: bool,
) -> tuple[NDArray[Any], NDArray[Any] | None, NDArray[np.bool] | None, None]: ...
def _get_array(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[ChannelArrayBlock],
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: bool,
record_offset: int,
record_count: int | None,
master_is_required: bool,
) -> tuple[NDArray[Any], NDArray[Any] | None, NDArray[np.bool] | None, None]:
grp = group
gp_nr = group_index
ch_nr = channel_index
# get data group record
self._prepare_record(grp)
# get group data
if data is None:
fragments = self._load_data(grp, record_offset=record_offset, record_count=record_count)
one_piece = False
else:
fragments = iter((data,))
one_piece = True
axes = []
for dep in dependency_list:
axes.extend(dep.get_axes_information())
shape = tuple(axis["size"] for axis in axes)
shape = shape or (1,)
size = prod(shape, start=(channel.bit_count // 8) or 1)
if group.uses_ld:
record_size = group.channel_group.samples_byte_nr
else:
record_size = group.channel_group.samples_byte_nr + group.channel_group.invalidation_bytes_nr
channel_dtype = get_fmt_v4(
channel.data_type,
channel.bit_count,
channel.channel_type,
)
byte_offset = channel.byte_offset
dtype_fmt: np.dtype[Any] = np.dtype(
[
("", f"S{byte_offset}"),
("vals", channel_dtype, shape),
("", f"S{record_size - size - byte_offset}"),
]
)
channel_invalidation_present = (not self._ignore_invalidation_bits) and channel.flags & (
v4c.FLAG_CN_ALL_INVALID | v4c.FLAG_CN_INVALIDATION_PRESENT
)
inverse_layout = any(axis["inverse_layout"] for axis in axes)
channel_group = grp.channel_group
samples_size = channel_group.samples_byte_nr + channel_group.invalidation_bytes_nr
channel_values: list[NDArray[Any]] = []
masters: list[NDArray[Any]] = []
invalidation_arrays: list[InvalidationArray | None] = []
count = 0
timestamps: NDArray[Any] | None
for fragment in fragments:
arrays: list[NDArray[Any]] = []
types: list[tuple[str, np.dtype[Any], tuple[int, ...]]] = []
data_bytes, offset, _count, invalidation_bytes = (
fragment.data,
fragment.record_offset,
fragment.record_count,
fragment.invalidation_data,
)
cycles = len(data_bytes) // samples_size
vals = frombuffer(data_bytes, dtype=dtype_fmt)["vals"]
if inverse_layout:
shape = vals.shape
shape = (shape[0], *shape[1:][::-1])
vals = vals.reshape(shape)
arr_axes = (0, *reversed(range(1, len(shape))))
vals = transpose(vals, axes=arr_axes)
cycles_nr = len(vals)
dtype_pair: tuple[str, np.dtype[Any], tuple[int, ...]]
shape = vals.shape[1:]
arrays.append(vals)
dtype_pair = (
channel.name,
np.dtype(vals.dtype, metadata={"conversion": channel.conversion, "unit": channel.unit, "axes": axes}),
shape,
)
types.append(dtype_pair)
for i, axis in enumerate(axes):
shape = (axis["size"],)
match axis["type"]:
case "NO_AXIS" | "SCALE_AXIS":
pass
case "FIXED_AXIS":
fix_axis = array(axis["values"])
if cycles_nr:
axis_array = array([fix_axis for _ in range(cycles_nr)])
else:
axis_array = array([fix_axis])[:0]
arrays.append(axis_array)
dtype_pair = (
f"axis_{i}",
np.dtype(fix_axis.dtype, metadata={"axes": [axis], "conversion": axis["conversion"]}),
shape,
)
types.append(dtype_pair)
axis["dtype_field_name"] = f"axis_{i}"
case "REF_AXIS":
axis_channel = axis["ref"]
if axis_channel is None:
axisname = f"axis_{i}"
axis_values = array(
[arange(shape[0])] * cycles,
dtype=f"({shape[0]},)f8",
)
else:
try:
ref_dg_nr, ref_ch_nr = axis_channel
except:
debug_channel(self, grp, channel, dependency_list)
raise
axisname = self.groups[ref_dg_nr].channels[ref_ch_nr].name
if ref_dg_nr == gp_nr:
axis_values = self.get(
group=ref_dg_nr,
index=ref_ch_nr,
samples_only=True,
data=fragment,
ignore_invalidation_bits=ignore_invalidation_bits,
record_offset=record_offset,
record_count=cycles,
raw=True,
)[0]
else:
channel_group = grp.channel_group
record_size = channel_group.samples_byte_nr
record_size += channel_group.invalidation_bytes_nr
start = offset // record_size
end = start + len(data_bytes) // record_size + 1
ref = self.get(
group=ref_dg_nr,
index=ref_ch_nr,
samples_only=True,
ignore_invalidation_bits=ignore_invalidation_bits,
record_offset=record_offset,
record_count=cycles,
raw=True,
)[0]
axis_values = ref[start:end].copy()
axis_values = axis_values[axisname]
if len(axis_values) == 0 and cycles:
axis_values = array([arange(shape[0])] * cycles)
axis["ref_name"] = axisname
axis["dtype_field_name"] = axisname
arrays.append(axis_values)
dtype_pair = (
axisname,
np.dtype(
axis_values.dtype.base, metadata={"conversion": axis["conversion"], "axes": [axis]}
),
shape,
)
types.append(dtype_pair)
vals = np.rec.fromarrays(arrays, np.dtype(types))
if master_is_required:
masters.append(self.get_master(gp_nr, fragment, one_piece=True))
if channel_invalidation_present:
invalidation_arrays.append(
self.get_invalidation_bits(gp_nr, channel.pos_invalidation_bit, fragment, one_piece=one_piece)
)
channel_values.append(vals)
count += 1
if count > 1:
total_size = sum(len(_) for _ in channel_values)
shape = (total_size, *channel_values[0].shape[1:])
if count > 1:
out = empty(shape, dtype=channel_values[0].dtype)
vals = concatenate(channel_values, out=out)
elif count == 1:
vals = channel_values[0]
else:
vals = np.array([])
if master_is_required:
if count > 1:
out = empty(total_size, dtype=masters[0].dtype)
timestamps = concatenate(masters, out=out)
else:
timestamps = masters[0]
else:
timestamps = None
if channel_invalidation_present:
if one_piece:
invalidation_array = invalidation_arrays[0]
if invalidation_array is None:
invalidation_bits = None
elif not ignore_invalidation_bits:
vals = vals[nonzero(~invalidation_array)[0]]
if master_is_required:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None is 'master_is_required' is True")
timestamps = timestamps[nonzero(~invalidation_array)[0]]
invalidation_bits = None
else:
invalidation_bits = invalidation_array
else:
if count > 1:
out = empty(total_size, dtype=invalidation_arrays[0].dtype)
invalidation_array = concatenate(invalidation_arrays, out=out)
else:
invalidation_array = invalidation_arrays[0]
if not ignore_invalidation_bits:
vals = vals[nonzero(~invalidation_array)[0]]
if master_is_required:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None is 'master_is_required' is True")
timestamps = timestamps[nonzero(~invalidation_array)[0]]
invalidation_bits = None
else:
invalidation_bits = invalidation_array
else:
invalidation_bits = None
if raster:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None is 'raster' is not None")
if len(timestamps) > 1:
t = arange(timestamps[0], timestamps[-1], raster, dtype=np.float64)
signal = Signal(vals, timestamps, name="_", invalidation_bits=invalidation_bits).interp(
t,
integer_interpolation_mode=self._integer_interpolation,
float_interpolation_mode=self._float_interpolation,
)
vals, timestamps, invalidation_bits = (
signal.samples,
signal.timestamps,
signal.invalidation_bits,
)
return vals, timestamps, invalidation_bits, None
def _fast_scalar_path(
self,
gp_nr: int,
record_size: int,
byte_offset: int,
byte_size: int,
pos_invalidation_bit: int,
# data_type,
# channel_type,
# bit_count,
dtype: np.dtype[Any],
fragment: Fragment,
) -> tuple[NDArray[Any], None, InvalidationArray | None, None]:
buffer: Buffer
if fragment.is_record:
buffer = get_channel_raw_bytes(
fragment.data,
record_size,
byte_offset,
byte_size,
)
else:
buffer = fragment.data
vals = frombuffer(buffer, dtype=dtype)
if pos_invalidation_bit >= 0:
invalidation_bits = self.get_invalidation_bits(gp_nr, pos_invalidation_bit, fragment, one_piece=True)
else:
invalidation_bits = None
return vals, None, invalidation_bits, None
@overload
def _get_scalar(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[ChannelArrayBlock] | list[tuple[int, int]] | None,
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: Literal[False],
record_offset: int,
record_count: int | None,
master_is_required: Literal[False],
skip_vlsd: bool = ...,
) -> tuple[NDArray[Any], None, NDArray[np.bool] | None, str | None]: ...
@overload
def _get_scalar(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[ChannelArrayBlock] | list[tuple[int, int]] | None,
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: Literal[True],
record_offset: int,
record_count: int | None,
master_is_required: Literal[False],
skip_vlsd: bool = ...,
) -> tuple[NDArray[Any], None, None, str | None]: ...
@overload
def _get_scalar(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[ChannelArrayBlock] | list[tuple[int, int]] | None,
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: Literal[False],
record_offset: int,
record_count: int | None,
master_is_required: Literal[True],
skip_vlsd: bool = ...,
) -> tuple[NDArray[Any], NDArray[Any], NDArray[np.bool] | None, str | None]: ...
@overload
def _get_scalar(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[ChannelArrayBlock] | list[tuple[int, int]] | None,
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: Literal[True],
record_offset: int,
record_count: int | None,
master_is_required: Literal[True],
skip_vlsd: bool = ...,
) -> tuple[NDArray[Any], NDArray[Any], None, str | None]: ...
@overload
def _get_scalar(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[ChannelArrayBlock] | list[tuple[int, int]] | None,
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: bool,
record_offset: int,
record_count: int | None,
master_is_required: bool,
skip_vlsd: bool = ...,
) -> tuple[NDArray[Any], NDArray[Any] | None, NDArray[np.bool] | None, str | None]: ...
def _get_scalar(
self,
channel: Channel,
group: Group,
group_index: int,
channel_index: int,
dependency_list: list[ChannelArrayBlock] | list[tuple[int, int]] | None,
raster: RasterType | None,
data: Fragment | None,
ignore_invalidation_bits: bool,
record_offset: int,
record_count: int | None,
master_is_required: bool,
skip_vlsd: bool = False,
) -> tuple[NDArray[Any], NDArray[Any] | None, NDArray[np.bool] | None, str | None]:
grp = group
# get group data
if data is None:
fragments = self._load_data(grp, record_offset=record_offset, record_count=record_count)
one_piece = False
else:
fragments = iter((data,))
one_piece = True
gp_nr = group_index
ch_nr = channel_index
channel_invalidation_present = (not self._ignore_invalidation_bits) and channel.flags & (
v4c.FLAG_CN_ALL_INVALID | v4c.FLAG_CN_INVALIDATION_PRESENT
)
data_type = channel.data_type
channel_type = channel.channel_type
bit_count = channel.bit_count
encoding: str | None = None
channel_dtype = channel.dtype_fmt
vals: NDArray[Any]
timestamps: NDArray[Any] | None = None
invalidation_bits: NDArray[np.bool] | None = None
# get channel values
if channel_type in {
v4c.CHANNEL_TYPE_VIRTUAL,
v4c.CHANNEL_TYPE_VIRTUAL_MASTER,
}:
channel_values: list[NDArray[Any]] = []
masters: list[NDArray[Any]] = []
invalidation_arrays: list[InvalidationArray | None] = []
channel_group = grp.channel_group
record_size = channel_group.samples_byte_nr
record_size += channel_group.invalidation_bytes_nr
ch_dtype = np.min_scalar_type(channel_group.cycles_nr)
channel.dtype_fmt = ch_dtype
count = 0
for fragment in fragments:
data_bytes, offset, _count, invalidation_bytes = (
fragment.data,
fragment.record_offset,
fragment.record_count,
fragment.invalidation_data,
)
offset = offset // record_size
vals = arange(len(data_bytes) // record_size, dtype=ch_dtype)
np.add(vals, offset, out=vals, casting="unsafe")
if master_is_required:
masters.append(
self.get_master(
gp_nr,
fragment,
record_offset=offset,
record_count=_count,
one_piece=True,
)
)
if channel_invalidation_present:
invalidation_arrays.append(
self.get_invalidation_bits(gp_nr, channel.pos_invalidation_bit, fragment, one_piece=one_piece)
)
channel_values.append(vals)
count += 1
if count > 1:
total_size = sum(len(_) for _ in channel_values)
shape = (total_size, *channel_values[0].shape[1:])
if count > 1:
out = empty(shape, dtype=channel_values[0].dtype)
vals = concatenate(channel_values, out=out)
elif count == 1:
vals = channel_values[0]
else:
vals = np.array([])
if master_is_required:
if count > 1:
out = empty(total_size, dtype=masters[0].dtype)
timestamps = concatenate(masters, out=out)
else:
timestamps = masters[0]
else:
timestamps = None
if channel_invalidation_present:
if one_piece:
invalidation_array = invalidation_arrays[0]
if invalidation_array is None:
invalidation_bits = None
elif not ignore_invalidation_bits:
vals = vals[nonzero(~invalidation_array)[0]]
if master_is_required:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None if 'master_is_required' is True")
timestamps = timestamps[nonzero(~invalidation_array)[0]]
invalidation_bits = None
else:
invalidation_bits = invalidation_array
else:
if count > 1:
out = empty(total_size, dtype=invalidation_arrays[0].dtype)
invalidation_array = concatenate(invalidation_arrays, out=out)
else:
invalidation_array = invalidation_arrays[0]
if not ignore_invalidation_bits:
vals = vals[nonzero(~invalidation_array)[0]]
if master_is_required:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None if 'master_is_required' is True")
timestamps = timestamps[nonzero(~invalidation_array)[0]]
invalidation_bits = None
else:
invalidation_bits = invalidation_array
else:
invalidation_bits = None
if raster:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None if 'raster' is not None")
if len(timestamps) > 1:
num = float(float32((timestamps[-1] - timestamps[0]) / raster))
if num.is_integer():
t = linspace(timestamps[0], timestamps[-1], int(num))
else:
t = arange(timestamps[0], timestamps[-1], raster)
signal = Signal(vals, timestamps, name="_", invalidation_bits=invalidation_bits).interp(
t,
integer_interpolation_mode=self._integer_interpolation,
float_interpolation_mode=self._float_interpolation,
)
vals, timestamps, invalidation_bits = (
signal.samples,
signal.timestamps,
signal.invalidation_bits,
)
else:
channel_group = grp.channel_group
record_size = channel_group.samples_byte_nr
if one_piece:
fragment = next(fragments)
data_bytes, rec_offset, rec_count = fragment.data, fragment.record_offset, fragment.record_count
record = self._prepare_record(grp)
info = record[ch_nr]
if info is not None:
dtype_, byte_size, byte_offset, bit_offset = info
buffer: Buffer
if ch_nr == 0 and len(grp.channels) == 1 and channel.dtype_fmt.itemsize == record_size:
buffer = bytearray(data_bytes)
else:
if fragment.is_record:
buffer = get_channel_raw_bytes(
data_bytes,
record_size + channel_group.invalidation_bytes_nr,
byte_offset,
byte_size,
)
else:
buffer = data_bytes
vals = frombuffer(buffer, dtype=dtype_)
if not channel.standard_C_size:
size = byte_size
view: DTypeLike
if channel_dtype.byteorder == "=" and data_type in (
v4c.DATA_TYPE_SIGNED_MOTOROLA,
v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
):
view = np.dtype(f">u{vals.itemsize}")
else:
view = np.dtype(f"{channel_dtype.byteorder}u{vals.itemsize}")
if view != vals.dtype:
vals = vals.view(view)
if bit_offset:
vals >>= bit_offset
if bit_count != size * 8:
if data_type in v4c.SIGNED_INT:
vals = as_non_byte_sized_signed_int(vals, bit_count)
else:
mask = (1 << bit_count) - 1
vals &= mask
elif data_type in v4c.SIGNED_INT:
view = np.dtype(f"{channel_dtype.byteorder}i{vals.itemsize}")
if view != vals.dtype:
vals = vals.view(view)
elif channel_type == v4c.CHANNEL_TYPE_VALUE and channel.fast_path is None:
channel.fast_path = (
gp_nr,
record_size + channel_group.invalidation_bytes_nr,
byte_offset,
byte_size,
channel.pos_invalidation_bit if channel_invalidation_present else -1,
# data_type,
# channel_type,
# bit_count,
dtype_,
)
else:
vals = self._get_not_byte_aligned_data(data_bytes, grp, ch_nr)
if bit_count == 1 and self._single_bit_uint_as_bool:
vals = array(vals, dtype=bool)
if master_is_required:
timestamps = self.get_master(gp_nr, fragment, one_piece=True)
else:
timestamps = None
if channel_invalidation_present:
invalidation_array = self.get_invalidation_bits(
gp_nr, channel.pos_invalidation_bit, fragment, one_piece=one_piece
)
if invalidation_array is None:
invalidation_bits = None
elif not ignore_invalidation_bits:
vals = vals[nonzero(~invalidation_array)[0]]
if master_is_required:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None if 'master_is_required' is True")
timestamps = timestamps[nonzero(~invalidation_array)[0]]
invalidation_bits = None
else:
invalidation_bits = invalidation_array
else:
invalidation_bits = None
else:
channel_values = []
masters = []
invalidation_arrays = []
record = self._prepare_record(grp)
info = record[ch_nr]
if info is None:
for count, fragment in enumerate(fragments, 1):
data_bytes, offset, _count, invalidation_bytes = (
fragment.data,
fragment.record_offset,
fragment.record_count,
fragment.invalidation_data,
)
vals = self._get_not_byte_aligned_data(data_bytes, grp, ch_nr)
if bit_count == 1 and self._single_bit_uint_as_bool:
vals = array(vals, dtype=bool)
if master_is_required:
masters.append(self.get_master(gp_nr, fragment, one_piece=True))
if channel_invalidation_present:
invalidation_arrays.append(
self.get_invalidation_bits(
gp_nr, channel.pos_invalidation_bit, fragment, one_piece=one_piece
)
)
channel_values.append(vals)
vals = concatenate(channel_values)
else:
dtype_, byte_size, byte_offset, bit_offset = info
buffers: list[bytearray] = []
count = 0
for count, fragment in enumerate(fragments, 1):
data_bytes = fragment.data
if ch_nr == 0 and len(grp.channels) == 1 and channel.dtype_fmt.itemsize == record_size:
buffers.append(bytearray(data_bytes))
else:
buffers.append(
get_channel_raw_bytes(
data_bytes,
record_size + channel_group.invalidation_bytes_nr,
byte_offset,
byte_size,
)
)
if master_is_required:
masters.append(self.get_master(gp_nr, fragment, one_piece=True))
if channel_invalidation_present:
invalidation_arrays.append(
self.get_invalidation_bits(
gp_nr, channel.pos_invalidation_bit, fragment, one_piece=one_piece
)
)
if count > 1:
buffer = bytearray().join(buffers)
elif count == 1:
buffer = buffers[0]
else:
buffer = bytearray()
vals = frombuffer(buffer, dtype=dtype_)
if not channel.standard_C_size:
size = dtype_.itemsize
if channel_dtype.byteorder == "=" and data_type in (
v4c.DATA_TYPE_SIGNED_MOTOROLA,
v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
):
view = np.dtype(f">u{vals.itemsize}")
else:
view = np.dtype(f"{channel_dtype.byteorder}u{vals.itemsize}")
if view != dtype_:
vals = vals.view(view)
if bit_offset:
vals >>= bit_offset
if bit_count != size * 8:
if data_type in v4c.SIGNED_INT:
vals = as_non_byte_sized_signed_int(vals, bit_count)
else:
mask = (1 << bit_count) - 1
vals &= mask
elif data_type in v4c.SIGNED_INT:
view = np.dtype(f"{channel_dtype.byteorder}i{vals.itemsize}")
if view != vals.dtype:
vals = vals.view(view)
if bit_count == 1 and self._single_bit_uint_as_bool:
vals = array(vals, dtype=bool)
total_size = len(vals)
if master_is_required:
if count > 1:
out = empty(total_size, dtype=masters[0].dtype)
timestamps = concatenate(masters, out=out)
elif count == 1:
timestamps = masters[0]
else:
timestamps = np.array([], dtype=np.float64)
else:
timestamps = None
if channel_invalidation_present:
if count > 1:
out = empty(total_size, dtype=invalidation_arrays[0].dtype)
invalidation_array = concatenate(invalidation_arrays, out=out)
elif count == 1:
invalidation_array = invalidation_arrays[0]
else:
invalidation_array = typing.cast(
np.ndarray[tuple[int], np.dtype[np.bool]], np.array([], dtype=np.bool)
)
if not ignore_invalidation_bits:
vals = vals[nonzero(~invalidation_array)[0]]
if master_is_required:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None if 'master_is_required' is True")
timestamps = timestamps[nonzero(~invalidation_array)[0]]
invalidation_bits = None
else:
invalidation_bits = invalidation_array
else:
invalidation_bits = None
if raster:
if timestamps is None:
raise RuntimeError("'timestamps' cannot be None if 'raster' is not None")
if len(timestamps) > 1:
num = float(float32((timestamps[-1] - timestamps[0]) / raster))
if num.is_integer():
t = linspace(timestamps[0], timestamps[-1], int(num))
else:
t = arange(timestamps[0], timestamps[-1], raster)
signal = Signal(vals, timestamps, name="_", invalidation_bits=invalidation_bits).interp(
t,
integer_interpolation_mode=self._integer_interpolation,
float_interpolation_mode=self._float_interpolation,
)
vals, timestamps, invalidation_bits = (
signal.samples,
signal.timestamps,
signal.invalidation_bits,
)
if channel_type == v4c.CHANNEL_TYPE_VLSD and not skip_vlsd:
count_ = len(vals)
if count_:
signal_data = self._load_signal_data(group=grp, index=ch_nr, offsets=(vals[0], vals[-1]))
else:
signal_data = b""
max_vlsd_size = self.determine_max_vlsd_sample_size(group_index, channel_index)
if signal_data:
if data_type in (
v4c.DATA_TYPE_BYTEARRAY,
v4c.DATA_TYPE_UNSIGNED_INTEL,
v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
v4c.DATA_TYPE_SIGNED_INTEL,
v4c.DATA_TYPE_SIGNED_MOTOROLA,
v4c.DATA_TYPE_MIME_SAMPLE,
v4c.DATA_TYPE_MIME_STREAM,
):
vals = extract(signal_data, True, vals - vals[0])
if vals.shape[1] < max_vlsd_size:
vals = np.hstack(
(
vals,
np.zeros(
(
vals.shape[0],
max_vlsd_size - vals.shape[1],
),
dtype=vals.dtype,
),
)
)
else:
vals = extract(signal_data, False, vals - vals[0])
if data_type not in (
v4c.DATA_TYPE_BYTEARRAY,
v4c.DATA_TYPE_UNSIGNED_INTEL,
v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
v4c.DATA_TYPE_SIGNED_INTEL,
v4c.DATA_TYPE_SIGNED_MOTOROLA,
v4c.DATA_TYPE_MIME_SAMPLE,
v4c.DATA_TYPE_MIME_STREAM,
):
match data_type:
case v4c.DATA_TYPE_STRING_UTF_16_BE:
encoding = "utf-16-be"
case v4c.DATA_TYPE_STRING_UTF_16_LE:
encoding = "utf-16-le"
case v4c.DATA_TYPE_STRING_UTF_8:
encoding = "utf-8"
vals = np.array(
[e.rsplit(b"\0")[0] for e in typing.cast(list[bytes], vals.tolist())],
dtype=vals.dtype,
)
case v4c.DATA_TYPE_STRING_LATIN_1:
encoding = "latin-1"
vals = np.array(
[e.rsplit(b"\0")[0] for e in typing.cast(list[bytes], vals.tolist())],
dtype=vals.dtype,
)
case _:
raise MdfException(f'wrong data type "{data_type}" for vlsd channel "{channel.name}"')
vals = vals.astype(f"S{max_vlsd_size}")
else:
if len(vals):
raise MdfException(
f'Wrong signal data block refence (0x{channel.data_block_addr:X}) for VLSD channel "{channel.name}"'
)
# no VLSD signal data samples
if data_type != v4c.DATA_TYPE_BYTEARRAY:
vals = array([], dtype=f"S{max_vlsd_size}")
match data_type:
case v4c.DATA_TYPE_STRING_UTF_16_BE:
encoding = "utf-16-be"
case v4c.DATA_TYPE_STRING_UTF_16_LE:
encoding = "utf-16-le"
case v4c.DATA_TYPE_STRING_UTF_8:
encoding = "utf-8"
case v4c.DATA_TYPE_STRING_LATIN_1:
encoding = "latin-1"
case _:
raise MdfException(f'wrong data type "{data_type}" for vlsd channel "{channel.name}"')
else:
vals = array([], dtype=f"({max_vlsd_size},)u1")
elif v4c.DATA_TYPE_STRING_LATIN_1 <= data_type <= v4c.DATA_TYPE_STRING_UTF_16_BE:
if channel_type in (v4c.CHANNEL_TYPE_VALUE, v4c.CHANNEL_TYPE_MLSD):
match data_type:
case v4c.DATA_TYPE_STRING_UTF_16_BE:
encoding = "utf-16-be"
case v4c.DATA_TYPE_STRING_UTF_16_LE:
encoding = "utf-16-le"
case v4c.DATA_TYPE_STRING_UTF_8:
encoding = "utf-8"
case v4c.DATA_TYPE_STRING_LATIN_1:
encoding = "latin-1"
case _:
raise MdfException(f'wrong data type "{data_type}" for string channel')
elif data_type in (v4c.DATA_TYPE_CANOPEN_TIME, v4c.DATA_TYPE_CANOPEN_DATE):
# CANopen date
if data_type == v4c.DATA_TYPE_CANOPEN_DATE:
types: np.dtype[Any] = np.dtype(
[
("ms", "<u2"),
("min", "<u1"),
("hour", "<u1"),
("day", "<u1"),
("month", "<u1"),
("year", "<u1"),
]
)
vals = vals.view(types)
arrays = [
vals["ms"],
vals["min"] & 0x3F, # bit 6 and 7 of minutes are reserved
vals["hour"] & 0xF, # only first 4 bits of hour are used
vals["day"] & 0xF, # the first 4 bits are the day number
vals["month"] & 0x3F, # bit 6 and 7 of month are reserved
vals["year"] & 0x7F, # bit 7 of year is reserved
(vals["hour"] & 0x80) >> 7, # add summer or standard time information for hour
(vals["day"] & 0xF0) >> 4, # add day of week information
]
names = [
"ms",
"min",
"hour",
"day",
"month",
"year",
"summer_time",
"day_of_week",
]
vals = np.rec.fromarrays(arrays, names=names) # type: ignore[call-overload]
# CANopen time
elif data_type == v4c.DATA_TYPE_CANOPEN_TIME:
types = np.dtype([("ms", "<u4"), ("days", "<u2")])
vals = vals.view(types)
return vals, timestamps, invalidation_bits, encoding
def _get_not_byte_aligned_data(self, data: bytes | bytearray, group: Group, ch_nr: int) -> NDArray[Any]:
big_endian_types = (
v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
v4c.DATA_TYPE_REAL_MOTOROLA,
v4c.DATA_TYPE_SIGNED_MOTOROLA,
)
if group.uses_ld:
record_size = group.channel_group.samples_byte_nr
else:
record_size = group.channel_group.samples_byte_nr + group.channel_group.invalidation_bytes_nr
channel = group.channels[ch_nr]
bit_offset = channel.bit_offset
byte_offset = channel.byte_offset
bit_count = channel.bit_count
if ch_nr >= 0:
dependencies = group.channel_dependencies[ch_nr]
if dependencies and isinstance(dependencies[0], ChannelArrayBlock):
ca_block = dependencies[0]
size = bit_count // 8
shape = tuple(typing.cast(int, ca_block[f"dim_size_{i}"]) for i in range(ca_block.dims))
if ca_block.byte_offset_base // size > 1 and len(shape) == 1:
shape += (ca_block.byte_offset_base // size,)
dim = 1
for d in shape:
dim *= d
size *= dim
bit_count = size * 8
byte_size = bit_offset + bit_count
if byte_size % 8:
byte_size = (byte_size // 8) + 1
else:
byte_size //= 8
types = [
("", f"S{byte_offset}"),
("vals", f"({byte_size},)u1"),
("", f"S{record_size - byte_size - byte_offset}"),
]
vals: NDArray[Any] = np.rec.fromstring(data, dtype=np.dtype(types))
vals = vals["vals"]
if byte_size in {1, 2, 4, 8}:
extra_bytes = 0
elif byte_size < 8:
extra_bytes = 4 - (byte_size % 4)
else:
extra_bytes = 0
std_size = byte_size + extra_bytes
big_endian = channel.data_type in big_endian_types
# prepend or append extra bytes columns
# to get a standard size number of bytes
if extra_bytes:
if big_endian:
vals = column_stack([vals, zeros(len(vals), dtype=f"<({extra_bytes},)u1")])
try:
vals = vals.view(f">u{std_size}").ravel()
except:
vals = frombuffer(vals.tobytes(), dtype=f">u{std_size}")
vals = vals >> (extra_bytes * 8 + bit_offset)
vals &= (1 << bit_count) - 1
else:
vals = column_stack([vals, zeros(len(vals), dtype=f"<({extra_bytes},)u1")])
try:
vals = vals.view(f"<u{std_size}").ravel()
except:
vals = frombuffer(vals.tobytes(), dtype=f"<u{std_size}")
vals = vals >> bit_offset
vals &= (1 << bit_count) - 1
else:
if big_endian:
try:
vals = vals.view(f">u{std_size}").ravel()
except:
vals = frombuffer(vals.tobytes(), dtype=f">u{std_size}")
vals = vals >> bit_offset
vals &= (1 << bit_count) - 1
else:
try:
vals = vals.view(f"<u{std_size}").ravel()
except:
vals = frombuffer(vals.tobytes(), dtype=f"<u{std_size}")
vals = vals >> bit_offset
vals &= (1 << bit_count) - 1
data_type = channel.data_type
if data_type in v4c.SIGNED_INT:
return as_non_byte_sized_signed_int(vals, bit_count)
elif data_type in v4c.FLOATS:
return vals.view(get_fmt_v4(data_type, bit_count))
else:
return vals
def _determine_max_vlsd_sample_size(self, group_idx: int, index: int) -> int:
group_index = group_idx
channel_index = index
group = self.groups[group_idx]
ch = group.channels[index]
if ch.channel_type != v4c.CHANNEL_TYPE_VLSD:
return 0
if (group_index, ch.name) in self.vlsd_max_length:
return self.vlsd_max_length[(group_index, ch.name)]
else:
offsets, *_ = self._get_scalar(
ch,
group,
group_index,
channel_index,
group.channel_dependencies[channel_index],
raster=None,
data=None,
ignore_invalidation_bits=True,
record_offset=0,
record_count=None,
master_is_required=False,
skip_vlsd=True,
)
offsets = offsets.astype("u8")
data = self._load_signal_data(group, channel_index)
max_size = get_vlsd_max_sample_size(data, offsets, len(offsets))
return max_size
def included_channels(
self,
index: int | None = None,
channels: ChannelsType | None = None,
skip_master: bool = True,
minimal: bool = True,
) -> dict[int, dict[int, list[int]]]:
if channels is None:
if index is None:
raise ValueError("index cannot be None if channels is None")
virtual_channel_group = self.virtual_groups[index]
groups = virtual_channel_group.groups
gps: dict[int, list[int]] = {}
for gp_index in groups:
group = self.groups[gp_index]
included_channels = set(range(len(group.channels)))
master_index = self.masters_db.get(gp_index, None)
if master_index is not None:
included_channels.remove(master_index)
for dependencies in group.channel_dependencies:
if dependencies is None:
continue
if all(not isinstance(dep, ChannelArrayBlock) for dep in dependencies):
dependencies = typing.cast(list[tuple[int, int]], dependencies)
for _, ch_nr in dependencies:
try:
included_channels.remove(ch_nr)
except KeyError:
pass
else:
dependencies = typing.cast(list[ChannelArrayBlock], dependencies)
for dep in dependencies:
for referenced_channels in (
dep.axis_channels,
dep.dynamic_size_channels,
dep.input_quantity_channels,
):
for gp_nr, ch_nr in filter(None, referenced_channels):
if gp_nr == gp_index:
try:
included_channels.remove(ch_nr)
except KeyError:
pass
if dep.output_quantity_channel:
gp_nr, ch_nr = dep.output_quantity_channel
if gp_nr == gp_index:
try:
included_channels.remove(ch_nr)
except KeyError:
pass
if dep.comparison_quantity_channel:
gp_nr, ch_nr = dep.comparison_quantity_channel
if gp_nr == gp_index:
try:
included_channels.remove(ch_nr)
except KeyError:
pass
gps[gp_index] = sorted(included_channels)
result = {index: gps}
else:
gps_set: dict[int, set[int]] = {}
for item in channels:
if isinstance(item, list | tuple):
if len(item) not in (2, 3):
raise MdfException(
"The items used for filtering must be strings, "
"or they must match the first 3 arguments of the get "
"method"
)
else:
gp_nr, idx = self._validate_channel_selection(*item)
gps_idx = gps_set.setdefault(gp_nr, set())
gps_idx.add(idx)
else:
name = item
gp_nr, idx = self._validate_channel_selection(name)
gps_idx = gps_set.setdefault(gp_nr, set())
gps_idx.add(idx)
result = {}
for gp_index, gps_idx in gps_set.items():
master = self.virtual_groups_map[gp_index]
group = self.groups[gp_index]
if minimal:
channel_dependencies = [group.channel_dependencies[ch_nr] for ch_nr in gps_idx]
for dependencies in channel_dependencies:
if dependencies is None:
continue
if all(not isinstance(dep, ChannelArrayBlock) for dep in dependencies):
dependencies = typing.cast(list[tuple[int, int]], dependencies)
for _, ch_nr in dependencies:
try:
gps_idx.remove(ch_nr)
except KeyError:
pass
else:
dependencies = typing.cast(list[ChannelArrayBlock], dependencies)
for dep in dependencies:
for referenced_channels in (
dep.axis_channels,
dep.dynamic_size_channels,
dep.input_quantity_channels,
):
for gp_nr, ch_nr in filter(None, referenced_channels):
if gp_nr == gp_index:
try:
gps_idx.remove(ch_nr)
except KeyError:
pass
if dep.output_quantity_channel:
gp_nr, ch_nr = dep.output_quantity_channel
if gp_nr == gp_index:
try:
gps_idx.remove(ch_nr)
except KeyError:
pass
if dep.comparison_quantity_channel:
gp_nr, ch_nr = dep.comparison_quantity_channel
if gp_nr == gp_index:
try:
gps_idx.remove(ch_nr)
except KeyError:
pass
gp_master = self.masters_db.get(gp_index, None)
if gp_master is not None and gp_master in gps_idx:
gps_idx.remove(gp_master)
if master not in result:
result[master] = {}
result[master][master] = [self.masters_db.get(master, None)] # type: ignore[list-item]
result[master][gp_index] = sorted(gps_idx)
return result
def _yield_selected_signals(
self,
index: int,
groups: dict[int, list[int]] | None = None,
record_offset: int = 0,
record_count: int | None = None,
skip_master: bool = True,
version: str | None = None,
) -> Iterator[list[Signal] | list[tuple[NDArray[Any], None]]]:
version = version or self.version
virtual_channel_group = self.virtual_groups[index]
record_size = virtual_channel_group.record_size
from time import perf_counter
tt = perf_counter()
if groups is None:
groups = self.included_channels(index, skip_master=skip_master)[index]
record_size = 0
for group_index in groups:
grp = self.groups[group_index]
record_size += grp.channel_group.samples_byte_nr + grp.channel_group.invalidation_bytes_nr
record_size = record_size or 1
if self._read_fragment_size:
count = self._read_fragment_size // record_size or 1
else:
if version < "4.20":
count = 64 * 1024 * 1024 // record_size or 1
else:
count = 128 * 1024 * 1024 // record_size or 1
data_streams: list[Iterator[Fragment]] = []
for idx, group_index in enumerate(groups):
grp = self.groups[group_index]
grp.read_split_count = count
data_streams.append(self._load_data(grp, record_offset=record_offset, record_count=record_count))
if group_index == index:
master_index = idx
encodings: dict[int, list[tuple[str | None, np.dtype[Any]] | None]] = {
group_index: [None] for group_index in groups
}
self._set_temporary_master(None)
idx = 0
group_info: dict[int, list[list[int]]] = {}
for group_index, channels in groups.items():
grp = self.groups[group_index]
if not grp.single_channel_dtype:
self._prepare_record(grp)
ch_info: list[list[int]] = []
group_info[group_index] = ch_info
dependency_list = grp.channel_dependencies
info = self._prepare_record(grp)
for channel_index in channels:
channel = grp.channels[channel_index]
if (
channel.byte_offset + (channel.bit_offset + channel.bit_count) / 8
> grp.channel_group.samples_byte_nr
):
ch_info.append([0, 0])
elif dependency_list[channel_index]:
ch_info.append([0, 0])
else:
if (record := info[channel_index]) is not None:
_, byte_size, byte_offset, _ = record
ch_info.append([byte_offset, byte_size])
else:
ch_info.append([0, 0])
while True:
try:
fragments = [next(stream) for stream in data_streams]
except Exception:
break
#
# if perf_counter() - tt > 120:
# x = 1 / 0
# prepare the master
_master = self.get_master(index, data=fragments[master_index], one_piece=True)
self._set_temporary_master(_master)
signals: list[Signal] | list[tuple[NDArray[Any], None]]
if idx == 0:
signals = []
else:
signals = [(_master, None)]
for fragment, (group_index, channels) in zip(fragments, groups.items(), strict=False):
grp = self.groups[group_index]
if not grp.single_channel_dtype:
self._prepare_record(grp)
self._invalidation_cache.clear()
if 1 and len(channels) >= 100:
# prepare the invalidation bytes for this group and fragment
invalidation_bytes = get_channel_raw_bytes(
fragment.data,
grp.channel_group.samples_byte_nr + grp.channel_group.invalidation_bytes_nr,
grp.channel_group.samples_byte_nr,
grp.channel_group.invalidation_bytes_nr,
)
channels_raw_data = get_channel_raw_bytes_parallel(
fragment.data,
grp.channel_group.samples_byte_nr + grp.channel_group.invalidation_bytes_nr,
group_info[group_index],
THREAD_COUNT,
)
if idx == 0:
sigs = typing.cast(list[Signal], signals)
for channel_index, raw_data in zip(channels, channels_raw_data, strict=False):
signal = self.get(
group=group_index,
index=channel_index,
data=(
Fragment(
data=raw_data,
record_offset=fragment.record_offset,
record_count=fragment.record_count,
invalidation_data=invalidation_bytes,
is_record=False,
)
if raw_data
else fragment
),
raw=True,
ignore_invalidation_bits=True,
samples_only=False,
skip_channel_validation=True,
)
sigs.append(signal)
else:
for channel_index, raw_data in zip(channels, channels_raw_data, strict=False):
samples, invalidation_bits = self.get(
group=group_index,
index=channel_index,
data=(
Fragment(
data=raw_data,
record_offset=fragment.record_offset,
record_count=fragment.record_count,
invalidation_data=invalidation_bytes,
is_record=False,
)
if raw_data
else fragment
),
raw=True,
ignore_invalidation_bits=True,
samples_only=True,
skip_channel_validation=True,
)
signals.append((samples, invalidation_bits))
else:
if idx == 0:
sigs = typing.cast(list[Signal], signals)
for channel_index in channels:
signal = self.get(
group=group_index,
index=channel_index,
data=fragment,
raw=True,
ignore_invalidation_bits=True,
samples_only=False,
)
sigs.append(signal)
else:
for channel_index in channels:
samples, invalidation_bits = self.get(
group=group_index,
index=channel_index,
data=fragment,
raw=True,
ignore_invalidation_bits=True,
samples_only=True,
)
signals.append((samples, invalidation_bits))
if version < "4.00":
if idx == 0:
sigs = typing.cast(list[Signal], signals)
for sig, channel_index in zip(sigs, channels, strict=False):
if sig.samples.dtype.kind == "S":
strsig = self.get(
group=group_index,
index=channel_index,
samples_only=True,
ignore_invalidation_bits=True,
)[0]
_dtype = strsig.dtype
sig.samples = sig.samples.astype(_dtype)
encodings[group_index].append((sig.encoding, _dtype))
del strsig
if sig.encoding != "latin-1":
if sig.encoding == "utf-16-le":
sig.samples = sig.samples.view(uint16).byteswap().view(sig.samples.dtype)
sig.samples = encode(decode(sig.samples, "utf-16-be"), "latin-1")
else:
sig.samples = encode(
decode(sig.samples, sig.encoding),
"latin-1",
)
sig.samples = sig.samples.astype(_dtype)
else:
encodings[group_index].append(None)
else:
for i, (sig_samples, encoding_tuple) in enumerate(
zip(signals, encodings[group_index], strict=False)
):
if encoding_tuple:
encoding, _dtype = encoding_tuple
samples = sig_samples[0]
if encoding != "latin-1":
if encoding == "utf-16-le":
samples = samples.view(uint16).byteswap().view(samples.dtype)
samples = encode(decode(samples, "utf-16-be"), "latin-1")
else:
samples = encode(decode(samples, encoding), "latin-1")
samples = samples.astype(_dtype)
signals[i] = (samples, sig_samples[1])
self._set_temporary_master(None)
idx += 1
yield signals
grp.read_split_count = 0
self._invalidation_cache.clear()
[docs]
def get_master(
self,
index: int,
data: Fragment | None = None,
record_offset: int = 0,
record_count: int | None = None,
one_piece: bool = False,
) -> NDArray[Any]:
"""Get master channel samples for the given group.
Parameters
----------
index : int
Group index.
data : Fragment, optional
Data bytes as a Fragment.
record_offset : int, optional
If `data=None`, use this to select the record offset from which the
group data should be loaded.
record_count : int, optional
Number of records to read; default is None and in this case all
available records are used.
Returns
-------
t, virtual_master_conversion : (np.ndarray, ChannelConversion | None)
Master channel samples and virtual master conversion.
"""
if self._master is not None:
return self._master
group = self.groups[index]
if group.channel_group.flags & v4c.FLAG_CG_REMOTE_MASTER:
if data is not None:
record_offset = data.record_offset
record_count = data.record_count
return self.get_master(
typing.cast(int, group.channel_group.cg_master_index),
record_offset=record_offset,
record_count=record_count,
)
time_ch_nr = self.masters_db.get(index, None)
channel_group = group.channel_group
record_size = channel_group.samples_byte_nr
record_size += channel_group.invalidation_bytes_nr
if record_count is not None:
cycles_nr = record_count
else:
cycles_nr = group.channel_group.cycles_nr
fragment = data
if fragment:
data_bytes, offset, _, invalidation_bytes = (
fragment.data,
fragment.record_offset,
fragment.record_count,
fragment.invalidation_data,
)
cycles_nr = len(data_bytes) // record_size if record_size else 0
record_offset = fragment.record_offset
record_count = fragment.record_count
else:
offset = 0
t: NDArray[Any]
if time_ch_nr is None:
if record_size:
t = arange(cycles_nr, dtype=float64)
t += offset
else:
t = array([], dtype=float64)
metadata = ("timestamps", v4c.SYNC_TYPE_TIME)
else:
time_ch = group.channels[time_ch_nr]
time_conv = time_ch.conversion
time_name = time_ch.name
metadata = (time_name, time_ch.sync_type)
if time_ch.channel_type == v4c.CHANNEL_TYPE_VIRTUAL_MASTER:
if record_count is None:
t = arange(record_offset, cycles_nr, 1, dtype=float64)
else:
t = arange(record_offset, record_offset + record_count, 1, dtype=float64)
if time_conv is not None:
t = time_conv.convert(t)
else:
# check if the channel group contains just the master channel
# and that there are no padding bytes
if len(group.channels) == 1 and time_ch.dtype_fmt.itemsize == record_size:
if one_piece:
if data is None:
raise RuntimeError("data is None")
t = frombuffer(data.data, dtype=time_ch.dtype_fmt)
else:
# get data
fragments: Iterable[Fragment]
if fragment is None:
fragments = self._load_data(
group,
record_offset=record_offset,
record_count=record_count,
)
else:
fragments = (fragment,)
buffer = bytearray().join([fragment.data for fragment in fragments])
t = frombuffer(buffer, dtype=time_ch.dtype_fmt)
else:
record = typing.cast(list[tuple[np.dtype[Any], int, int, int]], self._prepare_record(group))
dtype_, byte_size, byte_offset, bit_offset = record[time_ch_nr]
if one_piece:
if data is None:
raise RuntimeError("data is None")
data_bytes = data.data
buffer = get_channel_raw_bytes(
data_bytes,
record_size,
byte_offset,
byte_size,
)
t = frombuffer(buffer, dtype=dtype_)
else:
# get data
if fragment is None:
fragments = self._load_data(
group,
record_offset=record_offset,
record_count=record_count,
)
else:
fragments = (fragment,)
buffer = bytearray().join(
[
get_channel_raw_bytes(
fragment.data,
record_size,
byte_offset,
byte_size,
)
for fragment in fragments
]
)
t = frombuffer(buffer, dtype=dtype_)
if not time_ch.standard_C_size:
channel_dtype = time_ch.dtype_fmt
bit_count = time_ch.bit_count
data_type = time_ch.data_type
size = byte_size
if channel_dtype.byteorder == "=" and time_ch.data_type in (
v4c.DATA_TYPE_SIGNED_MOTOROLA,
v4c.DATA_TYPE_UNSIGNED_MOTOROLA,
):
dtype = f">u{t.itemsize}"
else:
dtype = f"{channel_dtype.byteorder}u{t.itemsize}"
if np.dtype(dtype) != t.dtype:
t = t.view(dtype)
if bit_offset:
t >>= bit_offset
if bit_count != size * 8:
if data_type in v4c.SIGNED_INT:
t = as_non_byte_sized_signed_int(t, bit_count)
else:
mask = (1 << bit_count) - 1
t &= mask
elif data_type in v4c.SIGNED_INT:
dtype = f"{channel_dtype.byteorder}i{t.itemsize}"
if np.dtype(dtype) != t.dtype:
t = t.view(dtype)
# get timestamps
if time_conv:
t = time_conv.convert(t)
self._master_channel_metadata[index] = metadata
if t.dtype != float64:
t = t.astype(float64)
return t
[docs]
def get_bus_signal(
self,
bus: BusType,
name: str,
database: CanMatrix | StrPath | None = None,
ignore_invalidation_bits: bool = False,
data: Fragment | None = None,
raw: bool = False,
ignore_value2text_conversion: bool = True,
) -> Signal:
"""Get a signal decoded from a raw bus logging. The currently supported
buses are CAN and LIN (LDF databases are not supported, they need to be
converted to DBC and fed to this function).
.. versionadded:: 6.0.0
Parameters
----------
bus : str
"CAN" or "LIN".
name : str
Signal name.
database : str | path-like | CanMatrix, optional
Path of external CAN/LIN database file (.dbc or .arxml) or
canmatrix.CanMatrix.
.. versionchanged:: 6.0.0
`db` and `database` arguments were merged into this single
argument.
ignore_invalidation_bits : bool, default False
Option to ignore invalidation bits.
data : Fragment, optional
Data bytes as a Fragment.
raw : bool, default False
Return channel samples without applying the conversion rule.
ignore_value2text_conversion : bool, default True
Return channel samples without values that have a description in
.dbc or .arxml file.
Returns
-------
sig : Signal
Signal object with the physical values.
"""
match bus:
case "CAN":
return self.get_can_signal(
name,
database=database,
ignore_invalidation_bits=ignore_invalidation_bits,
data=data,
raw=raw,
ignore_value2text_conversion=ignore_value2text_conversion,
)
case "LIN":
return self.get_lin_signal(
name,
database=database,
ignore_invalidation_bits=ignore_invalidation_bits,
data=data,
raw=raw,
ignore_value2text_conversion=ignore_value2text_conversion,
)
[docs]
def get_can_signal(
self,
name: str,
database: CanMatrix | StrPath | None = None,
ignore_invalidation_bits: bool = False,
data: Fragment | None = None,
raw: bool = False,
ignore_value2text_conversion: bool = True,
) -> Signal:
"""Get CAN message signal. You can specify an external CAN database path
or a canmatrix database object that has already been loaded from a file.
The signal name can be specified in the following ways:
* ``CAN<ID>.<MESSAGE_NAME>.<SIGNAL_NAME>`` - the `ID` value starts from 1
and must match the ID found in the measurement (the source CAN bus ID).
Example: CAN1.Wheels.FL_WheelSpeed
* ``CAN<ID>.CAN_DataFrame_<MESSAGE_ID>.<SIGNAL_NAME>`` - the `ID` value
starts from 1 and the `MESSAGE_ID` is the decimal message ID as found
in the database. Example: CAN1.CAN_DataFrame_218.FL_WheelSpeed
* ``<MESSAGE_NAME>.<SIGNAL_NAME>`` - in this case the first occurrence
of the message name and signal are returned (the same message could
be found on multiple CAN buses; for example on CAN1 and CAN3).
Example: Wheels.FL_WheelSpeed
* ``CAN_DataFrame_<MESSAGE_ID>.<SIGNAL_NAME>`` - in this case the first
occurrence of the message name and signal are returned (the same
message could be found on multiple CAN buses; for example on CAN1 and
CAN3). Example: CAN_DataFrame_218.FL_WheelSpeed
* ``<SIGNAL_NAME>`` - in this case the first occurrence of the signal
name is returned (the same signal name could be found in multiple
messages and on multiple CAN buses). Example: FL_WheelSpeed
Parameters
----------
name : str
Signal name.
database : str | path-like | CanMatrix, optional
Path of external CAN database file (.dbc or .arxml) or
canmatrix.CanMatrix.
.. versionchanged:: 6.0.0
`db` and `database` arguments were merged into this single
argument.
ignore_invalidation_bits : bool, default False
Option to ignore invalidation bits.
data : Fragment, optional
Data bytes as a Fragment.
raw : bool, default False
Return channel samples without applying the conversion rule.
ignore_value2text_conversion : bool, default True
Return channel samples without values that have a description in
.dbc or .arxml file.
Returns
-------
sig : Signal
Signal object with the physical values.
"""
if database is None:
return self.get(name)
if isinstance(database, (str, Path)):
database_path = Path(database)
if database_path.suffix.lower() not in (".arxml", ".dbc"):
message = f'Expected .dbc or .arxml file as CAN channel attachment but got "{database_path}"'
logger.exception(message)
raise MdfException(message)
else:
db_string = database_path.read_bytes()
md5_sum = md5(db_string).digest()
if md5_sum in self._external_dbc_cache:
db = self._external_dbc_cache[md5_sum]
else:
db = load_can_database(database_path, contents=db_string)
if db is None:
raise MdfException("failed to load database")
else:
db = database
is_j1939 = db.contains_j1939
name_ = name.split(".")
message_id: str | int
if len(name_) == 3:
can_id_str, message_id_str, signal_name = name_
match = v4c.CAN_ID_PATTERN.search(can_id_str)
if match is None:
raise MdfException(f'CAN id "{can_id_str}" of signal name "{name}" is not recognised by this library')
else:
can_id = int(match.group("id"))
match = v4c.CAN_DATA_FRAME_PATTERN.search(message_id_str)
if match is None:
message_id = message_id_str
else:
message_id = int(match.group("id"))
if isinstance(message_id, str):
frame = db.frame_by_name(message_id)
else:
frame = db.frame_by_id(message_id)
elif len(name_) == 2:
message_id_str, signal_name = name_
can_id = None
match = v4c.CAN_DATA_FRAME_PATTERN.search(message_id_str)
if match is None:
message_id = message_id_str
else:
message_id = int(match.group("id"))
if isinstance(message_id, str):
frame = db.frame_by_name(message_id)
else:
frame = db.frame_by_id(message_id)
else:
frame = None
for msg in db:
for signal in msg:
if signal.name == name:
frame = msg
can_id = None
signal_name = name
if frame is None:
raise MdfException(f"Could not find signal {name} in {database}")
for signal in frame.signals:
if signal.name == signal_name:
break
else:
raise MdfException(f'Signal "{signal_name}" not found in message "{frame.name}" of "{database}"')
test_ids: Collection[int]
if can_id is None:
index = None
for _can_id, messages in self.bus_logging_map["CAN"].items():
if is_j1939:
test_ids = [
canmatrix.ArbitrationId(id_, extended=True).pgn for id_ in self.bus_logging_map["CAN"][_can_id]
]
id_ = frame.arbitration_id.pgn
else:
id_ = frame.arbitration_id.id
test_ids = self.bus_logging_map["CAN"][_can_id]
if id_ in test_ids:
if is_j1939:
for id__, idx in self.bus_logging_map["CAN"][_can_id].items():
if canmatrix.ArbitrationId(id__, extended=True).pgn == id_:
index = idx
break
else:
index = self.bus_logging_map["CAN"][_can_id][frame.arbitration_id.id]
if index is not None:
break
else:
raise MdfException(
f'Message "{frame.name}" (ID={hex(frame.arbitration_id.id)}) not found in the measurement'
)
else:
if can_id in self.bus_logging_map["CAN"]:
if is_j1939:
test_ids = [
canmatrix.ArbitrationId(id_, extended=True).pgn for id_ in self.bus_logging_map["CAN"][can_id]
]
id_ = frame.arbitration_id.pgn
else:
id_ = frame.arbitration_id.id
test_ids = self.bus_logging_map["CAN"][can_id]
if id_ in test_ids:
if is_j1939:
for id__, idx in self.bus_logging_map["CAN"][can_id].items():
if canmatrix.ArbitrationId(id__, extended=True).pgn == id_:
index = idx
break
else:
index = self.bus_logging_map["CAN"][can_id][frame.arbitration_id.id]
else:
raise MdfException(
f'Message "{frame.name}" (ID={hex(frame.arbitration_id.id)}) not found in the measurement'
)
else:
raise MdfException(f'No logging from "{can_id}" was found in the measurement')
can_ids = self.get(
"CAN_DataFrame.ID",
group=index,
ignore_invalidation_bits=ignore_invalidation_bits,
data=data,
)
can_ids.samples = can_ids.samples.astype("<u4") & 0x1FFFFFFF
payload = self.get(
"CAN_DataFrame.DataBytes",
group=index,
samples_only=True,
ignore_invalidation_bits=ignore_invalidation_bits,
data=data,
)[0]
if is_j1939:
tmp_pgn = can_ids.samples >> 8
ps = tmp_pgn & 0xFF
pf = (can_ids.samples >> 16) & 0xFF
_pgn = tmp_pgn & 0x3FF00
can_ids.samples = where(pf >= 240, _pgn + ps, _pgn)
indexes = argwhere(can_ids.samples == frame.arbitration_id.pgn).ravel()
else:
indexes = argwhere(can_ids.samples == frame.arbitration_id.id).ravel()
payload = payload[indexes]
t = can_ids.timestamps[indexes].copy()
if can_ids.invalidation_bits is not None:
invalidation_bits = can_ids.invalidation_bits[indexes]
else:
invalidation_bits = None
if not ignore_invalidation_bits and invalidation_bits is not None:
payload = payload[nonzero(~invalidation_bits)[0]]
t = t[nonzero(~invalidation_bits)[0]]
extracted_signals = bus_logging_utils.extract_mux(
payload,
frame,
None,
None,
t,
original_message_id=None,
ignore_value2text_conversion=ignore_value2text_conversion,
raw=raw,
)
comment = signal.comment or ""
for entry, signals in extracted_signals.items():
for signal_name, extracted_signal in signals.items():
if signal_name == signal.name:
sig = Signal(
samples=extracted_signal["samples"],
timestamps=extracted_signal["t"],
name=signal_name,
unit=signal.unit or "",
comment=comment,
conversion=extracted_signal["conversion"],
)
if len(sig):
return sig
else:
raise MdfException(f'No logging from "{signal}" was found in the measurement')
raise MdfException(f'No logging from "{signal}" was found in the measurement')
[docs]
def get_lin_signal(
self,
name: str,
database: CanMatrix | str | Path | None = None,
ignore_invalidation_bits: bool = False,
data: Fragment | None = None,
raw: bool = False,
ignore_value2text_conversion: bool = True,
) -> Signal:
"""Get LIN message signal. You can specify an external LIN database path
or a canmatrix database object that has already been loaded from a file.
The signal name can be specified in the following ways:
* ``LIN_Frame_<MESSAGE_ID>.<SIGNAL_NAME>`` - Example: LIN_Frame_218.FL_WheelSpeed
* ``<MESSAGE_NAME>.<SIGNAL_NAME>`` - Example: Wheels.FL_WheelSpeed
* ``<SIGNAL_NAME>`` - Example: FL_WheelSpeed
.. versionadded:: 6.0.0
Parameters
----------
name : str
Signal name.
database : str | path-like | CanMatrix, optional
Path of external LIN database file (.dbc, .arxml or .ldf) or
canmatrix.CanMatrix.
ignore_invalidation_bits : bool, default False
Option to ignore invalidation bits.
data : Fragment, optional
Data bytes as a Fragment.
raw : bool, default False
Return channel samples without applying the conversion rule.
ignore_value2text_conversion : bool, default True
Return channel samples without values that have a description in
.dbc, .arxml or .ldf file.
Returns
-------
sig : Signal
Signal object with the physical values.
"""
if database is None:
return self.get(name)
if isinstance(database, (str, Path)):
database_path = Path(database)
if database_path.suffix.lower() not in (".arxml", ".dbc", ".ldf"):
message = f'Expected .dbc, .arxml or .ldf file as LIN channel attachment but got "{database_path}"'
logger.exception(message)
raise MdfException(message)
else:
db_string = database_path.read_bytes()
md5_sum = md5(db_string).digest()
if md5_sum in self._external_dbc_cache:
db = self._external_dbc_cache[md5_sum]
else:
contents = None if database_path.suffix.lower() == ".ldf" else db_string
db = load_can_database(database_path, contents=contents)
if db is None:
raise MdfException("failed to load database")
else:
db = database
name_ = name.split(".")
if len(name_) == 2:
message_id_str, signal_name = name_
match = v4c.LIN_DATA_FRAME_PATTERN.search(message_id_str)
message_id: str | int
if match is None:
message_id = message_id_str
else:
message_id = int(match.group("id"))
if isinstance(message_id, str):
frame = db.frame_by_name(message_id)
else:
frame = db.frame_by_id(message_id)
else:
frame = None
for msg in db:
for signal in msg:
if signal.name == name:
frame = msg
signal_name = name
if frame is None:
raise MdfException(f"Could not find signal {name} in {database}")
for signal in frame.signals:
if signal.name == signal_name:
break
else:
raise MdfException(f'Signal "{signal_name}" not found in message "{frame.name}" of "{database}"')
id_ = frame.arbitration_id.id
if id_ in self.bus_logging_map["LIN"]:
index = self.bus_logging_map["LIN"][id_]
else:
raise MdfException(
f'Message "{frame.name}" (ID={hex(frame.arbitration_id.id)}) not found in the measurement'
)
can_ids = self.get(
"LIN_Frame.ID",
group=index,
ignore_invalidation_bits=ignore_invalidation_bits,
data=data,
)
can_ids.samples = can_ids.samples.astype("<u4") & 0x1FFFFFFF
payload = self.get(
"LIN_Frame.DataBytes",
group=index,
samples_only=True,
ignore_invalidation_bits=ignore_invalidation_bits,
data=data,
)[0]
idx = argwhere(can_ids.samples == frame.arbitration_id.id).ravel()
payload = payload[idx]
t = can_ids.timestamps[idx].copy()
if can_ids.invalidation_bits is not None:
invalidation_bits = can_ids.invalidation_bits[idx]
else:
invalidation_bits = None
if not ignore_invalidation_bits and invalidation_bits is not None:
payload = payload[nonzero(~invalidation_bits)[0]]
t = t[nonzero(~invalidation_bits)[0]]
extracted_signals = bus_logging_utils.extract_mux(
payload,
frame,
None,
None,
t,
original_message_id=None,
ignore_value2text_conversion=ignore_value2text_conversion,
raw=raw,
)
comment = signal.comment or ""
for entry, signals in extracted_signals.items():
for signal_name, extracted_signal in signals.items():
if signal_name == signal.name:
sig = Signal(
samples=extracted_signal["samples"],
timestamps=extracted_signal["t"],
name=signal_name,
unit=signal.unit or "",
comment=comment,
conversion=extracted_signal["conversion"],
)
if len(sig):
return sig
else:
raise MdfException(f'No logging from "{signal}" was found in the measurement')
raise MdfException(f'No logging from "{signal}" was found in the measurement')
[docs]
def info(self) -> dict[str, object]:
"""Get MDF information as a dict.
Examples
--------
>>> mdf = MDF('test.mdf')
>>> mdf.info()
"""
info: dict[str, object] = {
"version": self.version,
"program": self.identification.program_identification.decode("utf-8").strip(" \0\n\r\t"),
"comment": self.header.comment,
}
info["groups"] = len(self.groups)
for i, gp in enumerate(self.groups):
inf: dict[str, object] = {}
info[f"group {i}"] = inf
inf["cycles"] = gp.channel_group.cycles_nr
inf["comment"] = gp.channel_group.comment
inf["channels count"] = len(gp.channels)
for j, channel in enumerate(gp.channels):
name = channel.name
ch_type = v4c.CHANNEL_TYPE_TO_DESCRIPTION[channel.channel_type]
inf[f"channel {j}"] = f'name="{name}" type={ch_type}'
return info
def _extract_can_logging(
self,
output_file: "MDF4",
dbc_files: Iterable[DbcFileType],
ignore_value2text_conversion: bool = True,
prefix: str = "",
progress: Callable[[int, int], None] | Any | None = None,
) -> "MDF4":
out = output_file
max_flags: list[list[list[bool]]] = []
valid_dbc_files: list[tuple[CanMatrix, StrPath, int]] = []
unique_name = UniqueDB()
for dbc_name, bus_channel in dbc_files:
if isinstance(dbc_name, CanMatrix):
valid_dbc_files.append(
(
dbc_name,
unique_name.get_unique_name("UserProvidedCanMatrix"),
bus_channel,
)
)
else:
dbc = load_can_database(Path(dbc_name))
if dbc is None:
continue
else:
valid_dbc_files.append((dbc, dbc_name, bus_channel))
count = sum(
1
for group in self.groups
if group.channel_group.flags & v4c.FLAG_CG_BUS_EVENT
and group.channel_group.acq_source
and group.channel_group.acq_source.bus_type == v4c.BUS_TYPE_CAN
)
count *= len(valid_dbc_files)
if progress is not None:
if callable(progress):
progress(0, count)
else:
progress.signals.setValue.emit(0)
progress.signals.setMaximum.emit(count)
if progress.stop:
raise Terminated
cntr = 0
total_unique_ids: set[tuple[int, bool]] = set()
found_ids: defaultdict[StrPath, set[tuple[tuple[int, int, bool], str]]] = defaultdict(set)
not_found_ids: defaultdict[StrPath, list[tuple[tuple[int, bool] | int, str]]] = defaultdict(list)
unknown_ids: defaultdict[int | tuple[int, bool], list[bool]] = defaultdict(list)
for dbc, dbc_name, bus_channel in valid_dbc_files:
messages = {(message.arbitration_id.id, message.arbitration_id.extended): message for message in dbc}
global_is_j1939 = dbc.attributes.get("ProtocolType", "").lower() == "j1939"
not_extended = [msg for msg in dbc if not msg.arbitration_id.extended]
if global_is_j1939 and not_extended:
logger.warning(
f"Not all j1939 messages in <{dbc_name}> seem to use extended addressing. Disabling global j1939 flag..."
)
for msg in not_extended:
logger.warning(f" {msg} with id {msg.arbitration_id}")
global_is_j1939 = False # Relax req on j1939 adressing
j1939_messages = {
(
message.arbitration_id.pgn,
message.arbitration_id.j1939_source,
): message
for message in dbc
if message.is_j1939 or global_is_j1939
}
current_not_found = {
(
(
(message.arbitration_id.id, message.arbitration_id.extended)
if not message.is_j1939 and not global_is_j1939
else message.arbitration_id.pgn
),
message.name,
)
for msg_id, message in messages.items()
}
msg_map: dict[tuple[int | None, int | None, bool, int | None, str | None, int, int], int] = {}
for i, group in enumerate(self.groups):
if (
not group.channel_group.flags & v4c.FLAG_CG_BUS_EVENT
or (group.channel_group.acq_source and group.channel_group.acq_source.bus_type != v4c.BUS_TYPE_CAN)
or not "CAN_DataFrame" in [ch.name for ch in group.channels]
):
continue
self._prepare_record(group)
data = self._load_data(group, optimize_read=False)
for fragment in data:
self._set_temporary_master(None)
self._set_temporary_master(self.get_master(i, data=fragment, one_piece=True))
bus_ids = self.get(
"CAN_DataFrame.BusChannel",
group=i,
data=fragment,
).samples.astype("<u1")
msg_ids = self.get("CAN_DataFrame.ID", group=i, data=fragment).astype("<u4")
try:
msg_ide = self.get("CAN_DataFrame.IDE", group=i, data=fragment).samples.astype("<u1")
except:
msg_ide = ((msg_ids & 0x80000000) >> 31).samples
msg_ids &= 0x1FFFFFFF
data_bytes = self.get(
"CAN_DataFrame.DataBytes",
group=i,
data=fragment,
).samples
buses = np.unique(bus_ids)
for bus in buses:
if bus_channel and bus != bus_channel:
continue
idx = np.argwhere(bus_ids == bus).ravel()
bus_t = msg_ids.timestamps[idx]
bus_msg_ids = msg_ids.samples[idx]
bus_msg_ide = msg_ide[idx]
bus_data_bytes = data_bytes[idx]
tmp_pgns = bus_msg_ids >> 8
pss = tmp_pgns & 0xFF
pfs = (bus_msg_ids >> 16) & 0xFF
_pgns = tmp_pgns & 0x3FF00
j1939_msg_pgns = np.where(pfs >= 240, _pgns + pss, _pgns)
j9193_msg_sa = bus_msg_ids & 0xFF
unique_ids = set(
zip(
typing.cast(list[int], bus_msg_ids.tolist()),
typing.cast(list[bool], bus_msg_ide.tolist()),
strict=False,
)
)
total_unique_ids = total_unique_ids | set(unique_ids)
for msg_id, is_extended in sorted(unique_ids):
message = messages.get((msg_id, is_extended), None)
if message is None:
tmp_pgn = msg_id >> 8
ps = tmp_pgn & 0xFF
pf = (msg_id >> 16) & 0xFF
_pgn = tmp_pgn & 0x3FF00
msg_pgn = _pgn + ps if pf >= 240 else _pgn
for (_pgn, _sa), _msg in j1939_messages.items():
if _pgn == msg_pgn:
message = _msg
break
else:
unknown_ids[(msg_id, is_extended)].append(True)
continue
is_j1939 = message.is_j1939 or global_is_j1939
if is_j1939:
source_address = msg_id & 0xFF
pgn_number = message.arbitration_id.pgn
key = (pgn_number, source_address, True)
found_ids[dbc_name].add((key, message.name))
try:
current_not_found.remove((pgn_number, message.name))
except KeyError:
pass
else:
key = msg_id, bool(is_extended), False
found_ids[dbc_name].add((key, message.name))
try:
current_not_found.remove(((msg_id, is_extended), message.name))
except KeyError:
pass
unknown_ids[(msg_id, is_extended)].append(False)
if is_j1939:
idx = np.argwhere(
(j1939_msg_pgns == pgn_number) & (j9193_msg_sa == source_address)
).ravel()
else:
idx = np.argwhere((bus_msg_ids == msg_id) & (bus_msg_ide == is_extended)).ravel()
payload = bus_data_bytes[idx]
t = bus_t[idx]
try:
extracted_signals = bus_logging_utils.extract_mux(
payload,
message,
msg_id,
bus,
t,
original_message_id=source_address if is_j1939 else None,
ignore_value2text_conversion=ignore_value2text_conversion,
is_j1939=is_j1939,
is_extended=is_extended,
raw=True,
)
except:
print(format_exc())
raise
for entry, signals in extracted_signals.items():
if len(next(iter(signals.values()))["samples"]) == 0:
continue
if entry not in msg_map:
sigs: list[Signal] = []
index = len(out.groups)
msg_map[entry] = index
for name_, signal in signals.items():
signal_name = f"{prefix}{signal['name']}"
sig = Signal(
samples=signal["samples"],
timestamps=signal["t"],
name=signal_name,
comment=signal["comment"],
unit=signal["unit"],
invalidation_bits=signal["invalidation_bits"],
display_names={
f"CAN{bus}.{message.name}.{signal_name}": "bus",
f"{message.name}.{signal_name}": "message",
},
conversion=signal["conversion"],
)
sigs.append(sig)
if is_j1939:
if prefix:
comment = f"{prefix}: CAN{bus} ID=0x{msg_id:X} {message} PGN=0x{pgn_number:X} SA=0x{source_address:X}"
else:
comment = f"CAN{bus} ID=0x{msg_id:X} {message} PGN=0x{pgn_number:X} SA=0x{source_address:X}"
acq_name = f"SourceAddress = 0x{source_address}"
else:
if prefix:
acq_name = (
f"{prefix}: CAN{bus} message ID=0x{msg_id:X} EXT={bool(is_extended)}"
)
comment = f'{prefix}: CAN{bus} - message "{message}" 0x{msg_id:X} EXT={bool(is_extended)}'
else:
acq_name = f"CAN{bus} message ID=0x{msg_id:X} EXT={bool(is_extended)}"
comment = (
f"CAN{bus} - message {message} 0x{msg_id:X} EXT={bool(is_extended)}"
)
acq_source = Source(
name=acq_name,
path=f"CAN{int(bus)}.CAN_DataFrame.ID=0x{message.arbitration_id.id:X} EXT={bool(is_extended)}",
comment=f"""\
<SIcomment>
<TX>CAN{bus} data frame 0x{message.arbitration_id.id:X} EXT={bool(is_extended)} - {message.name}</TX>
<bus name="CAN{int(bus)}"/>
<common_properties>
<e name="ChannelNo" type="integer">{int(bus)}</e>
</common_properties>
</SIcomment>""",
source_type=v4c.SOURCE_BUS,
bus_type=v4c.BUS_TYPE_CAN,
)
for sig in sigs:
sig.source = acq_source
cg_nr = out.append(
sigs,
acq_name=acq_name,
acq_source=acq_source,
comment=comment,
common_timebase=True,
)
out.groups[cg_nr].channel_group.flags = v4c.FLAG_CG_BUS_EVENT
if is_j1939:
max_flags.append([[False]])
for ch_index, sig in enumerate(sigs, 1):
max_flags[cg_nr].append(
[
(
bool(np.all(sig.invalidation_bits))
if sig.invalidation_bits is not None
else False
)
]
)
else:
max_flags.append([[False]] * (len(sigs) + 1))
else:
index = msg_map[entry]
signal_samples: list[tuple[NDArray[Any], NDArray[np.bool] | None]] = []
for name_, signal in signals.items():
signal_samples.append(
(
signal["samples"],
signal["invalidation_bits"],
)
)
t = signal["t"]
if is_j1939:
for ch_index, sig_sample in enumerate(signal_samples, 1):
max_flags[index][ch_index].append(
bool(np.all(sig_sample[1])) if sig_sample[1] is not None else False
)
signal_samples.insert(0, (t, None))
out.extend(index, signal_samples)
self._set_temporary_master(None)
cntr += 1
if progress is not None:
if callable(progress):
progress(cntr, count)
else:
progress.signals.setValue.emit(cntr)
if progress.stop:
raise Terminated
if current_not_found:
not_found_ids[dbc_name] = list(current_not_found)
unknown_id_set = {msg_id for msg_id, not_found in unknown_ids.items() if all(not_found)}
self.last_call_info["CAN"] = {
"dbc_files": dbc_files,
"total_unique_ids": total_unique_ids,
"unknown_id_count": len(unknown_id_set),
"not_found_ids": not_found_ids,
"found_ids": found_ids,
"unknown_ids": unknown_id_set,
"max_flags": max_flags,
}
if not out.groups:
logger.warning(f'No CAN signals could be extracted from "{self.name}". The output file will be empty.')
return out
def _extract_lin_logging(
self,
output_file: "MDF4",
dbc_files: Iterable[DbcFileType],
ignore_value2text_conversion: bool = True,
prefix: str = "",
progress: Callable[[int, int], None] | Any | None = None,
) -> "MDF4":
out = output_file
valid_dbc_files: list[tuple[CanMatrix, StrPath, int]] = []
unique_name = UniqueDB()
for dbc_name, bus_channel in dbc_files:
if isinstance(dbc_name, CanMatrix):
valid_dbc_files.append(
(
dbc_name,
unique_name.get_unique_name("UserProvidedCanMatrix"),
bus_channel,
)
)
else:
dbc = load_can_database(Path(dbc_name))
if dbc is None:
continue
else:
valid_dbc_files.append((dbc, dbc_name, bus_channel))
count = sum(
1
for group in self.groups
if group.channel_group.flags & v4c.FLAG_CG_BUS_EVENT
and group.channel_group.acq_source
and group.channel_group.acq_source.bus_type == v4c.BUS_TYPE_LIN
)
count *= len(valid_dbc_files)
if progress is not None:
if callable(progress):
progress(0, count)
else:
progress.signals.setValue.emit(0)
progress.signals.setMaximum.emit(count)
if progress.stop:
raise Terminated
cntr = 0
total_unique_ids: set[tuple[int, ...]] = set()
found_ids: defaultdict[StrPath, set[tuple[tuple[int, bool, bool], str]]] = defaultdict(set)
not_found_ids: defaultdict[StrPath, list[tuple[int, str]]] = defaultdict(list)
unknown_ids: defaultdict[int, list[bool]] = defaultdict(list)
for dbc, dbc_name, bus_channel in valid_dbc_files:
messages = {message.arbitration_id.id: message for message in dbc}
current_not_found_ids = {(msg_id, message.name) for msg_id, message in messages.items()}
msg_map = {}
for i, group in enumerate(self.groups):
if (
not group.channel_group.flags & v4c.FLAG_CG_BUS_EVENT
or (group.channel_group.acq_source and group.channel_group.acq_source.bus_type != v4c.BUS_TYPE_LIN)
or not "LIN_Frame" in [ch.name for ch in group.channels]
):
continue
self._prepare_record(group)
data = self._load_data(group, optimize_read=False)
for fragment in data:
self._set_temporary_master(None)
self._set_temporary_master(self.get_master(i, data=fragment, one_piece=True))
msg_ids = self.get("LIN_Frame.ID", group=i, data=fragment).astype("<u4") & 0x1FFFFFFF
original_ids = msg_ids.samples.copy()
data_bytes = self.get(
"LIN_Frame.DataBytes",
group=i,
data=fragment,
).samples
try:
bus_ids = self.get(
"LIN_Frame.BusChannel",
group=i,
data=fragment,
).samples.astype("<u1")
except:
bus_ids = np.ones(len(original_ids), dtype="u1")
bus_t = msg_ids.timestamps
bus_msg_ids = msg_ids.samples
bus_data_bytes = data_bytes
original_msg_ids = original_ids
unique_ids = np.unique(np.rec.fromarrays([bus_msg_ids, bus_msg_ids]))
total_unique_ids = total_unique_ids | {tuple(int(e) for e in f) for f in unique_ids}
buses = np.unique(bus_ids)
for bus in buses:
if bus_channel and bus != bus_channel:
continue
for msg_id_record in sorted(unique_ids.tolist()):
msg_id = int(msg_id_record[0])
original_msg_id = int(msg_id_record[1])
message = messages.get(msg_id, None)
if message is None:
unknown_ids[msg_id].append(True)
continue
found_ids[dbc_name].add(((msg_id, False, False), message.name))
try:
current_not_found_ids.remove((msg_id, message.name))
except KeyError:
pass
unknown_ids[msg_id].append(False)
idx = np.argwhere(bus_msg_ids == msg_id).ravel()
payload = bus_data_bytes[idx]
t = bus_t[idx]
extracted_signals = bus_logging_utils.extract_mux(
payload,
message,
msg_id,
bus,
t,
original_message_id=None,
ignore_value2text_conversion=ignore_value2text_conversion,
raw=True,
)
for entry, signals in extracted_signals.items():
if len(next(iter(signals.values()))["samples"]) == 0:
continue
if entry not in msg_map:
sigs: list[Signal] = []
index = len(out.groups)
msg_map[entry] = index
for name_, signal in signals.items():
signal_name = f"{prefix}{signal['name']}"
sig = Signal(
samples=signal["samples"],
timestamps=signal["t"],
name=signal_name,
comment=signal["comment"],
unit=signal["unit"],
invalidation_bits=signal["invalidation_bits"],
display_names={
f"LIN{bus}.{message.name}.{signal_name}": "bus",
f"{message.name}.{signal_name}": "message",
},
conversion=signal["conversion"],
)
sigs.append(sig)
if prefix:
acq_name = f"{prefix}: from LIN{bus} message ID=0x{msg_id:X}"
else:
acq_name = f"from LIN{bus} message ID=0x{msg_id:X}"
acq_source = Source(
name=acq_name,
path=f"LIN{int(bus)}.LIN_Frame.ID=0x{message.arbitration_id.id:X}",
comment=f"""\
<SIcomment>
<TX>LIN{bus} data frame 0x{message.arbitration_id.id:X} - {message.name}</TX>
<bus name="LIN{int(bus)}"/>
<common_properties>
<e name="ChannelNo" type="integer">{int(bus)}</e>
</common_properties>
</SIcomment>""",
source_type=v4c.SOURCE_BUS,
bus_type=v4c.BUS_TYPE_LIN,
)
for sig in sigs:
sig.source = acq_source
cg_nr = out.append(
sigs,
acq_name=acq_name,
acq_source=acq_source,
comment=f"from LIN{bus} - message {message} 0x{msg_id:X}",
common_timebase=True,
)
out.groups[cg_nr].channel_group.flags = v4c.FLAG_CG_BUS_EVENT
else:
index = msg_map[entry]
signal_samples: list[tuple[NDArray[Any], NDArray[np.bool] | None]] = []
for name_, signal in signals.items():
signal_samples.append(
(
signal["samples"],
signal["invalidation_bits"],
)
)
t = signal["t"]
signal_samples.insert(0, (t, None))
out.extend(index, signal_samples)
self._set_temporary_master(None)
cntr += 1
if progress is not None:
if callable(progress):
progress(cntr, count)
else:
progress.signals.setValue.emit(cntr)
if progress.stop:
raise Terminated
if current_not_found_ids:
not_found_ids[dbc_name] = list(current_not_found_ids)
unknown_id_set = {msg_id for msg_id, not_found in unknown_ids.items() if all(not_found)}
self.last_call_info["LIN"] = {
"dbc_files": dbc_files,
"total_unique_ids": total_unique_ids,
"unknown_id_count": len(unknown_id_set),
"not_found_ids": not_found_ids,
"found_ids": found_ids,
"unknown_ids": unknown_id_set,
}
if not out.groups:
logger.warning(f'No LIN signals could be extracted from "{self.name}". The output file will be empty.')
return out
@property
def start_time(self) -> datetime:
"""Getter and setter of the measurement start timestamp.
Returns
-------
timestamp : datetime.datetime
Start timestamp.
"""
return self.header.start_time
@start_time.setter
def start_time(self, timestamp: datetime) -> None:
self.header.start_time = timestamp
[docs]
def save(
self,
dst: FileLike | StrPath,
overwrite: bool = False,
compression: CompressionType = v4c.CompressionAlgorithm.NO_COMPRESSION,
progress: Any | None = None,
add_history_block: bool = True,
) -> Path:
"""Save `MDF` to `dst`. If `overwrite` is True, then the destination
file is overwritten, otherwise the file name is appended with '.<cntr>',
where '<cntr>' is the first counter that produces a new file name that
does not already exist in the filesystem.
Parameters
----------
dst : str | path-like | file-like
Destination file name.
overwrite : bool, default False
Overwrite flag.
compression : int, default 0
Use compressed data blocks; valid since MDF version 4.10.
* 0 - no compression
* 1 - deflate (slower, but produces smaller files)
* 2 - transposition + deflate (slowest, but produces
the smallest files)
* 3 - zstd (a bit slower than lz4 but double compression ratio)
* 4 - transposition + zstd (a bit slower than lz4 but double compression ratio)
* 5 - lz4 (fastest speed but lower compression ratio compared to zstd)
* 6 - transposition + lz4 (fastest speed but lower compression ratio compared to zstd)
.. versionchanged:: 8.8.0
added zstd and lz4 compression options; only available for MDF version >= 4.30
add_history_block : bool, default True
Option to add file history block.
Returns
-------
output_file : pathlib.Path
Path to saved file.
"""
if is_file_like(dst):
dst_ = dst
file_like = True
if hasattr(dst, "name"):
dst = Path(dst.name)
else:
dst = Path("__file_like.mf4")
dst_.seek(0)
suffix = ".mf4"
else:
file_like = False
suffix = Path(dst).suffix.lower()
dst = Path(dst).with_suffix(".mf4")
destination_dir = dst.parent
destination_dir.mkdir(parents=True, exist_ok=True)
if overwrite is False:
if dst.is_file():
cntr = 0
while True:
name = dst.with_suffix(f".{cntr}.mf4")
if not name.exists():
break
else:
cntr += 1
message = (
f'Destination file "{dst}" already exists and "overwrite" is False. Saving MDF file as "{name}"'
)
logger.warning(message)
dst = name
if dst == self.name:
destination = dst.with_suffix(".savetemp")
else:
destination = dst
dst_ = open(destination, "wb+")
if not self.file_history:
comment = "created"
else:
comment = "updated"
if add_history_block:
fh = FileHistory()
fh.comment = f"""<FHcomment>
<TX>{comment}</TX>
<tool_id>{tool.__tool__}</tool_id>
<tool_vendor>{tool.__vendor__}</tool_vendor>
<tool_version>{tool.__version__}</tool_version>
</FHcomment>"""
self.file_history.append(fh)
cg_map = {}
try:
defined_texts: dict[bytes | str, int] = {"": 0, b"": 0}
cc_map: dict[bytes | int, int] = {}
si_map: dict[bytes | int, int] = {}
groups_nr = len(self.groups)
write = dst_.write
tell = dst_.tell
seek = dst_.seek
blocks: list[bytes | SupportsBytes] = []
write(bytes(self.identification))
self.header.to_blocks(dst_.tell(), blocks)
for block in blocks:
write(bytes(block))
original_data_addresses = []
if self.version < "4.30":
if compression > v4c.CompressionAlgorithm.TRANSPOSED_DEFLATE:
zip_type = v4c.FLAG_DZ_TRANSPOSED_DEFLATE
elif compression == v4c.CompressionAlgorithm.DEFLATE:
zip_type = v4c.FLAG_DZ_DEFLATE
else:
zip_type = v4c.FLAG_DZ_TRANSPOSED_DEFLATE
else:
match compression:
case v4c.CompressionAlgorithm.DEFLATE:
zip_type = v4c.FLAG_DZ_DEFLATE
case v4c.CompressionAlgorithm.TRANSPOSED_DEFLATE:
zip_type = v4c.FLAG_DZ_TRANSPOSED_DEFLATE
case v4c.CompressionAlgorithm.LZ4:
zip_type = v4c.FLAG_DZ_LZ4
case v4c.CompressionAlgorithm.TRANSPOSED_LZ4:
zip_type = v4c.FLAG_DZ_TRANSPOSED_LZ4
case v4c.CompressionAlgorithm.ZSTD:
zip_type = v4c.FLAG_DZ_ZSTD
case v4c.CompressionAlgorithm.TRANSPOSED_ZSTD:
zip_type = v4c.FLAG_DZ_TRANSPOSED_ZSTD
# write DataBlocks first
for gp_nr, gp in enumerate(self.groups):
original_data_addresses.append(gp.data_group.data_block_addr)
if gp.channel_group.flags & v4c.FLAG_CG_VLSD:
continue
address = tell()
total_size = (
gp.channel_group.samples_byte_nr + gp.channel_group.invalidation_bytes_nr
) * gp.channel_group.cycles_nr
if total_size:
if self._write_fragment_size:
samples_size = gp.channel_group.samples_byte_nr + gp.channel_group.invalidation_bytes_nr
if samples_size:
split_size = self._write_fragment_size // samples_size
split_size *= samples_size
if split_size == 0:
split_size = samples_size
chunks = float(total_size) / split_size
chunks = ceil(chunks)
self._read_fragment_size = split_size
else:
chunks = 1
else:
chunks = 1
data = self._load_data(gp)
if chunks == 1:
fragment = next(data)
data_, inval_ = fragment.data, fragment.invalidation_data
if self.version >= "4.20" and gp.uses_ld:
if compression:
if gp.channel_group.samples_byte_nr > 1:
current_zip_type = zip_type
if compression in (
v4c.CompressionAlgorithm.DEFLATE,
v4c.CompressionAlgorithm.LZ4,
v4c.CompressionAlgorithm.ZSTD,
):
param = 0
else:
param = gp.channel_group.samples_byte_nr
else:
current_zip_type = v4c.FLAG_DZ_DEFLATE
param = 0
dz_kwargs: DataZippedBlockKwargs = {
"data": data_,
"zip_type": current_zip_type,
"param": param,
"original_type": b"DV",
}
data_block: DataZippedBlock | DataBlock = DataZippedBlock(**dz_kwargs)
else:
data_block = DataBlock(data=data_, type="DV")
write(bytes(data_block))
data_address = address
align = data_block.block_len % 8
if align:
write(b"\0" * (8 - align))
if inval_ is not None:
inval_address = address = tell()
if compression:
if compression in (
v4c.CompressionAlgorithm.DEFLATE,
v4c.CompressionAlgorithm.LZ4,
v4c.CompressionAlgorithm.ZSTD,
):
param = 0
else:
param = gp.channel_group.invalidation_bytes_nr
dz_kwargs = {
"data": inval_,
"zip_type": zip_type,
"param": param,
"original_type": b"DI",
}
inval_block: DataZippedBlock | DataBlock = DataZippedBlock(**dz_kwargs)
else:
inval_block = DataBlock(data=inval_, type="DI")
write(bytes(inval_block))
align = inval_block.block_len % 8
if align:
write(b"\0" * (8 - align))
address = tell()
ld_kwargs: ListDataKwargs = { # type: ignore[typeddict-unknown-key]
"flags": v4c.FLAG_LD_EQUAL_LENGHT,
"data_block_nr": 1,
"data_block_len": gp.channel_group.cycles_nr,
"data_block_addr_0": data_address,
"flags_ext": 0,
}
if inval_:
ld_kwargs["flags_ext"] |= v4c.FLAG_LD_EXT_INVALIDATION_PRESENT
ld_kwargs["invalidation_bits_addr_0"] = inval_address # type: ignore[typeddict-unknown-key]
ld_block = ListData(**ld_kwargs)
write(bytes(ld_block))
align = ld_block.block_len % 8
if align:
write(b"\0" * (8 - align))
if gp.channel_group.cycles_nr:
gp.data_group.data_block_addr = address
else:
gp.data_group.data_block_addr = 0
else:
if compression and self.version >= "4.10":
if compression == 1:
param = 0
else:
param = gp.channel_group.samples_byte_nr + gp.channel_group.invalidation_bytes_nr
dz_kwargs = {
"data": data_,
"zip_type": zip_type,
"param": param,
}
data_block = DataZippedBlock(**dz_kwargs)
else:
data_block = DataBlock(data=data_)
write(bytes(data_block))
align = data_block.block_len % 8
if align:
write(b"\0" * (8 - align))
if gp.channel_group.cycles_nr:
gp.data_group.data_block_addr = address
else:
gp.data_group.data_block_addr = 0
else:
if self.version >= "4.20" and gp.uses_ld:
dv_addr = []
di_addr = []
block_size = 0
for i, fragment in enumerate(data):
data_, inval_ = fragment.data, fragment.invalidation_data
if i == 0:
block_size = len(data_)
if compression:
if compression == 1:
param = 0
else:
param = gp.channel_group.samples_byte_nr
dz_kwargs = {
"data": data_,
"zip_type": zip_type,
"param": param,
"original_type": b"DV",
}
data_block = DataZippedBlock(**dz_kwargs)
else:
data_block = DataBlock(data=data_, type="DV")
dv_addr.append(tell())
write(bytes(data_block))
align = data_block.block_len % 8
if align:
write(b"\0" * (8 - align))
if inval_ is not None:
if compression:
if compression == 1:
param = 0
else:
param = gp.channel_group.invalidation_bytes_nr
dz_kwargs = {
"data": inval_,
"zip_type": zip_type,
"param": param,
"original_type": b"DI",
}
inval_block = DataZippedBlock(**dz_kwargs)
else:
inval_block = DataBlock(data=inval_, type="DI")
di_addr.append(tell())
write(bytes(inval_block))
align = inval_block.block_len % 8
if align:
write(b"\0" * (8 - align))
address = tell()
ld_kwargs = {
"flags": v4c.FLAG_LD_EQUAL_LENGHT,
"data_block_nr": len(dv_addr),
"data_block_len": block_size // gp.channel_group.samples_byte_nr,
"flags_ext": 0,
}
for i, addr in enumerate(dv_addr):
ld_kwargs[f"data_block_addr_{i}"] = addr # type: ignore[literal-required]
if di_addr:
ld_kwargs["flags_ext"] |= v4c.FLAG_LD_EXT_INVALIDATION_PRESENT
for i, addr in enumerate(di_addr):
ld_kwargs[f"invalidation_bits_addr_{i}"] = addr # type: ignore[literal-required]
if self.version >= "4.30":
ld_kwargs["zip_info"] = zip_type
ld_kwargs["zip_info_inval"] = zip_type
ld_kwargs["flags"] |= v4c.FLAG_LD_ZIP_INFO_VALID
ld_block = ListData(**ld_kwargs)
write(bytes(ld_block))
align = ld_block.block_len % 8
if align:
write(b"\0" * (8 - align))
if gp.channel_group.cycles_nr:
gp.data_group.data_block_addr = address
else:
gp.data_group.data_block_addr = 0
else:
hl_kwargs: HeaderListKwargs = {
"flags": v4c.FLAG_DL_EQUAL_LENGHT,
"zip_type": zip_type,
}
hl_block = HeaderList(**hl_kwargs)
dl_kwargs: DataListKwargs = {
"flags": v4c.FLAG_DL_EQUAL_LENGHT,
"links_nr": chunks + 1,
"data_block_nr": chunks,
"data_block_len": split_size,
}
dl_block = DataList(**dl_kwargs)
for i, fragment in enumerate(data):
data_ = fragment.data
if compression and self.version >= "4.10":
if compression == 1:
zip_type = v4c.FLAG_DZ_DEFLATE
else:
zip_type = v4c.FLAG_DZ_TRANSPOSED_DEFLATE
if compression == 1:
param = 0
else:
param = (
gp.channel_group.samples_byte_nr + gp.channel_group.invalidation_bytes_nr
)
dz_kwargs = {
"data": data_,
"zip_type": zip_type,
"param": param,
}
block = DataZippedBlock(**dz_kwargs)
else:
block = DataBlock(data=data_)
address = tell()
block.address = address
write(bytes(block))
align = block.block_len % 8
if align:
write(b"\0" * (8 - align))
dl_block[f"data_block_addr{i}"] = address
address = tell()
dl_block.address = address
write(bytes(dl_block))
if compression and self.version != "4.00":
hl_block.first_dl_addr = address
address = tell()
hl_block.address = address
write(bytes(hl_block))
gp.data_group.data_block_addr = address
else:
gp.data_group.data_block_addr = 0
if progress is not None:
progress.signals.setValue.emit(int(50 * (gp_nr + 1) / groups_nr))
if progress.stop:
dst_.close()
self.close()
raise Terminated
address = tell()
blocks = []
# file history blocks
for fh in self.file_history:
address = fh.to_blocks(address, blocks, defined_texts)
for i, fh in enumerate(self.file_history[:-1]):
fh.next_fh_addr = self.file_history[i + 1].address
self.file_history[-1].next_fh_addr = 0
# data groups
gp_rec_ids = []
valid_data_groups = []
for gp in self.groups:
if gp.channel_group.flags & v4c.FLAG_CG_VLSD:
continue
valid_data_groups.append(gp.data_group)
gp_rec_ids.append(gp.data_group.record_id_len)
address = gp.data_group.to_blocks(address, blocks, defined_texts)
if valid_data_groups:
for i, dg in enumerate(valid_data_groups[:-1]):
addr_ = valid_data_groups[i + 1].address
dg.next_dg_addr = addr_
valid_data_groups[-1].next_dg_addr = 0
# go through each data group and append the rest of the blocks
for i, gp in enumerate(self.groups):
channels = gp.channels
for j, channel in enumerate(channels):
if channel.attachment is not None:
channel.attachment_addr = self.attachments[channel.attachment].address
elif channel.attachment_nr:
channel.attachment_addr = 0
address = channel.to_blocks(address, blocks, defined_texts, cc_map, si_map)
if channel.channel_type == v4c.CHANNEL_TYPE_SYNC:
if channel.attachment is not None:
channel.data_block_addr = self.attachments[channel.attachment].address
else:
sdata = self._load_signal_data(group=gp, index=j)
if sdata:
split_size = self._write_fragment_size
if self._write_fragment_size:
chunks = float(len(sdata)) / split_size
chunks = ceil(chunks)
else:
chunks = 1
if chunks == 1:
if compression and self.version > "4.00":
signal_data: DataZippedBlock | DataBlock = DataZippedBlock(
data=sdata,
zip_type=v4c.FLAG_DZ_DEFLATE,
original_type=b"SD",
)
signal_data.address = address
address += signal_data.block_len
blocks.append(signal_data)
align = signal_data.block_len % 8
if align:
blocks.append(b"\0" * (8 - align))
address += 8 - align
else:
signal_data = DataBlock(data=sdata, type="SD")
signal_data.address = address
address += signal_data.block_len
blocks.append(signal_data)
align = signal_data.block_len % 8
if align:
blocks.append(b"\0" * (8 - align))
address += 8 - align
channel.data_block_addr = signal_data.address
else:
dl_kwargs = {
"flags": v4c.FLAG_DL_EQUAL_LENGHT,
"links_nr": chunks + 1,
"data_block_nr": chunks,
"data_block_len": self._write_fragment_size,
}
dl_block = DataList(**dl_kwargs)
for k in range(chunks):
data_ = sdata[k * split_size : (k + 1) * split_size]
if compression and self.version > "4.00":
zip_type = v4c.FLAG_DZ_DEFLATE
param = 0
dz_kwargs = {
"data": data_,
"zip_type": zip_type,
"param": param,
"original_type": b"SD",
}
block = DataZippedBlock(**dz_kwargs)
else:
block = DataBlock(data=data_, type="SD")
blocks.append(block)
block.address = address
address += block.block_len
align = block.block_len % 8
if align:
blocks.append(b"\0" * (8 - align))
address += 8 - align
dl_block[f"data_block_addr{k}"] = block.address
dl_block.address = address
blocks.append(dl_block)
address += dl_block.block_len
if compression and self.version > "4.00":
hl_kwargs = {
"flags": v4c.FLAG_DL_EQUAL_LENGHT,
"zip_type": v4c.FLAG_DZ_DEFLATE,
"first_dl_addr": dl_block.address,
}
hl_block = HeaderList(**hl_kwargs)
hl_block.address = address
address += hl_block.block_len
blocks.append(hl_block)
channel.data_block_addr = hl_block.address
else:
channel.data_block_addr = dl_block.address
else:
channel.data_block_addr = 0
dep_list = gp.channel_dependencies[j]
if dep_list:
if all(isinstance(dep, ChannelArrayBlock) for dep in dep_list):
dep_list = typing.cast(list[ChannelArrayBlock], dep_list)
for dep in dep_list:
address = dep.to_blocks(address, blocks, defined_texts, cc_map)
for k, dep in enumerate(dep_list[:-1]):
dep.composition_addr = dep_list[k + 1].address
dep_list[-1].composition_addr = 0
channel.component_addr = dep_list[0].address
else:
dep_list = typing.cast(list[tuple[int, int]], dep_list)
index = dep_list[0][1]
addr_ = gp.channels[index].address
group_channels = gp.channels
if group_channels:
for j, channel in enumerate(group_channels[:-1]):
channel.next_ch_addr = group_channels[j + 1].address
group_channels[-1].next_ch_addr = 0
# channel dependencies
j = len(channels) - 1
while j >= 0:
dep_list = gp.channel_dependencies[j]
if dep_list and all(isinstance(dep, tuple) for dep in dep_list):
dep_list = typing.cast(list[tuple[int, int]], dep_list)
index = dep_list[0][1]
channels[j].component_addr = channels[index].address
index = dep_list[-1][1]
channels[j].next_ch_addr = channels[index].next_ch_addr
channels[index].next_ch_addr = 0
for _, ch_nr in dep_list:
channels[ch_nr].source_addr = 0
j -= 1
# channel group
if gp.channel_group.flags & v4c.FLAG_CG_VLSD:
continue
gp.channel_group.first_sample_reduction_addr = 0
if channels:
gp.channel_group.first_ch_addr = gp.channels[0].address
else:
gp.channel_group.first_ch_addr = 0
gp.channel_group.next_cg_addr = 0
address = gp.channel_group.to_blocks(address, blocks, defined_texts, si_map)
gp.data_group.first_cg_addr = gp.channel_group.address
cg_map[i] = gp.channel_group.address
if progress is not None:
progress.signals.setValue.emit(int(50 * (i + 1) / groups_nr) + 25)
if progress.stop:
dst_.close()
self.close()
raise Terminated
for gp in self.groups:
for dep_list in gp.channel_dependencies:
if dep_list:
if all(isinstance(dep, ChannelArrayBlock) for dep in dep_list):
dep_list = typing.cast(list[ChannelArrayBlock], dep_list)
for dep in dep_list:
for i, (gp_nr, ch_nr) in enumerate(filter(None, dep.dynamic_size_channels)):
grp = self.groups[gp_nr]
ch = grp.channels[ch_nr]
dep[f"dynamic_size_{i}_dg_addr"] = grp.data_group.address
dep[f"dynamic_size_{i}_cg_addr"] = grp.channel_group.address
dep[f"dynamic_size_{i}_ch_addr"] = ch.address
for i, (gp_nr, ch_nr) in enumerate(filter(None, dep.input_quantity_channels)):
grp = self.groups[gp_nr]
ch = grp.channels[ch_nr]
dep[f"input_quantity_{i}_dg_addr"] = grp.data_group.address
dep[f"input_quantity_{i}_cg_addr"] = grp.channel_group.address
dep[f"input_quantity_{i}_ch_addr"] = ch.address
if dep.output_quantity_channel:
gp_nr, ch_nr = dep.output_quantity_channel
grp = self.groups[gp_nr]
ch = grp.channels[ch_nr]
dep.output_quantity_dg_addr = grp.data_group.address
dep.output_quantity_cg_addr = grp.channel_group.address
dep.output_quantity_ch_addr = ch.address
if dep.comparison_quantity_channel:
gp_nr, ch_nr = dep.comparison_quantity_channel
grp = self.groups[gp_nr]
ch = grp.channels[ch_nr]
dep.comparison_quantity_dg_addr = grp.data_group.address
dep.comparison_quantity_cg_addr = grp.channel_group.address
dep.comparison_quantity_ch_addr = ch.address
for i, (gp_nr, ch_nr) in enumerate(filter(None, dep.axis_channels)):
grp = self.groups[gp_nr]
ch = grp.channels[ch_nr]
dep[f"scale_axis_{i}_dg_addr"] = grp.data_group.address
dep[f"scale_axis_{i}_cg_addr"] = grp.channel_group.address
dep[f"scale_axis_{i}_ch_addr"] = ch.address
position = tell()
for gp in self.groups:
gp.data_group.record_id_len = 0
cg_master_index = gp.channel_group.cg_master_index
if cg_master_index is not None:
gp.channel_group.cg_master_addr = cg_map[cg_master_index]
seek(gp.channel_group.address)
write(bytes(gp.channel_group))
seek(position)
ev_map = []
if self.events:
for event in self.events:
for i, ref in enumerate(event.scopes):
if isinstance(ref, tuple):
dg_cntr, ch_cntr = ref
event[f"scope_{i}_addr"] = self.groups[dg_cntr].channels[ch_cntr].address
else:
dg_cntr = ref
event[f"scope_{i}_addr"] = self.groups[dg_cntr].channel_group.address
blocks.append(event)
ev_map.append(address)
event.address = address
address += event.block_len
if event.name:
tx_block = TextBlock(text=event.name)
tx_block.address = address
blocks.append(tx_block)
address += tx_block.block_len
event.name_addr = tx_block.address
else:
event.name_addr = 0
if event.comment:
meta = event.comment.startswith("<EVcomment")
tx_block = TextBlock(text=event.comment, meta=meta)
tx_block.address = address
blocks.append(tx_block)
address += tx_block.block_len
event.comment_addr = tx_block.address
else:
event.comment_addr = 0
if event.parent is not None:
event.parent_ev_addr = ev_map[event.parent]
if event.range_start is not None:
event.range_start_ev_addr = ev_map[event.range_start]
for i in range(len(self.events) - 1):
self.events[i].next_ev_addr = self.events[i + 1].address
self.events[-1].next_ev_addr = 0
self.header.first_event_addr = self.events[0].address
if progress is not None and progress.stop:
dst_.close()
self.close()
raise Terminated
# attachments
at_map: dict[int, int] = {}
if self.attachments:
# put the attachment texts before the attachments
for at_block in self.attachments:
for text in (at_block.file_name, at_block.mime, at_block.comment):
if text not in defined_texts:
tx_block = TextBlock(text=str(text))
defined_texts[text] = address
tx_block.address = address
address += tx_block.block_len
blocks.append(tx_block)
for at_block in self.attachments:
address = at_block.to_blocks(address, blocks, defined_texts)
for i in range(len(self.attachments) - 1):
at_block = self.attachments[i]
at_block.next_at_addr = self.attachments[i + 1].address
self.attachments[-1].next_at_addr = 0
if self.events:
for event in self.events:
for i in range(event.attachment_nr):
key = f"attachment_{i}_addr"
addr = typing.cast(int, event[key])
event[key] = at_map[addr]
for i, gp in enumerate(self.groups):
for j, channel in enumerate(gp.channels):
if channel.attachment is not None:
channel.attachment_addr = self.attachments[channel.attachment].address
elif channel.attachment_nr:
channel.attachment_addr = 0
if channel.channel_type == v4c.CHANNEL_TYPE_SYNC and channel.attachment is not None:
channel.data_block_addr = self.attachments[channel.attachment].address
if progress is not None:
blocks_nr = len(blocks)
threshold = blocks_nr / 25
count = 1
for i, block in enumerate(blocks):
write(bytes(block))
if i >= threshold:
progress.signals.setValue.emit(75 + count)
count += 1
threshold += blocks_nr / 25
else:
for block in blocks:
write(bytes(block))
for gp, rec_id in zip(self.groups, gp_rec_ids, strict=False):
gp.data_group.record_id_len = rec_id
if valid_data_groups:
addr_ = valid_data_groups[0].address
self.header.first_dg_addr = addr_
else:
self.header.first_dg_addr = 0
self.header.file_history_addr = self.file_history[0].address
if self.attachments:
first_attachment = self.attachments[0]
addr_ = first_attachment.address
self.header.first_attachment_addr = addr_
else:
self.header.first_attachment_addr = 0
seek(v4c.IDENTIFICATION_BLOCK_SIZE)
write(bytes(self.header))
for orig_addr, gp in zip(original_data_addresses, self.groups, strict=False):
gp.data_group.data_block_addr = orig_addr
at_map = {value: key for key, value in at_map.items()}
for event in self.events:
for i in range(event.attachment_nr):
key = f"attachment_{i}_addr"
addr = typing.cast(int, event[key])
event[key] = at_map[addr]
except:
if not file_like:
dst_.close()
raise
else:
if not file_like:
dst_.close()
if suffix in (".zip", ".mf4z"):
output_fname = dst.with_suffix(suffix)
try:
zipped_mf4 = ZipFile(output_fname, "w", compression=ZIP_DEFLATED)
zipped_mf4.write(
str(dst),
dst.name,
compresslevel=1,
)
zipped_mf4.close()
os.remove(destination)
dst = output_fname
except:
pass
if dst == self.name:
self.close()
try:
Path.unlink(self.name)
Path.rename(destination, self.name)
except:
pass
self._tempfile = NamedTemporaryFile(dir=self.temporary_folder)
self._file = open(self.name, "rb")
self._read(self._file)
return dst
[docs]
def get_channel_name(self, group: int, index: int) -> str:
"""Get channel name.
Parameters
----------
group : int
0-based group index.
index : int
0-based channel index.
Returns
-------
name : str
Found channel name.
"""
gp_nr, ch_nr = self._validate_channel_selection(None, group, index)
return self.groups[gp_nr].channels[ch_nr].name
def get_channel_metadata(
self,
name: str | None = None,
group: int | None = None,
index: int | None = None,
) -> Channel:
gp_nr, ch_nr = self._validate_channel_selection(name, group, index)
grp = self.groups[gp_nr]
channel = grp.channels[ch_nr]
return channel
[docs]
def get_channel_unit(
self,
name: str | None = None,
group: int | None = None,
index: int | None = None,
) -> str:
"""Get channel unit.
The channel can be specified in two ways:
* Using the first positional argument `name`.
* If there are multiple occurrences for this channel, then the `group`
and `index` arguments can be used to select a specific group.
* If there are multiple occurrences for this channel and either the
`group` or `index` arguments is None, then a warning is issued.
* Using the group number (keyword argument `group`) and the channel
number (keyword argument `index`). Use `info` method for group and
channel numbers.
Parameters
----------
name : str, optional
Name of channel.
group : int, optional
0-based group index.
index : int, optional
0-based channel index.
Returns
-------
unit : str
Found channel unit.
"""
gp_nr, ch_nr = self._validate_channel_selection(name, group, index)
grp = self.groups[gp_nr]
channel = grp.channels[ch_nr]
conversion = channel.conversion
unit = (conversion and conversion.unit) or channel.unit or ""
return unit
def _finalize(self, stream: FileLike | mmap.mmap) -> None:
"""
Attempt finalization of the file.
:return: None
"""
flags = self.identification.unfinalized_standard_flags
blocks, block_groups, addresses = all_blocks_addresses(stream)
stream.seek(0, 2)
limit = stream.tell()
mapped = self._mapped
if flags & v4c.FLAG_UNFIN_UPDATE_LAST_DL:
for dg_addr in block_groups[b"##DG"]:
group = DataGroup(address=dg_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
data_addr = group.data_block_addr
if not data_addr:
continue
stream.seek(data_addr)
blk_id = stream.read(4)
if blk_id == b"##DT":
continue
elif blk_id in (b"##DL", b"##HL"):
if blk_id == b"##HL":
hl = HeaderList(address=data_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
data_addr = hl.first_dl_addr
while True:
dl = DataList(address=data_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
if not dl.next_dl_addr:
break
kwargs: DataListKwargs = {}
count = dl.links_nr - 1
valid_count = 0
for i in range(count):
dt_addr = dl[f"data_block_addr{i}"]
if dt_addr:
valid_count += 1
kwargs[f"data_block_addr{i}"] = dt_addr # type: ignore[literal-required]
else:
break
starting_address = dl.address
next_block_position = bisect.bisect_right(addresses, starting_address)
# search for data blocks after the DLBLOCK
for j in range(i, count):
if next_block_position >= len(addresses):
break
next_block_address = addresses[next_block_position]
next_block_type = blocks[next_block_address]
if next_block_type not in {b"##DZ", b"##DT", b"##DV", b"##DI"}:
break
else:
stream.seek(next_block_address + v4c.DZ_INFO_COMMON_OFFSET)
if next_block_type == b"##DZ":
(
zip_type,
param,
original_size,
zip_size,
) = v4c.DZ_COMMON_INFO_uf(stream.read(v4c.DZ_COMMON_INFO_SIZE))
exceeded = limit - (next_block_address + v4c.DZ_COMMON_SIZE + zip_size) < 0
else:
id_string, block_len = COMMON_SHORT_uf(stream.read(v4c.COMMON_SIZE))
original_size = block_len - 24
exceeded = limit - (next_block_address + block_len) < 0
# update the data block size in case all links were NULL before
if i == 0 and (dl.flags & v4c.FLAG_DL_EQUAL_LENGHT):
kwargs["data_block_len"] = original_size
# check if the file limit is exceeded
if exceeded:
break
else:
next_block_position += 1
valid_count += 1
kwargs[f"data_block_addr{j}"] = next_block_address # type: ignore[literal-required]
kwargs["links_nr"] = valid_count + 1
kwargs["flags"] = dl.flags
if dl.flags & v4c.FLAG_DL_EQUAL_LENGHT:
kwargs["data_block_len"] = dl.data_block_len
else:
for i in range(valid_count):
kwargs[f"offset_{i}"] = dl[f"offset_{i}"] # type: ignore[literal-required]
stream.seek(data_addr)
stream.write(bytes(DataList(**kwargs)))
self.identification.unfinalized_standard_flags -= v4c.FLAG_UNFIN_UPDATE_LAST_DL
if flags & v4c.FLAG_UNFIN_UPDATE_LAST_DT_LENGTH:
try:
for dg_addr in block_groups[b"##DG"]:
group = DataGroup(address=dg_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
data_addr = group.data_block_addr
if not data_addr:
continue
stream.seek(data_addr)
blk_id = stream.read(4)
if blk_id == b"##DT":
blk = DataBlock(address=data_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
elif blk_id == b"##DL":
while True:
dl = DataList(address=data_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
if not dl.next_dl_addr:
break
data_addr = typing.cast(int, dl[f"data_block_addr{dl.links_nr - 2}"])
blk = DataBlock(address=data_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
elif blk_id == b"##HL":
hl = HeaderList(address=data_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
data_addr = hl.first_dl_addr
while True:
dl = DataList(address=data_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
if not dl.next_dl_addr:
break
data_addr = typing.cast(int, dl[f"data_block_addr{dl.links_nr - 2}"])
blk = DataBlock(address=data_addr, stream=stream, mapped=mapped, file_limit=self.file_limit)
next_block = bisect.bisect_right(addresses, data_addr)
if next_block == len(addresses):
block_len = limit - data_addr
else:
block_len = addresses[next_block] - data_addr
blk.block_len = block_len
stream.seek(data_addr)
stream.write(bytes(blk))
except:
print(format_exc())
raise
self.identification.unfinalized_standard_flags -= v4c.FLAG_UNFIN_UPDATE_LAST_DT_LENGTH
self.identification.file_identification = b"MDF "
def _sort(
self,
stream: FileLike | mmap.mmap,
compress: bool = True,
current_progress_index: int = 0,
max_progress_count: int = 0,
progress: Callable[[int, int], None] | None = None,
) -> None:
flags = self.identification.unfinalized_standard_flags
common: defaultdict[int, list[tuple[int, int]]] = defaultdict(list)
for i, group in enumerate(self.groups):
if group.sorted:
continue
try:
data_block = next(group.get_data_blocks())
common[data_block.address].append((i, group.channel_group.record_id))
except:
continue
read = stream.read
seek = stream.seek
self._tempfile.seek(0, 2)
tell = self._tempfile.tell
write = self._tempfile.write
for address, groups in common.items():
cg_map = {rec_id: self.groups[index_].channel_group for index_, rec_id in groups}
final_records: dict[int, list[DataBlockInfo]] = {id_: [] for (_, id_) in groups}
for rec_id, channel_group in cg_map.items():
if channel_group.address in self._cn_data_map:
gp_idx, cn_idx = self._cn_data_map[channel_group.address]
self.groups[gp_idx].signal_data[cn_idx] = ([], iter(EMPTY_TUPLE))
group = self.groups[groups[0][0]]
record_id_nr = group.data_group.record_id_len
cg_size = group.record_size
match record_id_nr:
case 1:
_unpack_stuct = UINT8_uf
case 2:
_unpack_stuct = UINT16_uf
case 4:
_unpack_stuct = UINT32_uf
case 8:
_unpack_stuct = UINT64_uf
case _:
message = f"invalid record id size {record_id_nr}"
raise MdfException(message)
rem = b""
blocks = list(group.get_data_blocks()) # might be expensive ?
# most of the steps are for sorting, but the last 2 are after we've done sorting
# so remove the 2 steps that are not related to sorting from the count
step = float(SORT_STEPS - 2) / len(blocks) / len(common)
count = float(current_progress_index)
previous = count
for block_info in blocks:
dtblock_address, dtblock_raw_size, dtblock_size, block_type, param = (
block_info.address,
typing.cast(int, block_info.original_size),
block_info.compressed_size,
block_info.block_type,
block_info.param,
)
count += step
# if we've been told to notify about progress
# and we've been given a max progress count (only way we can do progress updates)
# and there's a tick update (at least 1 integer between the last update and the current index)
# then we can notify about the callback progress
if callable(progress) and max_progress_count and floor(previous) < floor(count):
progress(floor(count), max_progress_count)
previous = count
seek(dtblock_address)
if block_type:
partial_records: dict[int, list[bytes]] = {id_: [] for _, id_ in groups}
new_data = read(dtblock_size)
decompress = DECOMPRESS_FUNC_MAP[block_type]
new_data = decompress(new_data)
if block_type % 2 == 0:
# tranposed data
cols = typing.cast(int, param)
lines = dtblock_raw_size // cols
matrix_size = lines * cols
if matrix_size != dtblock_raw_size:
new_data = (
frombuffer(new_data[:matrix_size], dtype=uint8)
.reshape((cols, lines))
.T.ravel()
.tobytes()
+ new_data[matrix_size:]
)
else:
new_data = frombuffer(new_data, dtype=uint8).reshape((cols, lines)).T.ravel().tobytes()
new_data = rem + new_data
try:
rem = sort_data_block(
new_data,
partial_records,
cg_size,
record_id_nr,
_unpack_stuct,
)
except:
print(format_exc())
raise
for rec_id, records in partial_records.items():
channel_group = cg_map[rec_id]
dg_cntr: int | None
ch_cntr: int | None
if channel_group.address in self._cn_data_map:
dg_cntr, ch_cntr = self._cn_data_map[channel_group.address]
else:
dg_cntr, ch_cntr = None, None
if records:
tempfile_address = tell()
new_data = b"".join(records)
original_size = len(new_data)
if original_size:
if compress:
new_data = lz_compress(new_data, store_size=True)
compressed_size = len(new_data)
write(new_data)
if dg_cntr is not None and ch_cntr is not None:
info = SignalDataBlockInfo(
address=tempfile_address,
compressed_size=compressed_size,
original_size=original_size,
block_type=v4c.DZ_BLOCK_LZ,
location=v4c.LOCATION_TEMPORARY_FILE,
)
signal_data = typing.cast(
list[tuple[list[SignalDataBlockInfo], Iterator[SignalDataBlockInfo]]],
self.groups[dg_cntr].signal_data,
)
signal_data[ch_cntr][0].append(info)
else:
block_info = DataBlockInfo(
address=tempfile_address,
block_type=v4c.DZ_BLOCK_LZ,
compressed_size=compressed_size,
original_size=original_size,
param=0,
location=v4c.LOCATION_TEMPORARY_FILE,
)
final_records[rec_id].append(block_info)
else:
write(new_data)
if dg_cntr is not None and ch_cntr is not None:
info = SignalDataBlockInfo(
address=tempfile_address,
compressed_size=original_size,
original_size=original_size,
block_type=v4c.DT_BLOCK,
location=v4c.LOCATION_TEMPORARY_FILE,
)
signal_data = typing.cast(
list[tuple[list[SignalDataBlockInfo], Iterator[SignalDataBlockInfo]]],
self.groups[dg_cntr].signal_data,
)
signal_data[ch_cntr][0].append(info)
else:
block_info = DataBlockInfo(
address=tempfile_address,
block_type=v4c.DT_BLOCK,
compressed_size=original_size,
original_size=original_size,
param=0,
location=v4c.LOCATION_TEMPORARY_FILE,
)
final_records[rec_id].append(block_info)
else: # DTBLOCK
seek(dtblock_address)
limit = 32 * 1024 * 1024 # 32MB
while dtblock_size:
if dtblock_size > limit:
dtblock_size -= limit
new_data = rem + read(limit)
else:
new_data = rem + read(dtblock_size)
dtblock_size = 0
partial_records = {id_: [] for _, id_ in groups}
rem = sort_data_block(
new_data,
partial_records,
cg_size,
record_id_nr,
_unpack_stuct,
)
for rec_id, records in partial_records.items():
channel_group = cg_map[rec_id]
if channel_group.address in self._cn_data_map:
dg_cntr, ch_cntr = self._cn_data_map[channel_group.address]
else:
dg_cntr, ch_cntr = None, None
if records:
tempfile_address = tell()
new_data = b"".join(records)
original_size = len(new_data)
if original_size:
if compress:
new_data = lz_compress(new_data, store_size=False)
compressed_size = len(new_data)
write(new_data)
if dg_cntr is not None and ch_cntr is not None:
info = SignalDataBlockInfo(
address=tempfile_address,
compressed_size=compressed_size,
original_size=original_size,
block_type=v4c.DZ_BLOCK_LZ,
location=v4c.LOCATION_TEMPORARY_FILE,
)
signal_data = typing.cast(
list[tuple[list[SignalDataBlockInfo], Iterator[SignalDataBlockInfo]]],
self.groups[dg_cntr].signal_data,
)
signal_data[ch_cntr][0].append(info)
else:
block_info = DataBlockInfo(
address=tempfile_address,
block_type=v4c.DZ_BLOCK_LZ,
compressed_size=compressed_size,
original_size=original_size,
param=None,
location=v4c.LOCATION_TEMPORARY_FILE,
)
final_records[rec_id].append(block_info)
else:
write(new_data)
if dg_cntr is not None and ch_cntr is not None:
info = SignalDataBlockInfo(
address=tempfile_address,
compressed_size=original_size,
original_size=original_size,
block_type=v4c.DT_BLOCK,
location=v4c.LOCATION_TEMPORARY_FILE,
)
signal_data = typing.cast(
list[tuple[list[SignalDataBlockInfo], Iterator[SignalDataBlockInfo]]],
self.groups[dg_cntr].signal_data,
)
signal_data[ch_cntr][0].append(info)
else:
block_info = DataBlockInfo(
address=tempfile_address,
block_type=v4c.DT_BLOCK,
compressed_size=original_size,
original_size=original_size,
param=None,
location=v4c.LOCATION_TEMPORARY_FILE,
)
final_records[rec_id].append(block_info)
# after we read all DTBLOCKs in the original file,
# we assign freshly created blocks from temporary file to
# corresponding groups.
for idx, rec_id in groups:
group = self.groups[idx]
group.data_location = v4c.LOCATION_TEMPORARY_FILE
group.set_blocks_info(final_records[rec_id])
group.sorted = True
for i, group in enumerate(self.groups):
if flags & v4c.FLAG_UNFIN_UPDATE_CG_COUNTER:
channel_group = group.channel_group
if channel_group.flags & v4c.FLAG_CG_VLSD:
continue
if self.version >= "4.20" and channel_group.flags & v4c.FLAG_CG_REMOTE_MASTER:
index = typing.cast(int, channel_group.cg_master_index)
else:
index = i
if group.uses_ld:
samples_size = channel_group.samples_byte_nr
else:
samples_size = channel_group.samples_byte_nr + channel_group.invalidation_bytes_nr
total_size = sum(typing.cast(int, blk.original_size) for blk in group.get_data_blocks())
cycles_nr = total_size // samples_size
virtual_channel_group = self.virtual_groups[index]
virtual_channel_group.cycles_nr = cycles_nr
channel_group.cycles_nr = cycles_nr
if self.identification.unfinalized_standard_flags & v4c.FLAG_UNFIN_UPDATE_CG_COUNTER:
self.identification.unfinalized_standard_flags -= v4c.FLAG_UNFIN_UPDATE_CG_COUNTER
if self.identification.unfinalized_standard_flags & v4c.FLAG_UNFIN_UPDATE_VLSD_BYTES:
self.identification.unfinalized_standard_flags -= v4c.FLAG_UNFIN_UPDATE_VLSD_BYTES
def _process_bus_logging(self) -> None:
groups_count = len(self.groups)
for index in range(groups_count):
group = self.groups[index]
if group.channel_group.flags & v4c.FLAG_CG_BUS_EVENT:
source = group.channel_group.acq_source
if (
source
and source.bus_type in (v4c.BUS_TYPE_CAN, v4c.BUS_TYPE_OTHER)
and "CAN_DataFrame" in [ch.name for ch in group.channels]
):
try:
self._process_can_logging(index, group)
except Exception:
message = f"Error during CAN logging processing: {format_exc()}"
logger.error(message)
if (
source
and source.bus_type in (v4c.BUS_TYPE_LIN, v4c.BUS_TYPE_OTHER)
and "LIN_Frame" in [ch.name for ch in group.channels]
):
try:
self._process_lin_logging(index, group)
except Exception as e:
message = f"Error during LIN logging processing: {e}"
logger.error(message)
def _process_can_logging(self, group_index: int, grp: Group) -> None:
channels = grp.channels
group = grp
dbc = None
for channel in channels:
if channel.name == "CAN_DataFrame":
attachment_addr = channel.attachment
if attachment_addr is not None:
if attachment_addr not in self._dbc_cache:
try:
attachment, at_name, md5_sum = self.extract_attachment(
index=attachment_addr,
)
except:
print(format_exc())
continue
if at_name.suffix.lower() not in (".arxml", ".dbc"):
message = f'Expected .dbc or .arxml file as CAN channel attachment but got "{at_name}"'
logger.warning(message)
elif not attachment:
message = f'Attachment "{at_name}" not found'
logger.warning(message)
else:
dbc = load_can_database(at_name, contents=attachment)
if dbc:
self._dbc_cache[attachment_addr] = dbc
else:
dbc = self._dbc_cache[attachment_addr]
break
if not group.channel_group.flags & v4c.FLAG_CG_PLAIN_BUS_EVENT:
self._prepare_record(group)
data = self._load_data(group, record_offset=0, record_count=1)
for fragment in data:
self._set_temporary_master(None)
self._set_temporary_master(self.get_master(group_index, data=fragment, one_piece=True))
bus_ids = self.get(
"CAN_DataFrame.BusChannel",
group=group_index,
data=fragment,
samples_only=True,
)[
0
].astype("<u1")
msg_ids = (
self.get(
"CAN_DataFrame.ID",
group=group_index,
data=fragment,
samples_only=True,
)[
0
].astype("<u4")
& 0x1FFFFFFF
)
if len(bus_ids) == 0:
continue
bus = bus_ids[0]
msg_id = msg_ids[0]
bus_map = self.bus_logging_map["CAN"].setdefault(bus, {})
bus_map[int(msg_id)] = group_index
self._set_temporary_master(None)
elif dbc is None:
self._prepare_record(group)
data = self._load_data(group, optimize_read=False)
for fragment in data:
self._set_temporary_master(None)
self._set_temporary_master(self.get_master(group_index, data=fragment, one_piece=True))
bus_ids = self.get(
"CAN_DataFrame.BusChannel",
group=group_index,
data=fragment,
samples_only=True,
)[
0
].astype("<u1")
msg_ids = (
self.get(
"CAN_DataFrame.ID",
group=group_index,
data=fragment,
samples_only=True,
)[
0
].astype("<u4")
& 0x1FFFFFFF
)
if len(bus_ids) == 0:
continue
buses = unique(bus_ids)
for bus in buses:
bus_msg_ids = msg_ids[bus_ids == bus]
unique_id_array = unique(bus_msg_ids)
unique_id_array.sort()
unique_ids = typing.cast(list[int], unique_id_array.tolist())
bus_map = self.bus_logging_map["CAN"].setdefault(bus, {})
for msg_id in unique_ids:
bus_map[int(msg_id)] = group_index
self._set_temporary_master(None)
else:
is_j1939 = dbc.contains_j1939
if is_j1939:
messages = {frame.arbitration_id.pgn: frame for frame in dbc}
else:
messages = {frame.arbitration_id.id: frame for frame in dbc}
msg_map = {}
self._prepare_record(group)
data = self._load_data(group, optimize_read=False)
for fragment in data:
self._set_temporary_master(None)
self._set_temporary_master(self.get_master(group_index, data=fragment, one_piece=True))
data_bytes = self.get(
"CAN_DataFrame.DataBytes",
group=group_index,
data=fragment,
samples_only=True,
)[0]
bus_ids = self.get(
"CAN_DataFrame.BusChannel",
group=group_index,
data=fragment,
samples_only=True,
)[
0
].astype("<u1")
msg_id_signal = (
self.get("CAN_DataFrame.ID", group=group_index, data=fragment).astype("<u4") & 0x1FFFFFFF
)
if is_j1939:
tmp_pgn = msg_id_signal.samples >> 8
ps = tmp_pgn & 0xFF
pf = (msg_id_signal.samples >> 16) & 0xFF
_pgn = tmp_pgn & 0x3FF00
msg_id_signal.samples = where(pf >= 240, _pgn + ps, _pgn)
buses = unique(bus_ids)
if len(bus_ids) == 0:
continue
for bus in buses:
idx_ = bus_ids == bus
bus_msg_ids = msg_id_signal.samples[idx_]
bus_t = msg_id_signal.timestamps[idx_]
bus_data_bytes = data_bytes[idx_]
unique_ids = sorted(unique(bus_msg_ids).astype("<u8"))
bus_map = self.bus_logging_map["CAN"].setdefault(bus, {})
for msg_id in unique_ids:
bus_map[int(msg_id)] = group_index
for msg_id in unique_ids:
frame = messages.get(msg_id, None)
if frame is None:
continue
idx = bus_msg_ids == msg_id
payload = bus_data_bytes[idx]
t = bus_t[idx]
extracted_signals = bus_logging_utils.extract_mux(payload, frame, msg_id, bus, t, raw=True)
for msg, signals in extracted_signals.items():
if len(next(iter(signals.values()))["samples"]) == 0:
continue
if msg not in msg_map:
sigs: list[Signal] = []
for name_, signal in signals.items():
sig = Signal(
samples=signal["samples"],
timestamps=signal["t"],
name=signal["name"],
comment=signal["comment"],
unit=signal["unit"],
invalidation_bits=signal["invalidation_bits"],
display_names={
f"{frame.name}.{signal['name']}": "message_name",
f"CAN{bus}.{frame.name}.{signal['name']}": "bus_name",
},
conversion=signal["conversion"],
)
sigs.append(sig)
cg_nr = self.append(
sigs,
acq_name=f"from CAN{bus} message ID=0x{msg_id:X}",
comment=f"{frame} 0x{msg_id:X}",
common_timebase=True,
)
msg_map[msg] = cg_nr
for ch_index, ch in enumerate(self.groups[cg_nr].channels):
if ch_index == 0:
continue
entry = cg_nr, ch_index
name_ = f"{frame}.{ch.name}"
self.channels_db.add(name_, entry)
name_ = f"CAN{bus}.{frame}.{ch.name}"
self.channels_db.add(name_, entry)
name_ = f"CAN_DataFrame_{msg_id}.{ch.name}"
self.channels_db.add(name_, entry)
name_ = f"CAN{bus}.CAN_DataFrame_{msg_id}.{ch.name}"
self.channels_db.add(name_, entry)
else:
index = msg_map[msg]
sigs_samples: list[tuple[NDArray[Any], NDArray[np.bool] | None]] = []
for name_, signal in signals.items():
sigs_samples.append((signal["samples"], signal["invalidation_bits"]))
t = signal["t"]
sigs_samples.insert(0, (t, None))
self.extend(index, sigs_samples)
self._set_temporary_master(None)
def _process_lin_logging(self, group_index: int, grp: Group) -> None:
channels = grp.channels
group = grp
dbc = None
for channel in channels:
if channel.name == "LIN_Frame":
attachment_addr = channel.attachment
if attachment_addr is not None:
if attachment_addr not in self._dbc_cache:
try:
attachment, at_name, md5_sum = self.extract_attachment(
index=attachment_addr,
)
except:
print(format_exc())
continue
if at_name.suffix.lower() not in (".arxml", ".dbc", ".ldf"):
message = (
f'Expected .dbc, .arxml or .ldf file as LIN channel attachment but got "{at_name}"'
)
logger.warning(message)
elif not attachment:
message = f'Attachment "{at_name}" not found'
logger.warning(message)
else:
contents = None if at_name.suffix.lower() == ".ldf" else attachment
dbc = load_can_database(at_name, contents=contents)
if dbc:
self._dbc_cache[attachment_addr] = dbc
else:
dbc = self._dbc_cache[attachment_addr]
break
if dbc is None:
self._prepare_record(group)
data = self._load_data(group, optimize_read=False)
for fragment in data:
self._set_temporary_master(None)
self._set_temporary_master(self.get_master(group_index, data=fragment, one_piece=True))
msg_ids = (
self.get(
"LIN_Frame.ID",
group=group_index,
data=fragment,
samples_only=True,
)[
0
].astype("<u4")
& 0x1FFFFFFF
)
unique_ids = sorted(unique(msg_ids).astype("<u8"))
lin_map = self.bus_logging_map["LIN"]
for msg_id in unique_ids:
lin_map[int(msg_id)] = group_index
self._set_temporary_master(None)
else:
messages = {frame.arbitration_id.id: frame for frame in dbc}
msg_map: dict[tuple[int | None, int | None, bool, int | None, str | None, int, int], int] = {}
self._prepare_record(group)
data = self._load_data(group, optimize_read=False)
for fragment in data:
self._set_temporary_master(None)
self._set_temporary_master(self.get_master(group_index, data=fragment, one_piece=True))
sig = self.get("LIN_Frame.ID", group=group_index, data=fragment).astype("<u4") & 0x1FFFFFFF
data_bytes = self.get(
"LIN_Frame.DataBytes",
group=group_index,
data=fragment,
samples_only=True,
)[0]
bus_msg_ids = sig.samples
bus_t = sig.timestamps
bus_data_bytes = data_bytes
unique_ids = sorted(unique(bus_msg_ids).astype("<u8"))
lin_map = self.bus_logging_map["LIN"]
for msg_id in unique_ids:
lin_map[int(msg_id)] = group_index
for msg_id in unique_ids:
frame = messages.get(msg_id, None)
if frame is None:
continue
idx = bus_msg_ids == msg_id
payload = bus_data_bytes[idx]
t = bus_t[idx]
extracted_signals = bus_logging_utils.extract_mux(payload, frame, msg_id, 0, t, raw=True)
for msg, signals in extracted_signals.items():
if len(next(iter(signals.values()))["samples"]) == 0:
continue
if msg not in msg_map:
sigs = []
for name_, signal in signals.items():
sig = Signal(
samples=signal["samples"],
timestamps=signal["t"],
name=signal["name"],
comment=signal["comment"],
unit=signal["unit"],
invalidation_bits=signal["invalidation_bits"],
display_names={
f"{frame.name}.{signal['name']}": "message_name",
f"LIN.{frame.name}.{signal['name']}": "bus_name",
},
conversion=signal["conversion"],
)
sigs.append(sig)
cg_nr = self.append(
sigs,
acq_name=f"from LIN message ID=0x{msg_id:X}",
comment=f"{frame} 0x{msg_id:X}",
common_timebase=True,
)
msg_map[msg] = cg_nr
for ch_index, ch in enumerate(self.groups[cg_nr].channels):
if ch_index == 0:
continue
entry = cg_nr, ch_index
name_ = f"{frame}.{ch.name}"
self.channels_db.add(name_, entry)
name_ = f"LIN.{frame}.{ch.name}"
self.channels_db.add(name_, entry)
name_ = f"LIN_Frame_{msg_id}.{ch.name}"
self.channels_db.add(name_, entry)
name_ = f"LIN.LIN_Frame_{msg_id}.{ch.name}"
self.channels_db.add(name_, entry)
else:
index = msg_map[msg]
sigs_samples: list[tuple[NDArray[Any], NDArray[np.bool] | None]] = []
for name_, signal in signals.items():
sigs_samples.append((signal["samples"], signal["invalidation_bits"]))
t = signal["t"]
sigs_samples.insert(0, (t, None))
self.extend(index, sigs_samples)
self._set_temporary_master(None)
def reload_header(self) -> None:
if not self._file:
raise RuntimeError("self._file is None")
self.header = HeaderBlock(address=0x40, stream=self._file)
def debug_channel(
mdf: MDF4,
group: Group,
channel: Channel,
dependency: list[ChannelArrayBlock] | list[tuple[int, int]] | None,
file: StringIO | None = None,
) -> None:
"""Use this to print debug information in case of errors.
Parameters
----------
mdf : MDF
Source MDF object.
group : dict
Group.
channel : Channel
Channel object.
dependency : ChannelDependency
Channel dependency object.
"""
print("MDF", "=" * 76, file=file)
print("name:", mdf.name, file=file)
print("version:", mdf.version, file=file)
print("read fragment size:", mdf._read_fragment_size, file=file)
print("write fragment size:", mdf._write_fragment_size, file=file)
print()
record = mdf._prepare_record(group)
print("GROUP", "=" * 74, file=file)
print("sorted:", group.sorted, file=file)
print("data location:", group.data_location, file=file)
print("data blocks:", group.data_blocks, file=file)
print("dependencies", group.channel_dependencies, file=file)
print("record:", record, file=file)
print(file=file)
cg = group.channel_group
print("CHANNEL GROUP", "=" * 66, file=file)
print(cg, cg.cycles_nr, cg.samples_byte_nr, cg.invalidation_bytes_nr, file=file)
print(file=file)
print("CHANNEL", "=" * 72, file=file)
print(channel, file=file)
print(file=file)
print("CHANNEL ARRAY", "=" * 66, file=file)
print(dependency, file=file)
print(file=file)