1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
|
#!/usr/bin/env python3
"""SPICE sanity check for the auto-zero sample/hold/subtract primitive.
This is a generic switch-capacitor primitive, not a reconstruction of the
complete CLLN edge. It tests acquisition and droop around the published
5 mV AD633 output offset and 100 us learning window.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import re
import subprocess
import tempfile
OFFSET_V = 5.0e-3
LEARNING_SIGNAL_V = 1.0e-3
LEARNING_WINDOW_SECONDS = 100.0e-6
def netlist(
*,
acquisition_seconds: float,
switch_resistance_ohm: float,
hold_capacitance_f: float,
leakage_resistance_ohm: float,
) -> str:
switch_time = acquisition_seconds
switch_transition_end = switch_time + 1.0e-9
raw_transition_start = switch_time + 10.0e-9
raw_transition_end = switch_time + 11.0e-9
measurement_start = switch_time + 20.0e-9
measurement_end = measurement_start + LEARNING_WINDOW_SECONDS
simulation_end = measurement_end + 1.0e-6
return f"""Hardware auto-zero primitive sanity check
Vraw raw 0 PWL(0 {OFFSET_V} {raw_transition_start} {OFFSET_V} {raw_transition_end} {OFFSET_V + LEARNING_SIGNAL_V} {simulation_end} {OFFSET_V + LEARNING_SIGNAL_V})
Vsample sample 0 PWL(0 5 {switch_time} 5 {switch_transition_end} 0 {simulation_end} 0)
Ssample raw hold sample 0 sample_switch
.model sample_switch SW(Ron={switch_resistance_ohm} Roff=1e12 Vt=2.5 Vh=0.1)
Chold hold 0 {hold_capacitance_f} IC=0
Rleak hold 0 {leakage_resistance_ohm}
Ebuffer held 0 hold 0 1
Esubtract residual 0 VALUE={{V(raw)-V(held)}}
.tran 1n {simulation_end} UIC
.meas tran held_at_open FIND V(held) AT={measurement_start}
.meas tran residual_at_start FIND V(residual) AT={measurement_start}
.meas tran residual_at_end FIND V(residual) AT={measurement_end}
.end
"""
def extract_measurement(output: str, name: str) -> float:
match = re.search(
rf"^{re.escape(name)}\s*=\s*([-+0-9.eE]+)",
output,
flags=re.MULTILINE,
)
if match is None:
raise RuntimeError(f"ngspice did not report {name}\n{output[-2000:]}")
return float(match.group(1))
def simulate(configuration: dict) -> dict:
circuit = netlist(**configuration)
with tempfile.TemporaryDirectory(prefix="sdil-autozero-spice-") as folder:
path = Path(folder) / "autozero.cir"
path.write_text(circuit)
completed = subprocess.run(
["ngspice", "-b", str(path)],
check=True,
capture_output=True,
text=True,
)
combined_output = completed.stdout + completed.stderr
held = extract_measurement(combined_output, "held_at_open")
residual_start = extract_measurement(
combined_output, "residual_at_start")
residual_end = extract_measurement(combined_output, "residual_at_end")
return {
**configuration,
"held_at_switch_open_v": held,
"residual_at_learning_start_v": residual_start,
"residual_at_learning_end_v": residual_end,
"start_relative_error": abs(
residual_start - LEARNING_SIGNAL_V) / LEARNING_SIGNAL_V,
"end_relative_error": abs(
residual_end - LEARNING_SIGNAL_V) / LEARNING_SIGNAL_V,
}
def configurations() -> list[dict]:
records = []
for acquisition_us in (0.25, 0.5, 1.0, 2.0, 4.0):
for switch_resistance in (50.0, 200.0, 1000.0):
for capacitance_nf in (0.1, 1.0, 10.0):
records.append({
"acquisition_seconds": acquisition_us * 1e-6,
"switch_resistance_ohm": switch_resistance,
"hold_capacitance_f": capacitance_nf * 1e-9,
"leakage_resistance_ohm": 1e9,
})
for leakage_resistance in (1e7, 1e8, 1e9, 1e10):
records.append({
"acquisition_seconds": 1e-6,
"switch_resistance_ohm": 200.0,
"hold_capacitance_f": 1e-9,
"leakage_resistance_ohm": leakage_resistance,
})
unique = []
seen = set()
for record in records:
key = tuple(record.items())
if key not in seen:
unique.append(record)
seen.add(key)
return unique
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--output", type=Path,
default=Path(
"results/physical_bias/p8_spice_autozero_primitive.json"))
return parser.parse_args()
def main() -> None:
args = parse_args()
records = [simulate(configuration) for configuration in configurations()]
nominal = next(record for record in records if (
record["acquisition_seconds"] == 1e-6
and record["switch_resistance_ohm"] == 200.0
and record["hold_capacitance_f"] == 1e-9
and record["leakage_resistance_ohm"] == 1e9
))
output = {
"analysis": "generic_spice_autozero_primitive_p8",
"publication_evidence": False,
"scope": (
"Generic switch-capacitor acquisition and droop sanity check; "
"this is not the complete CLLN edge circuit."),
"ngspice_model": {
"raw_offset_v": OFFSET_V,
"learning_signal_v": LEARNING_SIGNAL_V,
"learning_window_seconds": LEARNING_WINDOW_SECONDS,
"buffer_and_subtractor": "ideal controlled voltage sources",
"switch": "ngspice voltage-controlled switch",
},
"nominal_configuration": nominal,
"configuration_count": len(records),
"fraction_below_1_percent_error_at_start": sum(
record["start_relative_error"] < 0.01 for record in records
) / len(records),
"fraction_below_1_percent_error_at_end": sum(
record["end_relative_error"] < 0.01 for record in records
) / len(records),
"records": records,
}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(output, indent=2) + "\n")
print(json.dumps({
"configuration_count": len(records),
"nominal_configuration": nominal,
"fraction_below_1_percent_error_at_start": (
output["fraction_below_1_percent_error_at_start"]),
"fraction_below_1_percent_error_at_end": (
output["fraction_below_1_percent_error_at_end"]),
}, indent=2))
print(f"wrote {args.output}")
if __name__ == "__main__":
main()
|