Coverage for src / tracekit / exporters / spice_export.py: 56%

45 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 23:04 +0000

1"""SPICE PWL export functionality for TraceKit. 

2 

3This module provides export to SPICE Piece-Wise Linear (PWL) format for 

4use in circuit simulation tools like LTspice, ngspice, Cadence, etc. 

5 

6 

7Example: 

8 >>> from tracekit.exporters.spice_export import export_pwl 

9 >>> export_pwl(trace, "stimulus.pwl") 

10 >>> # Use in SPICE: V1 in 0 PWL file=stimulus.pwl 

11""" 

12 

13from __future__ import annotations 

14 

15from pathlib import Path 

16from typing import TYPE_CHECKING, Any 

17 

18import numpy as np 

19 

20from tracekit.core.types import DigitalTrace, WaveformTrace 

21 

22if TYPE_CHECKING: 

23 from numpy.typing import NDArray 

24 

25 

26def export_pwl( 

27 data: WaveformTrace | DigitalTrace | NDArray[Any] | tuple[NDArray[Any], NDArray[Any]], 

28 path: str | Path, 

29 *, 

30 time_scale: float = 1.0, 

31 amplitude_scale: float = 1.0, 

32 time_offset: float = 0.0, 

33 amplitude_offset: float = 0.0, 

34 precision: int = 12, 

35 comment: str | None = None, 

36 downsample: int = 1, 

37 format_style: str = "ltspice", 

38) -> None: 

39 """Export data to SPICE PWL (Piece-Wise Linear) format. 

40 

41 Creates a PWL file that can be used as a stimulus source in SPICE 

42 circuit simulators. The format consists of time-value pairs. 

43 

44 Args: 

45 data: Data to export. Can be: 

46 - WaveformTrace or DigitalTrace 

47 - NumPy array (uses trace.time_vector or generates index-based time) 

48 - Tuple of (time_array, value_array) 

49 path: Output file path. 

50 time_scale: Scaling factor for time values (e.g., 1e-9 for ns). 

51 amplitude_scale: Scaling factor for amplitude values. 

52 time_offset: Offset to add to all time values. 

53 amplitude_offset: Offset to add to all amplitude values. 

54 precision: Decimal precision for output values. 

55 comment: Optional comment to include at top of file. 

56 downsample: Downsample factor to reduce file size (1 = no downsampling). 

57 format_style: Output format style: 

58 - "ltspice": LTspice compatible (time value pairs) 

59 - "ngspice": ngspice compatible (same as ltspice) 

60 - "hspice": HSPICE compatible (with header) 

61 

62 Raises: 

63 TypeError: If data type is not supported. 

64 

65 Example: 

66 >>> # Export as stimulus for simulation 

67 >>> export_pwl(trace, "input.pwl") 

68 >>> # In LTspice: V1 in 0 PWL file=input.pwl 

69 

70 >>> # Scale time to nanoseconds for display 

71 >>> export_pwl(trace, "input.pwl", time_scale=1e9) 

72 

73 References: 

74 EXP-005 

75 """ 

76 path = Path(path) 

77 

78 # Extract time and value arrays 

79 if isinstance(data, WaveformTrace | DigitalTrace): 

80 time = data.time_vector 

81 values = data.data 

82 elif isinstance(data, tuple) and len(data) == 2: 82 ↛ 84line 82 didn't jump to line 84 because the condition on line 82 was always true

83 time, values = data 

84 elif isinstance(data, np.ndarray): 

85 # Generate time based on array index (assume 1 unit per sample) 

86 time = np.arange(len(data), dtype=np.float64) 

87 values = data 

88 else: 

89 raise TypeError(f"Unsupported data type: {type(data)}") 

90 

91 # Apply downsampling 

92 if downsample > 1: 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true

93 time = time[::downsample] 

94 values = values[::downsample] 

95 

96 # Apply scaling and offset 

97 time = time * time_scale + time_offset 

