はじめに
本記事は Claude Code(⼀部 Codex)を使って SPICE シミュレータを6日間で開発してみたの続編です。前回は AI 実装で C++20 製の SPICE 互換シミュレータ「sukimaspice」を構築した経緯と、数値解法・検証方法を中心に紹介しました。続編となる今回は、sukimaspice の波形出力を支える独自フォーマット SSF (Signal Storage Format) V4.3 の仕様を、わかりやすく解説します。
目的:SPICE 系の解析(TRAN / DC / AC / NOISE / OP / Monte Carlo)の結果を、軽く・速く・正しく・拡張可能に保存するための実用仕様
先に要点
-
sukimasim ロジック波形出力フォーマットの SSF V4 に、SPICE/アナログ対応を追加:
- 軸モデルの一般化(Axis0 = time/freq/param/mc …)
- 実数は「量子化整数(R0)推奨」または「IEEE754 ビットキャスト(R1)」でロスレス保存
- 複素数は「ペア+グループ ID」で表現(real/imag または mag/phase)
- 解析マッピング標準化:TRAN/DC/AC/NOISE/OP/MonteCarlo それぞれで推奨メタデータセットを定義
-
ストリーミング圧縮に対応:
flags.bit6=1のとき、フレーム形式(4B ヘッダー + payload)を使用。compression_type=0(非圧縮)でもフレームヘッダーは必須。 - 拡張性重視:将来的な拡張はメタデータで吸収。フラグによる機能追加も可能
なぜ SSF を作ったのか(背景と設計方針)
既存の選択肢(CSV / TSV / 独自バイナリ / raw 互換)は「サイズ・速度・表現力・互換性」のいずれかで妥協が生じがちです。特に:
- TRAN と AC で軸の意味が異なる(時間 vs 周波数)
- AC は複素数(Real/Imag または Mag/Phase)を自然に扱いたい
- 長時間シミュレーションで「追記(ストリーミング)やランダムアクセス」を効かせたい
- 単位・次元・ノード/デバイス紐付けをメタデータで統一したい
起源:sukimasim からの拡張
SSF はもともと、デジタル論理シミュレータ「sukimasim」プロジェクトの波形を高速・軽量に保存するために設計したフォーマットです。ロジック系の要件(高速追記、部分読み出し、階層スコープ/シグナル管理、エイリアス、軽量メタデータ)を満たす骨格を整備した後、アナログ/SPICE ユースケースに対応するため、以下の機能を追加しました:
- Axis モデルの一般化:時間軸だけでなく、周波数軸、パラメータ掃引軸、Monte Carlo 試行番号など
- 実数・複素数の表現:量子化整数(R0)または IEEE754 ビットキャスト(R1)、複素数のペア表現
- SPICE メタデータ標準化:解析タイプ、物理量、単位、ノード/デバイス情報など
これにより、同一フォーマットでロジック波形と SPICE 波形(TRAN/DC/AC/NOISE/OP/Monte Carlo)をロスレスに扱えます。
設計指針
SSF V4.3 は以下の方針で設計されています:
- バイナリ構造の統一:256B ヘッダー、ディレクトリ、L1/L2/L3、データブロック
- メタデータ駆動:解析に依存する意味づけはメタデータで与え、波形は一貫した構造で保持
- スケーラビリティ:ストリーミング I/O とブロック独立性を優先(大規模波形でもスケール)
- 実装容易性:最小限の規範と、現実的な参考ガイド(非規範値)を併記
仕様の全体像
SSF ファイルは次の順で構成されます(固定順序)。
- ヘッダー(256B, offset=0)
- メタデータ(varint エントリ数 + 各エントリ: varint キー長 + UTF-8 キー + varint 値長 + UTF-8 値)
-
スコープディレクトリ(varint 数 + 各エントリ)
- 階層構造を表現(例:トップレベル回路 → サブサーキット)
- 各エントリ:
scope_id,parent_id,name等
-
シグナルディレクトリ(varint 数 + 各エントリ)
- 各信号の属性(名前、型、幅、スコープ所属など)
- 各エントリ:
signal_id,scope_id,name,type,width, フラグ等
-
階層的インデックス(任意: L3 → L2 → L1)
- L1 は必須(ブロックがある場合)
- L2/L3 は大規模データの高速検索用(任意)
-
データブロック(任意)
- スナップショット + 変化列のエンコード
- ストリーミング時は各ブロックがフレーム形式
重要な互換性:セクションは互いに重複せず、ヘッダーの *_offset/size と一致していなければなりません。
V4.3 の主要機能
ストリーミング圧縮(flags.bit6=1)
-
フレーム形式:
[uncompressed_size: uint32 LE] + payload -
compression_type=0(非圧縮)でもフレームヘッダーは必須 - L1 の
sizeは「フレーム全長(4 + payload 長)」を記録 - 利点:読み出し経路の一本化、整合性チェック、将来の圧縮方式追加が容易
バージョン管理
- 常に
version_major=3, version_minor=4(V4 世代の互換レベル) - ライターは 3.4 を出力、リーダーはそれ以外を警告/エラー
- 将来の破壊的変更は V5 以降で実施
SPICE/アナログ拡張(flags.bit7=1)
-
Axis モデルの一般化:
- Axis0 を主軸として扱う(
time/freq/param/mc/indexなど) - メタデータで
axis.0.typeとaxis.0.unitを指定
- Axis0 を主軸として扱う(
-
実数表現:
- R0(量子化整数)推奨:差分エンコーディングで高圧縮率
- R1(IEEE754 ビットキャスト)互換:ビット完全互換が必要な場合
-
複素数表現:
- 2 本の信号を
complex.groupで束ねる -
complex.role={real,imag}または{mag,phase}を付与
- 2 本の信号を
-
解析マッピング:
- TRAN/DC/AC/NOISE/OP/MonteCarlo それぞれで推奨メタデータセットを定義
- 解析タイプ、物理量、単位、ノード/デバイス情報など
ヘッダー要点(256B, 固定)
-
ファイル拡張子:
.ssf(推奨) - バイトオーダー: Little Endian(全ての整数フィールド)
- 識別:
magic='SSF\x04',version=3.4(厳密運用) - 解析軸:
start_time / end_time / timescale_{num,den}は Axis0(主軸)の整数座標スケール- 例:TRAN で ps 単位なら
timescale_num=1,timescale_den=1_000_000_000_000
- 例:TRAN で ps 単位なら
- 圧縮:
compression_type = 0(NONE)/1(LZ4)/2(ZLIB)/3(ZSTD) - フラグ:
-
0x01 DELTA_VARINT: 差分 varint エンコーディング(変化列) -
0x02 POSITION_TABLE: 位置テーブル(デジタル波形用) -
0x04 BLOCK_INDEPENDENCE: ブロック独立性(並列デコード可能) -
0x08 ADAPTIVE_ALIASING: 適応エイリアス(動的エイリアス解決) -
0x10 ADAPTIVE_BLOCKS: 適応ブロックサイズ -
0x20 HIERARCHICAL_INDEX: 階層インデックス(L3/L2/L1) -
0x40 STREAMING_COMPRESSION: ストリーミング圧縮(フレーム形式) -
0x80 ANALOG_EXTENSIONS: アナログ拡張(SPICE メタデータ)
-
- 推奨:ストリーミング時は フレーム必須(非圧縮でも 4B ヘッダー)。
Axis モデル(時間軸だけではない)
Axis0 は「主軸」です。TRAN なら time[s]、AC なら freq[Hz]、DC なら param[unit]、Monte Carlo なら mc[idx] といった具合に、解析ごとに意味が変わります。
主要な特徴:
- メタデータで
axis.0.typeとaxis.0.unitを必須化(flags.bit7=1のとき) - Axis1 以降もメタデータで説明可能(例:Monte Carlo × 時間の 2 軸データ)
- Header の
timescale_*は Axis0 の整数座標 ⇄ 物理量の比率を表す(TRAN なら ps, ns など)
解析タイプ別の Axis0:
| 解析 | axis.0.type |
axis.0.unit |
説明 |
|---|---|---|---|
| TRAN | time |
s |
時間軸 |
| AC | freq |
Hz |
周波数軸 |
| DC | param |
V, A など |
パラメータ掃引軸 |
| NOISE | freq |
Hz |
周波数軸 |
| OP | index |
idx |
単一点(start=end=0) |
| Monte Carlo | mc |
idx |
試行番号 |
メタデータ設計(キーセットの標準化)
メタデータは UTF-8 キー/値ペアの配列として格納されます(varint エントリ数 + 各エントリ: varint キー長 + UTF-8 キー + varint 値長 + UTF-8 値)。
代表キー(SPICE/アナログ拡張、flags.bit7=1)
ファイルレベル:
-
file.analysis: 解析タイプ("tran","ac","dc","noise","op","montecarlo") -
file.generator: 生成ツール名(例:"sukimaspice 0.6.0") -
file.title: タイトル(ネットリスト最初の行など)
軸(Axis):
-
axis.k.type: 軸の種類("time","freq","param","mc","index") -
axis.k.unit: 物理単位("s","Hz","V","idx") -
axis.k.name: 軸の名前(例: DC sweep なら"V1.dc") -
axis.k.scale.num/axis.k.scale.den: 量子化スケール(オプション)
信号(Signal):
-
signal.<id>.quantity: 物理量("voltage","current","power","noise_density") -
signal.<id>.unit: 単位("V","A","W","V^2/Hz","A^2/Hz") -
signal.<id>.kind: 種別("node","device","branch","internal") -
signal.<id>.node: ノード名(kind=node の場合) -
signal.<id>.device: デバイス名(kind=device の場合) -
signal.<id>.scale.num/signal.<id>.scale.den: R0 量子化スケール(必須、R0 使用時) -
signal.<id>.complex.group: 複素数グループ ID(文字列) -
signal.<id>.complex.role: 複素数の役割("real","imag","mag","phase")
不変条件(Invariants)
リーダー/ライターは以下を検証すべきです:
-
flags.bit7=1の場合、必須キー:-
file.analysis,axis.0.type,axis.0.unit - R0 使用時は
signal.<id>.scale.num/den
-
-
複素数の整合性:
- 同一
<id>でcomplex.roleが重複しない - 同一
complex.group内で role が 2 つ(real+imag または mag+phase) - 同一
complex.group内で{real,imag}と{mag,phase}が混在しない
- 同一
-
軸の単調性:
- L1 インデックスの
timestampは非減少(ブロック開始位置が逆行しない)
- L1 インデックスの
違反時は InvalidMetadataError または CorruptDataError を推奨。
実数・複素数の表現(R0/R1 と Complex Group)
実数(REAL)
-
R0: 量子化整数(推奨)
- 実数
xをX = round(x * num/den)に量子化し、スナップショットはvarint(X)、変化列はsigned_varint(ΔX)で保存。 - メタデータに
signal.<id>.scale.num/denを必ず出力(例:電圧なら 1µV 刻み →num=1_000_000,den=1)。
- 実数
-
R1: IEEE754 ビットキャスト(互換)
-
float64をuint64にビット再解釈して保存(スナップショットvarint(bits)、変化列signed_varint(Δbits))。 - 圧縮効率はデータ依存。Zstd 等のフレーム圧縮と併用が現実的。
-
複素数(AC 等)
複素数は2本の実数信号をペアにして表現します。
グループ化:
-
signal.<id>.complex.groupに同一のグループ ID(文字列、例:"g1","v_out_ac")を設定 - グループ ID は任意の文字列(推奨: 信号名ベース、または連番
"g1","g2", ...) - 同一グループに属する信号は必ず 2 本(real+imag または mag+phase)
Role 指定:
-
signal.<id>.complex.roleに以下のいずれかを設定:-
"real"+"imag": 直交座標(推奨、数値安定) -
"mag"+"phase": 極座標(phase の単位は"deg"推奨、"rad"可)
-
制約:
- 同一グループ内で role の重複禁止(real が 2 つある、など)
- 同一グループ内で
{real,imag}と{mag,phase}の混在禁止 - ビューア側での相互変換は可能(real/imag ⇔ mag/phase)
例(AC 解析の電圧):
# ノード "vout" の AC 解析結果(real/imag 表現)
signal.5.name=vout.real
signal.5.quantity=voltage
signal.5.unit=V
signal.5.complex.group=vout_ac
signal.5.complex.role=real
signal.5.scale.num=1000000
signal.5.scale.den=1
signal.6.name=vout.imag
signal.6.quantity=voltage
signal.6.unit=V
signal.6.complex.group=vout_ac # 同じグループ ID
signal.6.complex.role=imag
signal.6.scale.num=1000000
signal.6.scale.den=1
phase の量子化:
-
unit=degの場合:scale.num=1000000, scale.den=1で µdeg 精度(推奨) -
unit=radの場合: 適切なスケールで量子化(例:1e-9 rad精度)
解析マッピング(TRAN / DC / AC / NOISE / OP / Monte Carlo)
- TRAN:
file.analysis=tran,axis.0.type=time,axis.0.unit=s - AC:
file.analysis=ac,axis.0.type=freq,axis.0.unit=Hz, 複素ペア(real/imag もしくは mag/phase) - DC:
file.analysis=dc,axis.0.type=param,axis.0.unit=<掃引量の単位>,axis.0.name=<例: V1.dc> - NOISE:
file.analysis=noise,axis.0.type=freq,axis.0.unit=Hz(一般的) - OP:
file.analysis=op, 単一点は Axis0 をindexとしstart=end=0 - Monte Carlo:
file.analysis=montecarlo,axis.0.type=mc,axis.0.unit=idx(試行番号)
例(TRAN 電圧波形のメタデータ抜粋):
file.analysis=tran
axis.0.type=time
axis.0.unit=s
file.generator=sukimaspice 0.6.x
signal.1.quantity=voltage
signal.1.unit=V
signal.1.kind=node
signal.1.node=out
signal.1.scale.num=1000000 # 1 µV increments
signal.1.scale.den=1
ブロック/インデックス/ストリーミング(大規模データのために)
L3/L2/L1 の階層インデックス
- ブロック独立性を維持しつつ、範囲検索は L3→L2→L1 の順でスキャン。
- L1 は必須(ブロックがある場合)。
[timestamp: u64][data_offset: u64][size: u64][block_id: u32] - L1 の
timestampは Axis0 座標のブロック開始位置。
ストリーミングフレーム(規範)
flags.bit6=1 のとき:
frame := [uncompressed_size: uint32 LE] [payload]
uncompressed_size := len(block_bytes) # 圧縮前の元データサイズ
payload :=
compression_type=0 → block_bytes(非圧縮、len(payload) == uncompressed_size)
compression_type>0 → compress(block_bytes, type)(圧縮済み、len(payload) < uncompressed_size)
L1.size := len(frame) # 4 + len(payload)
重要: uncompressed_size は圧縮前の元データサイズを記録します。これにより:
- リーダーは展開バッファを事前に確保可能
-
compression_type=0でも 4B ヘッダーがあり、統一された読み出しパスで処理できる - 圧縮データの整合性チェックが可能(展開後のサイズが一致するか検証)
最小実装フロー(擬似コード)
ライタ(TRAN, R0 の例)
write_header(version=3.4, flags=STREAMING|ANALOG_EXTENSIONS, ...)
write_metadata({
'file.analysis':'tran', 'axis.0.type':'time', 'axis.0.unit':'s',
'signal.1.quantity':'voltage', 'signal.1.unit':'V',
'signal.1.scale.num':'1000000', 'signal.1.scale.den':'1',
})
write_scope_dir(...); write_signal_dir(...)
for each block:
block_bytes = encode_block(start_coord, snapshots_R0, changes_R0)
if flags & STREAMING:
payload = block_bytes if compression_type==0 else compress(block_bytes)
frame = u32_le(len(block_bytes)) + payload
write(frame); l1.size = len(frame)
else:
write(block_bytes); l1.size = len(block_bytes)
append_l1_index(timestamp=start_coord, data_offset=..., size=l1.size)
リーダ(共通)
f.seek(l1.data_offset)
frame_or_block = f.read(l1.size)
if flags & STREAMING:
uncompressed = u32_le(frame_or_block[:4])
payload = frame_or_block[4:]
block = payload if compression_type==0 else decompress(payload, compression_type, uncompressed)
else:
block = frame_or_block
decode_block(block) # スナップショット/変化列。R0 なら quantize/Δ を復元
互換性とエラー条件(実装時の落とし穴)
リーダー/ライター実装時に検証すべきエラー条件:
| 条件 | エラー型 | 説明 |
|---|---|---|
magic != 'SSF\x04' |
InvalidMagicError |
SSF ファイルではない |
version != 3.4 |
UnsupportedVersionError |
厳密運用を推奨(警告または拒否) |
| セクション重複 | CorruptHeaderError |
offset/size の整合性チェック |
| STREAMING 時の size 不一致 | CorruptDataError |
L1.size と実読込長の不一致 |
| Axis0 非単調 | CorruptDataError |
ブロック開始位置が逆行 |
| 必須メタデータ不足 | InvalidMetadataError |
flags.bit7=1 時の file.analysis など |
| 複素数グループ不整合 | InvalidMetadataError |
role 重複、real/imag と mag/phase 混在 |
推奨動作:
-
version != 3.4: 警告を出して読み込みを試みる(将来の V5 を考慮)、または拒否 - メタデータ不足: エラーとするか、デフォルト値で補完(ログに記録)
- 複素数不整合: 読み込み時にグループ単位で検証、エラーまたは警告
圧縮タイプの目安
重要: 以下の圧縮率は参考値であり、仕様の一部ではありません。実際の圧縮率はデータ特性(信号の複雑さ、サンプリングレート、ノイズレベルなど)に強く依存します。
| 圧縮方式 | 目安圧縮率 | 特徴 |
|---|---|---|
| LZ4 (type=1) | 1.5–2.5× | 高速、低CPU負荷、ストリーミング向き |
| Zlib (type=2) | 2–5× | 標準的、広く対応 |
| Zstd (type=3) | 2–8× | 推奨(level=3)、バランス良好 |
実測データ(sukimaspice RC lowpass, 10,000 points):
- 非圧縮(R0 量子化のみ): 100%
- R0 + LZ4: 42% (2.4×)
- R0 + Zstd (level=3): 23% (4.3×)
- R0 + Zstd (level=19): 18% (5.6×)
推奨戦略:
- ストリーミング/リアルタイム: LZ4(低レイテンシ)
- アーカイブ/長期保存: Zstd level=3(バランス)
- 極限圧縮: Zstd level=19(CPU コスト高)
R0(量子化)との併用でさらに効果が高まります。R1(IEEE754 ビットキャスト)の場合は Zstd 推奨。
Q&A
Q. AC は Real/Imag と Mag/Phase のどちらで保存すべき?
A. どちらも可ですが、同一 complex.group 内で混在は禁止。ビューア側で相互変換は可能です。運用上は Real/Imag を推奨します(数値安定性・変化追跡が明確、量子化の精度維持が容易)。
Q. R0 と R1 の使い分けは?
A. 既定は R0(量子化)推奨。単位あたりの分解能(µV/µA など)を決め、scale.num/den を必ず記録します。利点:
- 差分エンコーディングで高圧縮率
- 物理的な意味が明確
- 変化追跡が容易
R1(IEEE754 ビットキャスト)は以下の場合のみ:
- ビット完全互換性が必要(既存ツールとの連携)
- 実装簡便さを優先
- 圧縮効率は Zstd 前提で要検討
Q. 非圧縮でもフレーム必須なのはなぜ?
A. 「読み出し経路を 1 本化」するためです。compression_type=0 でも 4B ヘッダーがあれば、伸長ブランチを条件分岐なしに処理できます。これにより:
- コードがシンプルになる
- 将来的な圧縮方式の追加が容易
-
uncompressed_sizeで整合性チェックが可能
Q. L2/L3 インデックスはいつ使うべき?
A. データサイズが大きい場合(目安: ブロック数 > 1000、ファイルサイズ > 100MB)に有効です。L2 は 64 ブロック単位、L3 は 4096 ブロック単位でジャンプ可能にし、範囲検索を高速化します。小規模データでは L1 のみで十分です。
Q. NOISE 解析の単位 V^2/Hz はどう扱う?
A. ノイズ密度は quantity=noise_density, unit=V^2/Hz または A^2/Hz で表現します。総ノイズ(積分値)の場合は quantity=voltage または current, unit=V または A で通常の信号として扱います。
Q. Monte Carlo で時間軸もある場合は?
A. axis.0.type=mc (試行番号)、axis.1.type=time として 2 軸データを表現します。ブロックは MC 試行ごとに分割し、各ブロック内で時間変化をエンコードします。メタデータで両軸の説明を明記してください。
クイックリファレンス(実装時に見る表)
必須メタデータキー(flags.bit7=1 の場合)
| カテゴリ | キー | 必須 | 例 |
|---|---|---|---|
| ファイル | file.analysis |
✅ |
"tran", "ac", "dc", "noise", "op"
|
file.generator |
推奨 | "sukimaspice 0.6.0" |
|
file.title |
任意 | "RC Lowpass Filter" |
|
| Axis0 | axis.0.type |
✅ |
"time", "freq", "param", "mc"
|
axis.0.unit |
✅ |
"s", "Hz", "V", "idx"
|
|
axis.0.name |
推奨 |
"V1.dc" (DC sweep) |
|
axis.0.scale.num/den |
任意 | R0 使用時のみ | |
| 信号 | signal.<id>.quantity |
推奨 |
"voltage", "current", "noise_density"
|
signal.<id>.unit |
推奨 |
"V", "A", "V^2/Hz"
|
|
signal.<id>.kind |
任意 |
"node", "device"
|
|
signal.<id>.node |
kind=node時 | "vout" |
|
signal.<id>.device |
kind=device時 | "R1" |
|
signal.<id>.scale.num/den |
R0時必須 |
1000000 / 1 (1µV) |
|
signal.<id>.complex.group |
複素数時 | "vout_ac" |
|
signal.<id>.complex.role |
複素数時 |
"real", "imag", "mag", "phase"
|
ヘッダー固定値
| フィールド | 値 | 備考 |
|---|---|---|
magic |
'SSF\x04' |
固定(4バイト) |
version_major |
3 |
固定 |
version_minor |
4 |
固定(V4.3 = version 3.4) |
compression_type |
0/1/2/3
|
NONE/LZ4/Zlib/Zstd |
| バイトオーダー | Little Endian | 全整数フィールド |
おわりに
本記事では SSF V4.3 の設計思想と実装要点を、V4 系の互換性を保ちつつ SPICE 拡張を自然に取り込むという観点で解説しました。次回は、SSF を読み込んで高速に描画・比較・検算するビューア/ツールチェーン設計(インデックスの活用や部分読み出し、複素数可視化の UX)を掘り下げる予定です。
Appendix:varint / ZigZag エンコーディング
varint(符号なし整数)
エンコード規則:
- 各バイトの下位 7 ビットがデータ、最上位ビット (MSB) が継続フラグ
- MSB = 1: 次のバイトが続く
- MSB = 0: 最終バイト(これで終了)
- Little Endian バイト順(下位バイトから先に出力)
def encode_varint(value: int) -> bytes:
"""Encode unsigned integer as varint (Little Endian)"""
if value < 0:
raise ValueError("varint requires non-negative integer")
out = bytearray()
while True:
b = value & 0x7F # 下位 7 ビット取得
value >>= 7 # 7 ビット右シフト
if value != 0:
out.append(b | 0x80) # 継続フラグ ON(MSB=1)
else:
out.append(b) # 最終バイト(MSB=0)
break
return bytes(out)
def decode_varint(data: bytes, offset: int = 0) -> tuple[int, int]:
"""Decode varint and return (value, bytes_consumed)"""
result = 0
shift = 0
i = offset
while i < len(data):
b = data[i]
result |= (b & 0x7F) << shift
i += 1
if (b & 0x80) == 0: # MSB=0 なら終了
break
shift += 7
return result, i - offset
signed_varint(符号付き整数: ZigZag エンコーディング)
ZigZag マッピング: 符号付き整数を符号なし整数に変換
- 0 → 0, -1 → 1, 1 → 2, -2 → 3, 2 → 4, ...
- 小さな絶対値の整数(正負問わず)が小さな varint になる
標準的な ZigZag 実装:
def encode_signed_varint(value: int) -> bytes:
"""ZigZag encode signed integer, then varint encode"""
# ZigZag mapping: n -> 2*n (if n>=0), -n-1 -> 2*(-n-1)+1 (if n<0)
# Equivalent to: (n << 1) ^ (n >> 31) for 32-bit
# (n << 1) ^ (n >> 63) for 64-bit
# Python safe version (works for arbitrary precision int):
zz = (abs(value) << 1) if value >= 0 else (((-value) << 1) - 1)
return encode_varint(zz)
def decode_signed_varint(data: bytes, offset: int = 0) -> tuple[int, int]:
"""Decode signed varint (ZigZag + varint)"""
zz, consumed = decode_varint(data, offset)
# Reverse ZigZag: 0->0, 1->-1, 2->1, 3->-2, 4->2, ...
value = (zz >> 1) if (zz & 1) == 0 else -((zz + 1) >> 1)
return value, consumed
64-bit 固定長での ZigZag(C++/固定幅整数用):
def zigzag_encode_i64(n: int) -> int:
"""ZigZag encode for signed 64-bit integer"""
# Assumes n is in range [-2^63, 2^63-1]
return (n << 1) ^ (n >> 63)
def zigzag_decode_i64(zz: int) -> int:
"""ZigZag decode for signed 64-bit integer"""
return (zz >> 1) ^ (-(zz & 1))
使用例:
# 符号なし
assert encode_varint(0) == b'\x00'
assert encode_varint(127) == b'\x7f'
assert encode_varint(128) == b'\x80\x01' # 0x80 (継続) + 0x01
assert encode_varint(300) == b'\xac\x02' # 0xac (継続) + 0x02
# 符号付き (ZigZag)
assert encode_signed_varint(0) == b'\x00'
assert encode_signed_varint(-1) == b'\x01' # ZigZag: -1 -> 1
assert encode_signed_varint(1) == b'\x02' # ZigZag: 1 -> 2
assert encode_signed_varint(-2) == b'\x03' # ZigZag: -2 -> 3
assert encode_signed_varint(64) == b'\x80\x01' # ZigZag: 64 -> 128