Reconcile an RC circuit model
Compare an explicit transient solution with an analytic step and frequency response, then inspect declared tolerance assumptions.
Goal and how to run
This lesson assumes basic Python knowledge. Read each method and caption before changing parameters. AI exercises are optional; no AI account is needed for the core workflow. Data lives in notebook metadata, so there is no companion data download. Keep the notebook saved in the active working folder.
Your goal is to understand the research question, execute transparent calculations, check the results, and state the limits of the conclusion.
Setup and parameters
from pathlib import Path
import base64, gzip, hashlib, json, platform
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, HTML
from IPython.display import display as _display
def display(value):
if isinstance(value,pd.DataFrame):
table=value.to_html(max_rows=12,escape=True)
style="<style>.vibeit-readable-table{max-width:100%;overflow-x:auto}.vibeit-readable-table table{width:max-content;max-width:none;border-collapse:collapse}.vibeit-readable-table td,.vibeit-readable-table th{white-space:nowrap!important;word-break:normal!important;overflow-wrap:normal!important}</style>"
return _display(HTML(style+'<div class="vibeit-readable-table">'+table+'</div>'))
return _display(value)
RECIPE_ID='en05-rc-circuit'
OUTPUT_DIR=Path.cwd()/"results"/RECIPE_ID
plt.rcParams.update({"figure.dpi":120,"font.size":10,"axes.spines.top":False,"axes.spines.right":False,
"axes.prop_cycle":plt.cycler(color=["#18785f","#416aa6","#bc6541","#9974a5"])})
print("Python:",platform.python_version(),"; NumPy:",np.__version__,"; Pandas:",pd.__version__)
print("Working folder:",Path.cwd())
print("Core data mode: embedded snapshot; no network requests")
Python: 3.13.13 ; NumPy: 2.5.3 ; Pandas: 3.0.6 Working folder: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads Core data mode: embedded snapshot; no network requests
Load the embedded snapshot
def load_snapshot(recipe_id, expected_sha256, snapshot_key):
"""Read embedded data from this notebook, including a renamed copy."""
for path in sorted(Path.cwd().glob("*.ipynb")):
try:
notebook = json.loads(path.read_text(encoding="utf-8"))
metadata = notebook.get("metadata", {}).get("vibeit_cookbook", {})
blob = metadata.get("snapshots", {}).get(snapshot_key)
if metadata.get("recipe_id") != recipe_id or not blob:
continue
if blob.get("sha256") != expected_sha256:
continue
raw = gzip.decompress(base64.b64decode(blob["gzip_base64"], validate=True))
if hashlib.sha256(raw).hexdigest() != expected_sha256:
raise ValueError("Embedded snapshot checksum mismatch: " + snapshot_key)
return json.loads(raw)
except (OSError, json.JSONDecodeError):
continue
raise FileNotFoundError(
"Save/open the complete .ipynb in the active working folder. "
"The embedded snapshot could not be located for " + recipe_id)
SNAPSHOT_HASHES={'models': '48fd2e6e706650fe2ee0c0ad80b6d794a546a1dec78dd9d370ac09db3cc3adbf'}
def snapshot(key):
return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['models']
Read the method functions
def rc_step_response(resistance_ohm,capacitance_F,input_voltage_V,steps_per_tau=20,duration_tau=5):
if resistance_ohm<=0 or capacitance_F<=0 or steps_per_tau<1 or duration_tau<=0:
raise ValueError('Positive R/C and valid sampling settings are required')
tau=resistance_ohm*capacitance_F;count=int(np.ceil(duration_tau*steps_per_tau))
time=np.arange(count+1)*tau/steps_per_tau;voltage=np.zeros(count+1);dt=time[1]-time[0]
for j in range(count):voltage[j+1]=voltage[j]+dt*(input_voltage_V-voltage[j])/tau
exact=input_voltage_V*(-np.expm1(-time/tau))
return dict(t=time,numerical_voltage=voltage,analytic_voltage=exact,tau_s=tau,step_s=dt)
print("Method ready: rc_step_response")
Method ready: rc_step_response
Data and model boundary
The building record contains real ten-minute energy and environmental observations from one source building. Airport weather fields include upstream interpolation and are not colocated calibration references. Beam, circuit and PID examples are declared numerical design models; computed responses do not establish hardware performance or commissioning acceptance.
Steps
1. Compare analytic and explicit transient responses
Nominal tau=R·C in seconds. The explicit Euler step uses dt=tau/20, while the exact step response is Vin(1−exp(−t/tau)). These are model voltages, not oscilloscope measurements.
models=snapshot("models");p=models["circuit"]
solution=rc_step_response(p["resistance_ohm"],p["capacitance_F"],p["input_voltage_V"])
fig,ax=plt.subplots(figsize=(8,4));ax.plot(solution["t"],solution["analytic_voltage"],label="Analytic")
ax.plot(solution["t"],solution["numerical_voltage"],"--",label="Explicit Euler")
ax.set(xlabel="Model time (s)",ylabel="Capacitor voltage (V)",title="RC step-response consistency");ax.legend();plt.tight_layout();plt.show()
2. Read gain and phase with Hz units
The transfer response is 1/(1+j2pi f RC); angular frequency is 2pi f. The cutoff has magnitude 1/sqrt(2), approximately −3.01 dB. Phase is displayed in degrees, and the source has no measured parasitic components.
tau=solution["tau_s"];frequency=np.geomspace(.01,100,501);response=1/(1+1j*2*np.pi*frequency*tau)
cutoff=1/(2*np.pi*tau)
fig,axes=plt.subplots(1,2,figsize=(10,4));axes[0].semilogx(frequency,20*np.log10(abs(response)))
axes[0].axvline(cutoff,color="gray",linestyle="--");axes[0].set(xlabel="Frequency (Hz)",ylabel="Gain (dB)")
axes[1].semilogx(frequency,np.angle(response,deg=True));axes[1].set(xlabel="Frequency (Hz)",ylabel="Phase (degrees)")
plt.tight_layout();plt.show();print("Nominal tau (s):",tau,"; cutoff (Hz):",cutoff)
Nominal tau (s): 0.1 ; cutoff (Hz): 1.5915494309189535
3. Keep tolerance bounds separate from probability assumptions
For this explicit Monte Carlo exercise, assume independent uniform ±5% distributions for R and C. Real tolerance specifications are bounds and do not prove such a distribution or independence. Plot the resulting simulated tau values, not measured batch statistics.
rng=np.random.default_rng(models["seed"]);tolerance=p["tolerance_fraction"]
resistance=p["resistance_ohm"]*rng.uniform(1-tolerance,1+tolerance,5000)
capacitance=p["capacitance_F"]*rng.uniform(1-tolerance,1+tolerance,5000);drawn_tau=resistance*capacitance
fig,ax=plt.subplots(figsize=(8,4));ax.hist(drawn_tau,bins=35,edgecolor="white");ax.axvline(tau,color="black",linestyle="--")
ax.set(xlabel="Simulated time constant (s)",ylabel="Declared independent draws",title="Tolerance-propagation model");plt.tight_layout();plt.show()
4. Check limits and export units/assumptions
Verify the one-time-constant analytic voltage, cutoff magnitude and tolerance bounds. Save all model inputs and the assumed distribution. Numerical error and simulated component variation remain different quantities.
index=int(np.argmin(abs(solution["t"]-tau)))
assert np.isclose(solution["analytic_voltage"][index],p["input_voltage_V"]*(1-np.exp(-1)))
assert np.isclose(abs(1/(1+1j*2*np.pi*cutoff*tau)),1/np.sqrt(2))
assert drawn_tau.min()>=tau*(1-tolerance)**2 and drawn_tau.max()<=tau*(1+tolerance)**2
OUTPUT_DIR.mkdir(parents=True,exist_ok=True)
pd.DataFrame({"time_s":solution["t"],"analytic_voltage_V":solution["analytic_voltage"],"Euler_voltage_V":solution["numerical_voltage"]}).to_csv(OUTPUT_DIR/"rc-transient.csv",index=False)
pd.DataFrame({"frequency_Hz":frequency,"gain_dB":20*np.log10(abs(response)),"phase_degrees":np.angle(response,deg=True)}).to_csv(OUTPUT_DIR/"rc-frequency-response.csv",index=False)
(OUTPUT_DIR/"tolerance-assumptions.json").write_text(json.dumps({**p,"seed":models["seed"],"distribution":"independent uniform tolerance ranges; assumed, not inferred"},indent=2))
print("Circuit reference and bound checks passed; exports:",OUTPUT_DIR)
Circuit reference and bound checks passed; exports: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads/results/en05-rc-circuit
Exercises and reference explanation
- Explain units of RC and the cutoff.
- Why is a tolerance bound not a probability distribution?
Reference explanation. RC is seconds and 1/(2pi RC) is Hz. A manufacturing bound alone does not establish uniformity, independence or an empirical interval.
Keep a copy before changing parameters. Compare old and new figures and record which inputs, calculations and conclusions changed.
Optional AI coding exercises
Prompt 1
Using NumPy/Matplotlib, compare 10, 20 and 80 Euler steps per tau and show actual errors against the analytic response.
Prompt 2
With the same packages, compare independent-uniform with a clearly labelled correlated tolerance simulation.
Human acceptance. Analytic limits, −3 dB cutoff and units reconcile; all simulated distribution assumptions are saved.
Review the edited source, then manually run affected cells and subsequent checks. Ask the assistant to explain the purpose, packages and data provenance of its changes, and reconcile that explanation with actual output.
Optional online extension
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
import requests
try:
response=requests.get('https://docs.scipy.org/doc/scipy/tutorial/linalg.html',timeout=20)
response.raise_for_status()
print("Source:",response.url)
print(response.text[:700])
except requests.RequestException as error:
print("Online extension unavailable:",type(error).__name__,str(error)[:160])
else:
print("Online extension skipped; embedded core data unchanged")
Online extension skipped; embedded core data unchanged
Sources, snapshots and next steps
IAPWS saturation reference release — IAPWS SR1-86(1992): unrestricted publication allowed in all countries (cover page); retrieved 2026-10-09. SHA-256
a10d38d73339b0713c86cfac88f9adcba137202167ccdfa1cd58238c438361d7. Reference equations and numerical coefficients transcribed from release pages 2–3; pedagogical model values are not new experiments.Authored numerical teaching model parameters — Original teaching parameters/code MIT; reference equation coefficients attributed to IAPWS SR1-86(1992); retrieved 2026-10-09. SHA-256
4350c4305a83c772fa295a8be0ef86a247f2bfaae930a081720d3cd0c753abe0. Declared parameters and seed; generated trajectories and calibration observations must be labelled simulated.
Next steps. Transfer these methods to your own public or authorized data while retaining input checks, recorded parameters and result validation. Document the new experimental design and method limits; this example does not validate a new experiment. Full data licenses and generation instructions accompany the cookbook.