98 values = values * amplitude_scale + amplitude_offset 

99 

100 # Write to file 

101 with open(path, "w") as f: 

102 # Write comment/header 

103 if format_style == "hspice": 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true

104 f.write("* HSPICE PWL Data\n") 

105 if comment: 

106 f.write(f"* {comment}\n") 

107 f.write(f"* Points: {len(time)}\n") 

108 f.write("*\n") 

109 elif comment: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true

110 f.write(f"; {comment}\n") 

111 

112 # Write time-value pairs 

113 fmt = f"{{:.{precision}g}} {{:.{precision}g}}\n" 

114 f.writelines(fmt.format(t, v) for t, v in zip(time, values, strict=False)) 

115 

116 

117def export_pwl_multi( 

118 traces: dict[str, WaveformTrace | DigitalTrace | NDArray[Any]], 

119 path: str | Path, 

120 *, 

121 time_scale: float = 1.0, 

122 amplitude_scale: float = 1.0, 

123 precision: int = 12, 

124 downsample: int = 1, 

125) -> None: 

126 """Export multiple traces to individual PWL files. 

127 

128 Creates separate PWL files for each trace in the dictionary, 

129 with filenames based on the dictionary keys. 

130 

131 Args: 

132 traces: Dictionary mapping signal names to trace data. 

133 path: Output directory path. 

134 time_scale: Scaling factor for time values. 

135 amplitude_scale: Scaling factor for amplitude values. 

136 precision: Decimal precision for output values. 

137 downsample: Downsample factor to reduce file size. 

138 

139 Example: 

140 >>> traces = { 

141 ... "clk": clock_trace, 

142 ... "data": data_trace, 

143 ... "reset": reset_trace, 

144 ... } 

145 >>> export_pwl_multi(traces, "stimuli/") 

146 >>> # Creates: stimuli/clk.pwl, stimuli/data.pwl, stimuli/reset.pwl 

147 

148 References: 

149 EXP-005 

150 """ 

151 path = Path(path) 

152 path.mkdir(parents=True, exist_ok=True) 

153 

154 for name, data in traces.items(): 

155 # Sanitize filename 

156 safe_name = "".join(c if c.isalnum() or c in "_-" else "_" for c in name) 

157 file_path = path / f"{safe_name}.pwl" 

158 

159 export_pwl( 

160 data, 

161 file_path, 

162 time_scale=time_scale, 

163 amplitude_scale=amplitude_scale, 

164 precision=precision, 

165 downsample=downsample, 

166 comment=f"Signal: {name}", 

167 ) 

168 

169 

170def generate_spice_source( 

171 pwl_path: str | Path, 

172 node_positive: str = "in", 

173 node_negative: str = "0", 

174 source_name: str = "V1", 

175 source_type: str = "voltage", 

176) -> str: 

177 """Generate SPICE source definition for a PWL file. 

178 

179 Args: 

180 pwl_path: Path to PWL file. 

181 node_positive: Positive node name. 

182 node_negative: Negative node name (usually "0" for ground). 

183 source_name: Source instance name. 

184 source_type: Source type ("voltage" or "current"). 

185 

186 Returns: 

187 SPICE source definition string. 

188 

189 Example: 

190 >>> line = generate_spice_source("input.pwl", "in", "0", "V1") 

191 >>> print(line) 

192 V1 in 0 PWL file=input.pwl 

193 

194 References: 

195 EXP-005 

196 """ 

197 prefix = "V" if source_type == "voltage" else "I" 

198 

199 # Ensure source name starts with correct prefix 

200 if not source_name.upper().startswith(prefix): 

201 source_name = f"{prefix}{source_name}" 

202 

203 return f"{source_name} {node_positive} {node_negative} PWL file={pwl_path}" 

204 

205 

206__all__ = [ 

207 "export_pwl", 

208 "export_pwl_multi", 

209 "generate_spice_source", 

210]