3. Quantization and Sampling#
3.1. Goal#
Show how encoder/decoder resolution and sampling period affect tracking accuracy.
3.2. Model Map#
3.3. Assumptions and Scope#
No wall contact
Fixed-step co-simulation; reference uses variable-step DASSL
3.4. Prerequisites and Setup#
import numpy as np
import matplotlib.pyplot as plt
from demos.ControlledPendulum.src.master_pendulum import MasterPendulum
import demos.ControlledPendulum.src.master_pendulum.components.fem.pendulum_config as config
from syssimx import FMUComponent, Connection, System, SystemGraphVisualizer
3.5. Discover FMUs#
PLATFORM = sys.platform
demo_dir_path = _repo / "demos" / "ControlledPendulum"
package_path = Path(demo_dir_path / "src/modelica/ControlledPendulum")
fmu_output_dir = Path(demo_dir_path / f"artifacts/fmus/{PLATFORM}")
fmu_paths = {}
for subdir in fmu_output_dir.iterdir():
if subdir.is_dir():
fmu_paths[subdir.name] = {}
for fmu_file in subdir.glob("*.fmu"):
fmu_paths[subdir.name][fmu_file.stem] = fmu_file
for subsubdir in subdir.iterdir():
fmu_paths[subdir.name][subsubdir.name] = {}
if subsubdir.is_dir():
fmu_paths[subdir.name][subsubdir.name] = {}
for fmu_file in subsubdir.glob("*.fmu"):
fmu_paths[subdir.name][subsubdir.name][fmu_file.stem] = fmu_file
else:
fmu_paths[subdir.stem] = subdir
3.6. Modelica Reference Simulation#
We refer to the monolithic Modelica simulation as “Modelica” in the following. The simulation results are obtained by running the Quantization example model in OpenModelica using the OMPython interface. The model can be found in the ControlledPendulum package
from OMPython import ModelicaSystem
reference = ModelicaSystem(fileName=str(package_path / "package.mo"),
modelName="ControlledPendulum.Examples.NoContact.Quantization")
reference.buildModel()
3.6.1. Reference Sensor Parameters#
ref_params = reference.getParameters()
ref_sensor_params = {
"nBits": int(ref_params['angle_sensor.nBits']),
"samplePeriod": float(ref_params['angle_sensor.samplePeriod']),
"decoder_nBits": int(ref_params['angle_decoder.nBits']),
"decoder_samplePeriod": float(ref_params['angle_decoder.samplePeriod']),
}
print("Reference sensor parameters →")
print(f" nBits: {ref_sensor_params['nBits']}")
print(f" samplePeriod [s]: {ref_sensor_params['samplePeriod']}")
Reference sensor parameters →
nBits: 10
samplePeriod [s]: 0.01
3.6.2. Run Reference Simulation#
reference.simulate(simargs={
"stopTime": '1.0',
})
ref_sol_names = ('time', 'theta', 'theta_meas')
ref_sol = {name: reference.getSolutions(name).flatten() for name in ref_sol_names}
3.7. Instantiate FMU Components#
def print_parameteres(component: FMUComponent):
print(f"Parameters of {component.name}:")
for name, param in component.parameters.items():
if '.' not in name and param is not None:
print(f" - {name}: {param}")
3.7.1. Setpoint#
setpoint = FMUComponent(name="Setpoint",
fmu_path=fmu_paths['Trajectories']["SetPoint"],
group="Reference")
print_parameteres(setpoint)
Parameters of Setpoint:
- amplitude: 0.3490658503988659 rad
- frequency: 2.0 Hz
- offset: 0.0 rad
- phase: 0.0 rad
3.7.2. PID Controller#
pid = FMUComponent(name="PID",
fmu_path=fmu_paths['Controllers']["PIDController"],
group="Controller")
print_parameteres(pid)
Parameters of PID:
- Kd: 0.02
- Ki: 5.0
- Kp: 10.0
- uMax: 1.0
- uMin: -1.0
3.7.3. Drive#
drive = FMUComponent(name="Drive",
fmu_path=fmu_paths['Actuators']["DriveDynamic"],
group="Actuator")
print_parameteres(drive)
Parameters of Drive:
- I_max: 10.0 A
- L_arm: 0.000121 H
- R_arm: 0.151 Ohm
- V_rated: 48.0 V
- V_supply: 16.0 V
- eta: 0.85
- gearRatio: 60.0
- k_t: 0.03 m·N/A
- n_0: 12916.0 1/min
3.7.4. Pendulum#
pendulum = FMUComponent(name="Pendulum",
fmu_path=fmu_paths['Plants']["Pendulum_cvode"],
group="Plant")
print_parameteres(pendulum)
Parameters of Pendulum:
- J: 0.0442268222 kg·m²
- L: 0.181650853 m
- b_viscous: 0.0 m·N·s/rad
- g: 9.81 m/s²
- m: 1.08429606 kg
- omega_eps: 0.001 rad/s
- omega_start: 0.0 rad/s
- tau_c: 0.0 m·N
- theta_start: 0.0 rad
3.7.5. Angle Sensor and Decoder#
angle_sensor = FMUComponent(name="Angle Sensor",
fmu_path=fmu_paths['Sensors']['AngleSensor'],
group="Sensors")
print_parameteres(angle_sensor)
Parameters of Angle Sensor:
- pot_range: 4.71238898038469 rad
- r_bottom: 20000.0 Ohm
- r_top: 80000.0 Ohm
- samplePeriod: 0.01 s
- theta_max: 0.45378560551852565 rad
- theta_min: -0.45378560551852565 rad
- v_adc: 3.0 V
- v_pot: 5.0 V
- nBits: 10
angle_decoder = FMUComponent(name="Angle Decoder",
fmu_path=fmu_paths['Sensors']['AngleDecoder'],
group="Signal Processing")
print_parameteres(angle_decoder)
Parameters of Angle Decoder:
- pot_range: 4.71238898038469 rad
- r_bottom: 20000.0 Ohm
- r_top: 80000.0 Ohm
- samplePeriod: 0.01 s
- theta_max: 0.45378560551852565 rad
- theta_min: -0.45378560551852565 rad
- v_adc: 3.0 V
- v_pot: 5.0 V
- nBits: 10
3.8. Define Connections#
c1 = Connection(
src_comp=setpoint.name,
src_port=setpoint.output_specs["theta_ref"].name,
dst_comp=pid.name,
dst_port=pid.input_specs["theta_ref"].name,
)
c2 = Connection(
src_comp=pendulum.name,
src_port=pendulum.output_specs["theta"].name,
dst_comp=angle_sensor.name,
dst_port=angle_sensor.input_specs["theta"].name,
)
c3 = Connection(
src_comp=angle_sensor.name,
src_port=angle_sensor.output_specs["v_out"].name,
dst_comp=angle_decoder.name,
dst_port=angle_decoder.input_specs["v_in"].name,
)
c4 = Connection(
src_comp=angle_decoder.name,
src_port=angle_decoder.output_specs["theta"].name,
dst_comp=pid.name,
dst_port=pid.input_specs["theta_meas"].name,
)
c5 = Connection(
src_comp=pid.name,
src_port=pid.output_specs["u"].name,
dst_comp=drive.name,
dst_port=drive.input_specs["u_control"].name,
)
c6 = Connection(
src_comp=drive.name,
src_port=drive.output_specs["torque"].name,
dst_comp=pendulum.name,
dst_port=pendulum.input_specs["tau"].name,
)
c7 = Connection(
src_comp=pendulum.name,
src_port=pendulum.output_specs["omega"].name,
dst_comp=drive.name,
dst_port=drive.input_specs["omega"].name,
)
connections = [c1, c2, c3, c4, c5, c6, c7]
components = [setpoint, pid, drive, angle_sensor, angle_decoder, pendulum]
3.9. Build and Initialize the System#
system = System(name="Pendulum System with Quantization")
for comp in components:
system.add_component(comp)
for conn in connections:
system.add_connection(conn)
system.initialize(t0=0.0)
3.10. Visualize the System Graph#
visualizer = SystemGraphVisualizer(system)
visualizer.visualize()
Warning: Could not load "C:\Users\flori\anaconda3\envs\env-312\Library\bin\gvplugin_pango.dll" - It was found, so perhaps one of its dependents was not. Try ldd.
Warning: Could not load "C:\Users\flori\anaconda3\envs\env-312\Library\bin\gvplugin_pango.dll" - It was found, so perhaps one of its dependents was not. Try ldd.
3.11. Baseline Run#
angle_sensor.set_parameters(
nBits=ref_sensor_params["nBits"],
samplePeriod=ref_sensor_params["samplePeriod"],
)
angle_decoder.set_parameters(
nBits=ref_sensor_params["decoder_nBits"],
samplePeriod=ref_sensor_params["decoder_samplePeriod"],
)
t = 0.0
dt = 0.001
t_end = 1.0
system.run(t, t_end, dt)
print(f"Simulation config → dt [s]: {dt}, t_end [s]: {t_end}, nBits: {ref_sensor_params['nBits']}, samplePeriod [s]: {ref_sensor_params['samplePeriod']}")
Simulation config → dt [s]: 0.001, t_end [s]: 1.0, nBits: 10, samplePeriod [s]: 0.01
3.12. Collect Results#
history = system.get_history()
t_setpoint = history["Setpoint"][0]
theta_ref = history["Setpoint"][1]["theta_ref"]
t_pendulum = history["Pendulum"][0]
theta_pendulum = history["Pendulum"][1]["theta"]
t_decoder = history["Angle Decoder"][0]
theta_decoder = history["Angle Decoder"][1]["theta"]
3.13. Plot: Baseline Tracking vs Reference#
This baseline uses the reference sensor settings.
3.14. Experiment Design#
We vary the ADC resolution and sampling period while keeping all other parameters fixed.
3.14.1. Helper: Run a Quantization Case#
def run_quantization_case(label: str, n_bits: int, sample_period: float, dt: float = 0.01):
system.reset()
angle_sensor.set_parameters(nBits=n_bits, samplePeriod=sample_period)
angle_decoder.set_parameters(nBits=n_bits, samplePeriod=sample_period)
system.initialize(t0=0.0)
t = 0.0
t_end = 1.0
system.run(t, t_end, dt)
print(f"{label} → dt [s]: {dt}, nBits: {n_bits}, samplePeriod [s]: {sample_period}")
dec_hist = angle_decoder.get_history()
t_dec = dec_hist["theta"]["time"]
theta_dec = dec_hist["theta"]["values"]
pendulum_history = pendulum.get_history()
t_pendulum = pendulum_history['theta']['time']
theta_pendulum = pendulum_history['theta']['values']
return {
"label": label,
"t_dec": t_dec,
"theta_dec": theta_dec,
"t_pendulum": t_pendulum,
"theta_pendulum": theta_pendulum,
}
3.15. Quantization Cases#
case_high = run_quantization_case(label="High (12-bit, 5 ms)",
n_bits=12,
sample_period=0.005,
dt=0.001)
case_low = run_quantization_case(label="Low (8-bit, 20 ms)",
n_bits=8,
sample_period=0.02, dt=0.001)
3.16. Plot: Decoder Outputs#
This plot shows how sampling and quantization affect the decoded angle. It can be clearly seen that the low-resolution case (5-bit, 20 ms) has a much coarser output compared to the medium (10-bit, 10 ms) and high-resolution (12-bit, 5 ms) cases. The medium and high-resolution cases show more closely spaced points, indicating finer quantization levels and more frequent sampling.
Further the pendulum angle can follow the reference more closely in the medium and high-resolution cases, while the low-resolution case shows an oscillatory behavior due to the coarse quantization and less frequent sampling. The PID controller struggles to stabilize the pendulum in the low-resolution case, while it performs much better in the medium and high-resolution cases.
3.17. Summary#
Higher resolution and faster sampling reduce quantization artifacts in
theta_meas.Control performance degrades at low resolution and slow sampling.