Skip to content

Emulator

Emulator is a tool that records AUTD3 emission data without real hardware and computes the sound field offline from it (not provided for C#).

By passing a Geometry to Emulator::new, sending data inside record, and advancing time with tick, a Record with the sent data recorded is obtained.

let emulator = Emulator::new(geometry);
let record = emulator.record(async move |r| {
let mut builder = r.datagram_builder();
builder.push(Pattern::new(&patterns));
let datagrams = builder.build()?;
for frame in &datagrams {
r.send_checked(frame).await?;
}
r.tick(Duration::from_millis(1))?;
Ok(())
})?;

The time passed to tick must be an integer multiple of the ultrasound period (25 us).

The output data is a Polars DataFrame. For details, refer to the Polars documentation.

Emulator::transducer_table returns a list of all transducers as a DataFrame. Each row is one transducer, storing the device index (dev_idx), the local index within the device (tr_idx), the position (x/y/z[mm]), and the normal direction (nx/ny/nz). Each row of the emission data and output signal described later corresponds one-to-one with a row of this table.

let table = emulator.transducer_table();
dbg!(table);
shape: (249, 8)
┌─────────┬────────┬────────────┬────────────┬───────┬─────┬─────┬─────┐
│ dev_idx ┆ tr_idx ┆ x[mm] ┆ y[mm] ┆ z[mm] ┆ nx ┆ ny ┆ nz │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ u16 ┆ u8 ┆ f32 ┆ f32 ┆ f32 ┆ f32 ┆ f32 ┆ f32 │
╞═════════╪════════╪════════════╪════════════╪═══════╪═════╪═════╪═════╡
│ 0 ┆ 0 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 1.0 │
│ 0 ┆ 1 ┆ 10.16 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 1.0 │
│ 0 ┆ 2 ┆ 20.32 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 1.0 │
│ 0 ┆ 3 ┆ 30.48 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 1.0 │
│ 0 ┆ 4 ┆ 40.639999 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 1.0 │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 0 ┆ 244 ┆ 132.080002 ┆ 132.080002 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 1.0 │
│ 0 ┆ 245 ┆ 142.23999 ┆ 132.080002 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 1.0 │
│ 0 ┆ 246 ┆ 152.399994 ┆ 132.080002 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 1.0 │
│ 0 ┆ 247 ┆ 162.559998 ┆ 132.080002 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 1.0 │
│ 0 ┆ 248 ┆ 172.720001 ┆ 132.080002 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 1.0 │
└─────────┴────────┴────────────┴────────────┴───────┴─────┴─────┴─────┘

From the Record returned by record, the recorded emission data can be obtained. phase / pulse_width return a DataFrame whose columns are times. The column names are phase@<time>[ns] / pulse_width@<time>[ns], and the time is in increments of the ultrasound period (25 us).

let phase = record.phase();
dbg!(phase);
let pulse_width = record.pulse_width();
dbg!(pulse_width);
shape: (249, 40)
┌─────────────┬─────────────────┬─────────────────┬─────────────────┬───┬──────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ phase@0[ns] ┆ phase@25000[ns] ┆ phase@50000[ns] ┆ phase@75000[ns] ┆ … ┆ phase@900000[ns] ┆ phase@925000[ns] ┆ phase@950000[ns] ┆ phase@975000[ns] │
│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
│ u8 ┆ u8 ┆ u8 ┆ u8 ┆ ┆ u8 ┆ u8 ┆ u8 ┆ u8 │
╞═════════════╪═════════════════╪═════════════════╪═════════════════╪═══╪══════════════════╪══════════════════╪══════════════════╪══════════════════╡
│ 1 ┆ 2 ┆ 3 ┆ 4 ┆ … ┆ 38 ┆ 39 ┆ 40 ┆ 42 │
│ 254 ┆ 252 ┆ 250 ┆ 248 ┆ … ┆ 183 ┆ 181 ┆ 179 ┆ 178 │
│ 1 ┆ 2 ┆ 3 ┆ 4 ┆ … ┆ 40 ┆ 41 ┆ 42 ┆ 44 │
│ 253 ┆ 250 ┆ 248 ┆ 245 ┆ … ┆ 158 ┆ 156 ┆ 153 ┆ 151 │
│ 255 ┆ 255 ┆ 254 ┆ 254 ┆ … ┆ 242 ┆ 241 ┆ 241 ┆ 241 │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 0 ┆ 0 ┆ 0 ┆ 0 ┆ … ┆ 4 ┆ 4 ┆ 4 ┆ 5 │
│ 253 ┆ 251 ┆ 249 ┆ 247 ┆ … ┆ 177 ┆ 175 ┆ 173 ┆ 171 │
│ 1 ┆ 3 ┆ 4 ┆ 6 ┆ … ┆ 60 ┆ 61 ┆ 63 ┆ 65 │
│ 254 ┆ 253 ┆ 251 ┆ 250 ┆ … ┆ 203 ┆ 201 ┆ 200 ┆ 199 │
│ 1 ┆ 3 ┆ 4 ┆ 6 ┆ … ┆ 58 ┆ 59 ┆ 61 ┆ 63 │
└─────────────┴─────────────────┴─────────────────┴─────────────────┴───┴──────────────────┴──────────────────┴──────────────────┴──────────────────┘
shape: (249, 40)
┌───────────────────┬───────────────────────┬───────────────────────┬───────────────────────┬───┬────────────────────────┬────────────────────────┬────────────────────────┬────────────────────────┐
│ pulse_width@0[ns] ┆ pulse_width@25000[ns] ┆ pulse_width@50000[ns] ┆ pulse_width@75000[ns] ┆ … ┆ pulse_width@900000[ns] ┆ pulse_width@925000[ns] ┆ pulse_width@950000[ns] ┆ pulse_width@975000[ns] │
│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
│ u16 ┆ u16 ┆ u16 ┆ u16 ┆ ┆ u16 ┆ u16 ┆ u16 ┆ u16 │
╞═══════════════════╪═══════════════════════╪═══════════════════════╪═══════════════════════╪═══╪════════════════════════╪════════════════════════╪════════════════════════╪════════════════════════╡
│ 16 ┆ 33 ┆ 49 ┆ 67 ┆ … ┆ 256 ┆ 256 ┆ 256 ┆ 256 │
│ 16 ┆ 33 ┆ 49 ┆ 67 ┆ … ┆ 256 ┆ 256 ┆ 256 ┆ 256 │
│ 16 ┆ 33 ┆ 49 ┆ 67 ┆ … ┆ 256 ┆ 256 ┆ 256 ┆ 256 │
│ 16 ┆ 33 ┆ 49 ┆ 67 ┆ … ┆ 256 ┆ 256 ┆ 256 ┆ 256 │
│ 16 ┆ 33 ┆ 49 ┆ 67 ┆ … ┆ 256 ┆ 256 ┆ 256 ┆ 256 │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 16 ┆ 33 ┆ 49 ┆ 67 ┆ … ┆ 256 ┆ 256 ┆ 256 ┆ 256 │
│ 16 ┆ 33 ┆ 49 ┆ 67 ┆ … ┆ 256 ┆ 256 ┆ 256 ┆ 256 │
│ 16 ┆ 33 ┆ 49 ┆ 67 ┆ … ┆ 256 ┆ 256 ┆ 256 ┆ 256 │
│ 16 ┆ 33 ┆ 49 ┆ 67 ┆ … ┆ 256 ┆ 256 ┆ 256 ┆ 256 │
│ 16 ┆ 33 ┆ 49 ┆ 67 ┆ … ┆ 256 ┆ 256 ┆ 256 ┆ 256 │
└───────────────────┴───────────────────────┴───────────────────────┴───────────────────────┴───┴────────────────────────┴────────────────────────┴────────────────────────┴────────────────────────┘

The voltage applied to the transducer and the ultrasound output from the transducer are computed with a BVD (Butterworth-Van Dyke) model. Both have a time resolution of 25 us / 512.

let voltage = record.output_voltage();
dbg!(voltage);
let ultrasound = record.output_ultrasound();
dbg!(ultrasound);
shape: (249, 20_480)
┌────────────────────────┬────────────────────────┬────────────────────────┬────────────────────────┬───┬────────────────────────────┬────────────────────────────┬────────────────────────────┬────────────────────────────┐
│ voltage[V]@0[25us/512] ┆ voltage[V]@1[25us/512] ┆ voltage[V]@2[25us/512] ┆ voltage[V]@3[25us/512] ┆ … ┆ voltage[V]@20476[25us/512] ┆ voltage[V]@20477[25us/512] ┆ voltage[V]@20478[25us/512] ┆ voltage[V]@20479[25us/512] │
│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
│ f32 ┆ f32 ┆ f32 ┆ f32 ┆ ┆ f32 ┆ f32 ┆ f32 ┆ f32 │
╞════════════════════════╪════════════════════════╪════════════════════════╪════════════════════════╪═══╪════════════════════════════╪════════════════════════════╪════════════════════════════╪════════════════════════════╡
│ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ … ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 │
│ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ … ┆ -12.0 ┆ -12.0 ┆ -12.0 ┆ -12.0 │
│ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ … ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 │
│ 12.0 ┆ 12.0 ┆ -12.0 ┆ -12.0 ┆ … ┆ -12.0 ┆ -12.0 ┆ -12.0 ┆ -12.0 │
│ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ … ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ … ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 │
│ 12.0 ┆ 12.0 ┆ -12.0 ┆ -12.0 ┆ … ┆ -12.0 ┆ -12.0 ┆ -12.0 ┆ -12.0 │
│ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ … ┆ -12.0 ┆ -12.0 ┆ -12.0 ┆ -12.0 │
│ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ … ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 │
│ 12.0 ┆ 12.0 ┆ 12.0 ┆ 12.0 ┆ … ┆ -12.0 ┆ -12.0 ┆ 12.0 ┆ 12.0 │
└────────────────────────┴────────────────────────┴────────────────────────┴────────────────────────┴───┴────────────────────────────┴────────────────────────────┴────────────────────────────┴────────────────────────────┘
shape: (249, 20_480)
┌─────────────────────┬─────────────────────┬─────────────────────┬─────────────────────┬───┬─────────────────────────┬─────────────────────────┬─────────────────────────┬─────────────────────────┐
│ p[a.u.]@0[25us/512] ┆ p[a.u.]@1[25us/512] ┆ p[a.u.]@2[25us/512] ┆ p[a.u.]@3[25us/512] ┆ … ┆ p[a.u.]@20476[25us/512] ┆ p[a.u.]@20477[25us/512] ┆ p[a.u.]@20478[25us/512] ┆ p[a.u.]@20479[25us/512] │
│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
│ f32 ┆ f32 ┆ f32 ┆ f32 ┆ ┆ f32 ┆ f32 ┆ f32 ┆ f32 │
╞═════════════════════╪═════════════════════╪═════════════════════╪═════════════════════╪═══╪═════════════════════════╪═════════════════════════╪═════════════════════════╪═════════════════════════╡
│ 0.0 ┆ -0.000272 ┆ -0.000481 ┆ -0.000618 ┆ … ┆ 0.367795 ┆ 0.379597 ┆ 0.391337 ┆ 0.403013 │
│ 0.0 ┆ -0.000272 ┆ -0.000481 ┆ -0.000618 ┆ … ┆ 0.261409 ┆ 0.250499 ┆ 0.239553 ┆ 0.228573 │
│ 0.0 ┆ -0.000272 ┆ -0.000481 ┆ -0.000618 ┆ … ┆ 0.325682 ┆ 0.337693 ┆ 0.349649 ┆ 0.361547 │
│ 0.0 ┆ -0.000272 ┆ -0.000481 ┆ -0.000766 ┆ … ┆ -0.237067 ┆ -0.247149 ┆ -0.257188 ┆ -0.267184 │
│ 0.0 ┆ -0.000272 ┆ -0.000481 ┆ -0.000618 ┆ … ┆ 0.980448 ┆ 0.98075 ┆ 0.980904 ┆ 0.980913 │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 0.0 ┆ -0.000272 ┆ -0.000481 ┆ -0.000618 ┆ … ┆ 0.904004 ┆ 0.909175 ┆ 0.914209 ┆ 0.919103 │
│ 0.0 ┆ -0.000272 ┆ -0.000481 ┆ -0.000766 ┆ … ┆ 0.13367 ┆ 0.122602 ┆ 0.111521 ┆ 0.100428 │
│ 0.0 ┆ -0.000272 ┆ -0.000481 ┆ -0.000618 ┆ … ┆ -0.108901 ┆ -0.096903 ┆ -0.084897 ┆ -0.072884 │
│ 0.0 ┆ -0.000272 ┆ -0.000481 ┆ -0.000618 ┆ … ┆ 0.620835 ┆ 0.612537 ┆ 0.604152 ┆ 0.595681 │
│ 0.0 ┆ -0.000272 ┆ -0.000481 ┆ -0.000618 ┆ … ┆ -0.066326 ┆ -0.054301 ┆ -0.042275 ┆ -0.0301 │
└─────────────────────┴─────────────────────┴─────────────────────┴─────────────────────┴───┴─────────────────────────┴─────────────────────────┴─────────────────────────┴─────────────────────────┘

Passing an observation range (Range) and options to Record::sound_field yields a sound field computation object. There are two kinds of options: RmsRecordOption (RMS sound pressure) and InstantRecordOption (instantaneous sound pressure).

For both, observe_points gives the observation point coordinates, and next gives the sound field for the specified duration. skip advances time without computing.

Each row of the DataFrame returned by observe_points is an observation point and corresponds to a row of the sound field DataFrame.

let rms = record.sound_field(range, RmsRecordOption::default())?;
let observe_points = rms.observe_points();
dbg!(observe_points);
shape: (1_681, 3)
┌────────────┬───────────┬───────┐
│ x[mm] ┆ y[mm] ┆ z[mm] │
│ --- ┆ --- ┆ --- │
│ f32 ┆ f32 ┆ f32 │
╞════════════╪═══════════╪═══════╡
│ 66.625267 ┆ 46.713196 ┆ 150.0 │
│ 67.625267 ┆ 46.713196 ┆ 150.0 │
│ 68.625267 ┆ 46.713196 ┆ 150.0 │
│ 69.625267 ┆ 46.713196 ┆ 150.0 │
│ 70.625267 ┆ 46.713196 ┆ 150.0 │
│ … ┆ … ┆ … │
│ 102.625267 ┆ 86.713196 ┆ 150.0 │
│ 103.625267 ┆ 86.713196 ┆ 150.0 │
│ 104.625267 ┆ 86.713196 ┆ 150.0 │
│ 105.625267 ┆ 86.713196 ┆ 150.0 │
│ 106.625267 ┆ 86.713196 ┆ 150.0 │
└────────────┴───────────┴───────┘

Each column of the DataFrame returned by next stores the RMS sound pressure per observation point every 25 μs (column name rms[Pa]@<time>[ns]).

let range = RangeXY {
x: (target.x - 20.0)..=(target.x + 20.0),
y: (target.y - 20.0)..=(target.y + 20.0),
z: target.z,
resolution: 1.0,
};
let mut rms = record.sound_field(range.clone(), RmsRecordOption::default())?;
let field = rms.next(Duration::from_micros(25))?;
dbg!(field);
shape: (1_681, 1)
┌───────────────┐
│ rms[Pa]@0[ns] │
│ --- │
│ f32 │
╞═══════════════╡
│ 22.213818 │
│ 22.964384 │
│ 23.199003 │
│ 22.829695 │
│ 21.915253 │
│ … │
│ 19.375198 │
│ 19.254133 │
│ 18.922194 │
│ 18.434885 │
│ 17.942478 │
└───────────────┘

For the instantaneous sound pressure, the time resolution is specified with time_step. Each column of the DataFrame returned by next stores the instantaneous sound pressure per observation point in increments of time_step (column name p[Pa]@<time>[ns]).

let mut instant = record.sound_field(
range,
InstantRecordOption {
time_step: Duration::from_micros(1),
..Default::default()
},
)?;
instant.skip(Duration::from_micros(500))?;
let field = instant.next(Duration::from_micros(25))?;
dbg!(field);
shape: (1_681, 25)
┌──────────────────┬──────────────────┬──────────────────┬──────────────────┬───┬──────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ p[Pa]@500000[ns] ┆ p[Pa]@501000[ns] ┆ p[Pa]@502000[ns] ┆ p[Pa]@503000[ns] ┆ … ┆ p[Pa]@521000[ns] ┆ p[Pa]@522000[ns] ┆ p[Pa]@523000[ns] ┆ p[Pa]@524000[ns] │
│ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
│ f32 ┆ f32 ┆ f32 ┆ f32 ┆ ┆ f32 ┆ f32 ┆ f32 ┆ f32 │
╞══════════════════╪══════════════════╪══════════════════╪══════════════════╪═══╪══════════════════╪══════════════════╪══════════════════╪══════════════════╡
│ 10.003653 ┆ 3.894438 ┆ -6.252444 ┆ -16.506908 ┆ … ┆ 28.38765 ┆ 28.567314 ┆ 29.98049 ┆ 32.387314 │
│ 7.126377 ┆ 2.020372 ┆ -8.165694 ┆ -17.129423 ┆ … ┆ 31.707983 ┆ 31.654594 ┆ 30.74332 ┆ 31.946955 │
│ 3.92967 ┆ -1.315457 ┆ -9.487965 ┆ -16.549021 ┆ … ┆ 35.146038 ┆ 35.977501 ┆ 33.133389 ┆ 30.673653 │
│ 0.343901 ┆ -5.408858 ┆ -10.659369 ┆ -15.897531 ┆ … ┆ 38.936932 ┆ 39.213017 ┆ 35.64106 ┆ 29.435026 │
│ -1.625009 ┆ -8.594726 ┆ -11.886508 ┆ -15.217589 ┆ … ┆ 41.400124 ┆ 41.093994 ┆ 36.455021 ┆ 29.054434 │
│ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
│ 1.06873 ┆ -4.236325 ┆ -7.555728 ┆ -12.076913 ┆ … ┆ 33.985756 ┆ 36.456608 ┆ 34.53088 ┆ 29.42366 │
│ 4.188988 ┆ -0.0129 ┆ -5.104079 ┆ -12.541022 ┆ … ┆ 30.883274 ┆ 34.034138 ┆ 33.920597 ┆ 29.746302 │
│ 8.367446 ┆ 4.674797 ┆ -3.678846 ┆ -13.309359 ┆ … ┆ 28.576004 ┆ 30.555122 ┆ 31.229137 ┆ 31.220989 │
│ 12.870935 ┆ 7.263134 ┆ -2.312043 ┆ -13.103703 ┆ … ┆ 26.725754 ┆ 27.065441 ┆ 29.68734 ┆ 32.745209 │
│ 15.355244 ┆ 8.761391 ┆ -1.179362 ┆ -12.026244 ┆ … ┆ 24.756136 ┆ 25.81175 ┆ 29.540882 ┆ 33.987869 │
└──────────────────┴──────────────────┴──────────────────┴──────────────────┴───┴──────────────────┴──────────────────┴──────────────────┴──────────────────┘

Range represents a set of observation points. Besides planes such as RangeXY, there are single-axis variants such as RangeX and all-three-axis variants such as RangeXYZ. The order of the axes (RangeXY vs. RangeYX, etc.) determines the enumeration order of the observation points. resolution is the step width of each axis (mm).

Enabling the gpu feature allows the sound field computation to be done on the GPU. Only when enabled does each option gain a gpu: bool field, and setting it to true uses the GPU (default is false).

let mut rms = record.sound_field(
range,
RmsRecordOption {
gpu: true,
..Default::default()
},
)?;