11from __future__ import annotations
22
3+ import datetime as dt
34import sys
5+ import warnings
46from collections .abc import Sequence
5- from typing import (
6- Any ,
7- Generic ,
8- SupportsIndex ,
9- TypeVar ,
10- overload ,
11- )
7+ from typing import Any , Generic , SupportsIndex , TypeVar , Union , cast , overload
128
9+ import hightime as ht
1310import numpy as np
1411import numpy .typing as npt
1512
16- from nitypes ._arguments import arg_to_uint , validate_dtype
13+ from nitypes ._arguments import arg_to_uint , validate_dtype , validate_unsupported_arg
1714from nitypes ._exceptions import invalid_arg_type , invalid_array_ndim
15+ from nitypes ._typing import TypeAlias
1816from nitypes .waveform ._extended_properties import (
1917 CHANNEL_NAME ,
2018 UNIT_DESCRIPTION ,
2119 ExtendedPropertyDictionary ,
2220)
2321from nitypes .waveform ._scaling import NO_SCALING , ScaleMode
24- from nitypes .waveform ._timing import PrecisionTiming , Timing , convert_timing
22+ from nitypes .waveform ._timing import BaseTiming , PrecisionTiming , Timing , convert_timing
23+ from nitypes .waveform ._warnings import scale_mode_mismatch
2524
2625if sys .version_info < (3 , 10 ):
2726 import array as std_array
2827
28+
2929_ScalarType = TypeVar ("_ScalarType" , bound = np .generic )
3030_ScalarType_co = TypeVar ("_ScalarType_co" , bound = np .generic , covariant = True )
3131
32+ _AnyTiming : TypeAlias = Union [BaseTiming [Any , Any ], Timing , PrecisionTiming ]
33+ _TTiming = TypeVar ("_TTiming" , bound = BaseTiming [Any , Any ])
34+
3235# Use the C types here because np.isdtype() considers some of them to be distinct types, even when
3336# they have the same size (e.g. np.intc vs. np.int_ vs. np.long).
3437_ANALOG_DTYPES = (
5760 np .double ,
5861)
5962
60-
6163# Note about NumPy type hints:
6264# - At time of writing (April 2025), shape typing is still under development, so we do not
6365# distinguish between 1D and 2D arrays in type hints.
@@ -229,7 +231,7 @@ def from_array_2d(
229231 "_sample_count" ,
230232 "_extended_properties" ,
231233 "_timing" ,
232- "_precision_timing " ,
234+ "_converted_timing_cache " ,
233235 "_scale_mode" ,
234236 "__weakref__" ,
235237 ]
@@ -238,8 +240,8 @@ def from_array_2d(
238240 _start_index : int
239241 _sample_count : int
240242 _extended_properties : ExtendedPropertyDictionary
241- _timing : Timing | None
242- _precision_timing : PrecisionTiming | None
243+ _timing : BaseTiming [ Any , Any ]
244+ _converted_timing_cache : dict [ type [ _AnyTiming ], _AnyTiming ]
243245 _scale_mode : ScaleMode
244246
245247 # If neither dtype nor _data is specified, the type parameter defaults to np.float64.
@@ -357,7 +359,7 @@ def _init_with_new_array(
357359 self ._sample_count = sample_count
358360 self ._extended_properties = ExtendedPropertyDictionary ()
359361 self ._timing = Timing .empty
360- self ._precision_timing = None
362+ self ._converted_timing_cache = {}
361363 self ._scale_mode = NO_SCALING
362364
363365 def _init_with_provided_array (
@@ -414,7 +416,7 @@ def _init_with_provided_array(
414416 self ._sample_count = sample_count
415417 self ._extended_properties = ExtendedPropertyDictionary ()
416418 self ._timing = Timing .empty
417- self ._precision_timing = None
419+ self ._converted_timing_cache = {}
418420 self ._scale_mode = NO_SCALING
419421
420422 @property
@@ -579,32 +581,47 @@ def unit_description(self, value: str) -> None:
579581 raise invalid_arg_type ("unit description" , "str" , value )
580582 self ._extended_properties [UNIT_DESCRIPTION ] = value
581583
584+ def _get_timing (self , requested_type : type [_TTiming ]) -> _TTiming :
585+ if isinstance (self ._timing , requested_type ):
586+ return self ._timing
587+ value = cast (_TTiming , self ._converted_timing_cache .get (requested_type ))
588+ if value is None :
589+ value = convert_timing (requested_type , self ._timing )
590+ self ._converted_timing_cache [requested_type ] = value
591+ return value
592+
593+ def _set_timing (self , value : _TTiming ) -> None :
594+ if self ._timing is not value :
595+ self ._timing = value
596+ self ._converted_timing_cache .clear ()
597+
598+ def _validate_timing (self , value : _TTiming ) -> None :
599+ if value ._timestamps is not None and len (value ._timestamps ) != self ._sample_count :
600+ raise ValueError (
601+ "The number of irregular timestamps is not equal to the number of samples in the waveform.\n \n "
602+ f"Number of timestamps: { len (value ._timestamps )} \n "
603+ f"Number of samples in the waveform: { self ._sample_count } "
604+ )
605+
582606 @property
583607 def timing (self ) -> Timing :
584608 """The timing information of the analog waveform.
585609
586610 The default value is Timing.empty.
587611 """
588- if self ._timing is None :
589- if self ._precision_timing is PrecisionTiming .empty :
590- self ._timing = Timing .empty
591- elif self ._precision_timing is not None :
592- self ._timing = convert_timing (Timing , self ._precision_timing )
593- else :
594- raise RuntimeError ("The waveform has no timing information." )
595- return self ._timing
612+ return self ._get_timing (Timing )
596613
597614 @timing .setter
598615 def timing (self , value : Timing ) -> None :
599616 if not isinstance (value , Timing ):
600617 raise invalid_arg_type ("timing information" , "Timing object" , value )
601- self ._timing = value
602- self ._precision_timing = None
618+ self ._validate_timing ( value )
619+ self ._set_timing ( value )
603620
604621 @property
605622 def is_precision_timing_initialized (self ) -> bool :
606- """Indicates whether the waveform's precision timing information is initialized ."""
607- return self ._precision_timing is not None
623+ """Indicates whether the waveform's timing information was set using precision timing ."""
624+ return isinstance ( self ._timing , PrecisionTiming )
608625
609626 @property
610627 def precision_timing (self ) -> PrecisionTiming :
@@ -622,21 +639,14 @@ def precision_timing(self) -> PrecisionTiming:
622639 set using AnalogWaveform.timing. Use AnalogWaveform.is_precision_timing_initialized to
623640 determine if AnalogWaveform.precision_timing has been initialized.
624641 """
625- if self ._precision_timing is None :
626- if self ._timing is Timing .empty :
627- self ._precision_timing = PrecisionTiming .empty
628- elif self ._timing is not None :
629- self ._precision_timing = convert_timing (PrecisionTiming , self ._timing )
630- else :
631- raise RuntimeError ("The waveform has no timing information." )
632- return self ._precision_timing
642+ return self ._get_timing (PrecisionTiming )
633643
634644 @precision_timing .setter
635645 def precision_timing (self , value : PrecisionTiming ) -> None :
636646 if not isinstance (value , PrecisionTiming ):
637647 raise invalid_arg_type ("precision timing information" , "PrecisionTiming object" , value )
638- self ._precision_timing = value
639- self ._timing = None
648+ self ._validate_timing ( value )
649+ self ._set_timing ( value )
640650
641651 @property
642652 def scale_mode (self ) -> ScaleMode :
@@ -648,3 +658,129 @@ def scale_mode(self, value: ScaleMode) -> None:
648658 if not isinstance (value , ScaleMode ):
649659 raise invalid_arg_type ("scale mode" , "ScaleMode object" , value )
650660 self ._scale_mode = value
661+
662+ def append (
663+ self ,
664+ other : (
665+ npt .NDArray [_ScalarType_co ]
666+ | AnalogWaveform [_ScalarType_co ]
667+ | Sequence [AnalogWaveform [_ScalarType_co ]]
668+ ),
669+ / ,
670+ timestamps : Sequence [dt .datetime ] | Sequence [ht .datetime ] | None = None ,
671+ ) -> None :
672+ """Append data to the analog waveform.
673+
674+ Args:
675+ other: The array or waveform(s) to append.
676+ timestamps: A sequence of timestamps. When the current waveform has
677+ SampleIntervalMode.IRREGULAR, you must provide a sequence of timestamps with the
678+ same length as the array.
679+
680+ Raises:
681+ TimingMismatchError: The current and other waveforms have incompatible timing.
682+ ValueError: The other array has the wrong number of dimensions or the length of the
683+ timestamps argument does not match the length of the other array.
684+ TypeError: The data types of the current waveform and other array or waveform(s) do not
685+ match, or an argument has the wrong data type.
686+
687+ Warnings:
688+ TimingMismatchWarning: The sample intervals of the waveform(s) do not match.
689+ ScalingMismatchWarning: The scale modes of the waveform(s) do not match.
690+
691+ When appending waveforms:
692+
693+ * Timing information is merged based on the sample interval mode of the current
694+ waveform:
695+
696+ * SampleIntervalMode.NONE or SampleIntervalMode.REGULAR: The other waveform(s) must also
697+ have SampleIntervalMode.NONE or SampleIntervalMode.REGULAR. If the sample interval does
698+ not match, a TimingMismatchWarning is generated. Otherwise, the timing information of
699+ the other waveform(s) is discarded.
700+
701+ * SampleIntervalMode.IRREGULAR: The other waveforms(s) must also have
702+ SampleIntervalMode.IRREGULAR. The timestamps of the other waveforms(s) are appended to
703+ the current waveform's timing information.
704+
705+ * Extended properties of the other waveform(s) are merged into the current waveform if they
706+ are not already set in the current waveform.
707+
708+ * If the scale mode of other waveform(s) does not match the scale mode of the current
709+ waveform, a ScalingMismatchWarning is generated. Otherwise, the scaling information of the
710+ other waveform(s) is discarded.
711+ """
712+ if isinstance (other , np .ndarray ):
713+ self ._append_array (other , timestamps )
714+ elif isinstance (other , AnalogWaveform ):
715+ validate_unsupported_arg ("timestamps" , timestamps )
716+ self ._append_waveform (other )
717+ elif isinstance (other , Sequence ) and all (isinstance (x , AnalogWaveform ) for x in other ):
718+ validate_unsupported_arg ("timestamps" , timestamps )
719+ self ._append_waveforms (other )
720+ else :
721+ raise invalid_arg_type ("input" , "array or waveform(s)" , other )
722+
723+ def _append_array (
724+ self ,
725+ array : npt .NDArray [_ScalarType_co ],
726+ timestamps : Sequence [dt .datetime ] | Sequence [ht .datetime ] | None = None ,
727+ ) -> None :
728+ if array .dtype != self .dtype :
729+ raise TypeError (
730+ "The data type of the input array must match the waveform data type.\n \n "
731+ f"Input array data type: { array .dtype } \n "
732+ f"Waveform data type: { self .dtype } "
733+ )
734+ if array .ndim != 1 :
735+ raise ValueError (
736+ "The input array must be a one-dimensional array.\n \n "
737+ f"Number of dimensions: { array .ndim } "
738+ )
739+ if timestamps is not None and len (array ) != len (timestamps ):
740+ raise ValueError (
741+ "The number of irregular timestamps must be equal to the input array length.\n \n "
742+ f"Number of timestamps: { len (timestamps )} \n "
743+ f"Array length: { len (array )} "
744+ )
745+
746+ new_timing = self ._timing ._append_timestamps (timestamps )
747+
748+ self ._increase_capacity (len (array ))
749+ self ._set_timing (new_timing )
750+
751+ offset = self ._start_index + self ._sample_count
752+ self ._data [offset : offset + len (array )] = array
753+ self ._sample_count += len (array )
754+
755+ def _append_waveform (self , waveform : AnalogWaveform [_ScalarType_co ]) -> None :
756+ self ._append_waveforms ([waveform ])
757+
758+ def _append_waveforms (self , waveforms : Sequence [AnalogWaveform [_ScalarType_co ]]) -> None :
759+ for waveform in waveforms :
760+ if waveform .dtype != self .dtype :
761+ raise TypeError (
762+ "The data type of the input waveform must match the waveform data type.\n \n "
763+ f"Input waveform data type: { waveform .dtype } \n "
764+ f"Waveform data type: { self .dtype } "
765+ )
766+ if waveform ._scale_mode != self ._scale_mode :
767+ warnings .warn (scale_mode_mismatch ())
768+
769+ new_timing = self ._timing
770+ for waveform in waveforms :
771+ new_timing = new_timing ._append_timing (waveform ._timing )
772+
773+ self ._increase_capacity (sum (waveform .sample_count for waveform in waveforms ))
774+ self ._set_timing (new_timing )
775+
776+ offset = self ._start_index + self ._sample_count
777+ for waveform in waveforms :
778+ self ._data [offset : offset + waveform .sample_count ] = waveform .raw_data
779+ offset += waveform .sample_count
780+ self ._sample_count += waveform .sample_count
781+ self ._extended_properties ._merge (waveform ._extended_properties )
782+
783+ def _increase_capacity (self , amount : int ) -> None :
784+ new_capacity = self ._start_index + self ._sample_count + amount
785+ if new_capacity > self .capacity :
786+ self .capacity = new_capacity
0 commit comments