Add turbocharged twin scenario and cross-platform test harness
This commit is contained in:
161
tools/audio_capture.cpp
Normal file
161
tools/audio_capture.cpp
Normal file
@@ -0,0 +1,161 @@
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include "FluidSim/TwoStrokeScenario.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
using namespace fluidsim::port;
|
||||
namespace {
|
||||
class Wav {
|
||||
std::ofstream out_;
|
||||
void integer(std::uint32_t n, int bytes) {
|
||||
for (int i = 0; i < bytes; i++)
|
||||
out_.put(char(n >> (8 * i)));
|
||||
}
|
||||
|
||||
public:
|
||||
Wav(const std::string &path, int frames) : out_(path, std::ios::binary) {
|
||||
if (!out_)
|
||||
throw std::runtime_error("cannot open WAV: " + path);
|
||||
out_.write("RIFF", 4);
|
||||
integer(36 + frames * 4, 4);
|
||||
out_.write("WAVEfmt ", 8);
|
||||
integer(16, 4);
|
||||
integer(1, 2);
|
||||
integer(2, 2);
|
||||
integer(44100, 4);
|
||||
integer(44100 * 4, 4);
|
||||
integer(4, 2);
|
||||
integer(16, 2);
|
||||
out_.write("data", 4);
|
||||
integer(frames * 4, 4);
|
||||
}
|
||||
void sample(AudioFrame f) {
|
||||
for (float x : {f.left, f.right}) {
|
||||
const auto v = static_cast<std::int16_t>(std::lround(std::clamp(x, -1.f, 1.f) * 32767));
|
||||
integer(static_cast<std::uint16_t>(v), 2);
|
||||
}
|
||||
}
|
||||
};
|
||||
struct Metrics {
|
||||
double sum{}, peak{}, delta{};
|
||||
std::uint64_t samples{}, clips{};
|
||||
AudioFrame previous{};
|
||||
void add(AudioFrame f) {
|
||||
for (float x : {f.left, f.right}) {
|
||||
if (!std::isfinite(x))
|
||||
throw std::runtime_error("nonfinite capture");
|
||||
sum += x * x;
|
||||
peak = std::max(peak, double(std::abs(x)));
|
||||
clips += std::abs(x) > 1;
|
||||
++samples;
|
||||
}
|
||||
delta = std::max(
|
||||
{delta, double(std::abs(f.left - previous.left)), double(std::abs(f.right - previous.right))});
|
||||
previous = f;
|
||||
}
|
||||
void print(const char *name) const {
|
||||
std::cout << name << " peak=" << peak << " rms=" << std::sqrt(sum / samples)
|
||||
<< " max_sample_delta=" << delta << " clipping_samples=" << clips << '\n';
|
||||
}
|
||||
};
|
||||
template <class Scenario>
|
||||
void capture(const std::string &prefix, int frames, float pedal, OutdoorScene scene, TwoStrokeConfig config) {
|
||||
std::unique_ptr<Scenario> s;
|
||||
if constexpr (std::is_same_v<Scenario, TwoStrokeScenario>) s = std::make_unique<Scenario>(44100, config);
|
||||
else s = std::make_unique<Scenario>();
|
||||
s->throttle = pedal;
|
||||
s->set_audio_scene(scene);
|
||||
Wav rendered(prefix + "-rendered.wav", frames), dry(prefix + "-dry.wav", frames),
|
||||
raw(prefix + "-unfiltered.wav", frames);
|
||||
Metrics m, d, r;
|
||||
int cuts = 0;
|
||||
for (int i = 0; i < frames; i++) {
|
||||
auto f = s->process(), df = s->dry_audio(), rf = s->unfiltered_audio();
|
||||
m.add(f);
|
||||
d.add(df);
|
||||
r.add(rf);
|
||||
rendered.sample(f);
|
||||
dry.sample(df);
|
||||
raw.sample(rf);
|
||||
if (i % 4410 == 0)
|
||||
cuts += s->snapshot().fuel_cut;
|
||||
}
|
||||
m.print("rendered");
|
||||
d.print("dry filtered");
|
||||
r.print("unfiltered spatial");
|
||||
std::cout << "fuel-cut checkpoints=" << cuts << " final_rpm=" << s->snapshot().rpm
|
||||
<< " scene=" << scene_name(scene) << '\n';
|
||||
}
|
||||
} // namespace
|
||||
int main(int argc, char **argv) {
|
||||
try {
|
||||
std::string scenario = "twin", prefix = "build/audio/twin", scene = "wall";
|
||||
float seconds = 6, pedal = 1;
|
||||
TwoStrokeConfig config;
|
||||
bool turbo_enabled = false;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
std::string key = argv[i];
|
||||
if (i + 1 >= argc)
|
||||
throw std::runtime_error("missing option value");
|
||||
std::string value = argv[++i];
|
||||
if (key == "--scenario")
|
||||
scenario = value;
|
||||
else if (key == "--out")
|
||||
prefix = value;
|
||||
else if (key == "--seconds")
|
||||
seconds = std::stof(value);
|
||||
else if (key == "--throttle")
|
||||
pedal = std::stof(value);
|
||||
else if (key == "--turbo") {
|
||||
if (value != "none" && value != "vz21") throw std::runtime_error("choose none or vz21 turbo");
|
||||
turbo_enabled = value == "vz21";
|
||||
}
|
||||
else if (key == "--exhaust") {
|
||||
if (value == "racing") config.exhaust = TwoStrokeExhaust::Racing;
|
||||
else if (value == "straight") config.exhaust = TwoStrokeExhaust::Straight;
|
||||
else throw std::runtime_error("choose racing or straight exhaust");
|
||||
}
|
||||
else if (key == "--scene")
|
||||
scene = value;
|
||||
else
|
||||
throw std::runtime_error("unknown capture option");
|
||||
}
|
||||
if (!std::isfinite(seconds) || seconds <= 0 || seconds > 600 || !std::isfinite(pedal) || pedal < 0 ||
|
||||
pedal > 1)
|
||||
throw std::runtime_error("invalid capture duration or throttle");
|
||||
OutdoorScene selected;
|
||||
if (scene == "dry")
|
||||
selected = OutdoorScene::Dry;
|
||||
else if (scene == "field")
|
||||
selected = OutdoorScene::Field;
|
||||
else if (scene == "wall")
|
||||
selected = OutdoorScene::Wall;
|
||||
else if (scene == "woodland")
|
||||
selected = OutdoorScene::Woodland;
|
||||
else
|
||||
throw std::runtime_error("invalid scene");
|
||||
if (!std::filesystem::path(prefix).parent_path().empty())
|
||||
std::filesystem::create_directories(std::filesystem::path(prefix).parent_path());
|
||||
const int frames = int(std::lround(seconds * 44100));
|
||||
if (turbo_enabled && scenario != "twin") throw std::runtime_error("turbo test uses the twin scenario");
|
||||
if (scenario == "twin" && turbo_enabled)
|
||||
capture<TurboTwinScenario>(prefix, frames, pedal, selected, config);
|
||||
else if (scenario == "twin")
|
||||
capture<FourStrokeTwinScenario>(prefix, frames, pedal, selected, config);
|
||||
else if (scenario == "b205")
|
||||
capture<SaabB205Scenario>(prefix, frames, pedal, selected, config);
|
||||
else if (scenario == "two-stroke")
|
||||
capture<TwoStrokeScenario>(prefix, frames, pedal, selected, config);
|
||||
else
|
||||
throw std::runtime_error("choose twin, two-stroke or b205");
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << e.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
62
tools/physics_benchmark.cpp
Normal file
62
tools/physics_benchmark.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "FluidSim/PulseTestScenario.hpp"
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
using namespace fluidsim::port;
|
||||
int main(int argc, char **argv) {
|
||||
// Track the hot-engine cost over time; a one-second benchmark misses it.
|
||||
if (argc > 1 && std::string(argv[1]) == "--soak") {
|
||||
const int seconds = argc > 2 ? std::stoi(argv[2]) : 12;
|
||||
for (float pedal : {0.0f, 1.0f}) {
|
||||
FourStrokeTwinScenario scenario;
|
||||
scenario.throttle = pedal;
|
||||
for (int second = 0; second < seconds; ++second) {
|
||||
const auto before = scenario.snapshot();
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
for (int sample = 0; sample < 44100; ++sample)
|
||||
scenario.process();
|
||||
const double wall =
|
||||
std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
|
||||
const auto after = scenario.snapshot();
|
||||
std::cout << "pedal=" << pedal << " second=" << second + 1 << " wall_seconds=" << wall
|
||||
<< " rpm=" << after.rpm
|
||||
<< " substeps/frame=" << (after.solver_substeps - before.solver_substeps) / 44100.0
|
||||
<< " boundary_iterations/frame="
|
||||
<< (after.boundary_iterations - before.boundary_iterations) / 44100.0 << std::endl;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int samples = argc > 1 ? std::stoi(argv[1]) : 44100;
|
||||
for (int n : {64, 256, 1024}) {
|
||||
Pipe1D p(.001f, 2.f, n);
|
||||
p.damping_multiplier = 0;
|
||||
p.energy_relaxation_rate = 0;
|
||||
double sum = 0;
|
||||
auto t = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < samples; i++) {
|
||||
auto l = p.state(0), r = p.state(n - 1);
|
||||
l.velocity = -l.velocity;
|
||||
r.velocity = -r.velocity;
|
||||
p.left_ghost = l;
|
||||
p.right_ghost = r;
|
||||
p.advance(0.0000005f);
|
||||
sum += p.state(n / 2).pressure;
|
||||
}
|
||||
double sec = std::chrono::duration<double>(std::chrono::steady_clock::now() - t).count();
|
||||
std::cout << "pipe cells=" << n << " steps=" << samples << " seconds=" << sec
|
||||
<< " Mcells/s=" << n * double(samples) / sec / 1e6 << " checksum=" << sum << '\n';
|
||||
}
|
||||
FourStrokeTwinScenario s;
|
||||
s.throttle = .35f;
|
||||
double sum = 0;
|
||||
auto t = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < samples; i++) {
|
||||
auto a = s.process();
|
||||
sum += a.left;
|
||||
}
|
||||
double sec = std::chrono::duration<double>(std::chrono::steady_clock::now() - t).count();
|
||||
std::cout << "twin seconds=" << sec << " realtime_factor=" << samples / 44100.0 / sec
|
||||
<< " checksum=" << sum << '\n';
|
||||
}
|
||||
54
tools/thread_benchmark.cpp
Normal file
54
tools/thread_benchmark.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "FluidSim/Fluid.hpp"
|
||||
#include <barrier>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
using namespace fluidsim::port;
|
||||
int main() {
|
||||
for (auto size : {std::pair{6, 10}, std::pair{64, 256}}) {
|
||||
const int steps = 1000;
|
||||
for (int workers : {1, 2, 4}) {
|
||||
std::vector<Pipe1D> p;
|
||||
for (int i = 0; i < size.first; i++)
|
||||
p.emplace_back(.001f, 2, size.second);
|
||||
auto advance = [&](int worker) {
|
||||
for (int k = worker; k < size.first; k += workers) {
|
||||
auto &a = p[k];
|
||||
auto l = a.state(0), r = a.state(size.second - 1);
|
||||
l.velocity = -l.velocity;
|
||||
r.velocity = -r.velocity;
|
||||
a.left_ghost = l;
|
||||
a.right_ghost = r;
|
||||
a.advance(.0000005f);
|
||||
}
|
||||
};
|
||||
std::barrier sync(workers + 1);
|
||||
std::vector<std::jthread> pool;
|
||||
if (workers > 1)
|
||||
for (int k = 0; k < workers; k++)
|
||||
pool.emplace_back([&, k] {
|
||||
for (int i = 0; i < steps; i++) {
|
||||
sync.arrive_and_wait();
|
||||
advance(k);
|
||||
sync.arrive_and_wait();
|
||||
}
|
||||
});
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < steps; i++) {
|
||||
if (workers == 1)
|
||||
advance(0);
|
||||
else {
|
||||
sync.arrive_and_wait();
|
||||
sync.arrive_and_wait();
|
||||
}
|
||||
}
|
||||
double sec = std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
|
||||
pool.clear();
|
||||
double sum = 0;
|
||||
for (auto &q : p)
|
||||
sum += q.state(0).pressure;
|
||||
std::cout << "pipes=" << size.first << " cells/pipe=" << size.second << " workers=" << workers
|
||||
<< " steps=" << steps << " seconds=" << sec << " checksum=" << sum << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
48
tools/turbo_benchmark.cpp
Normal file
48
tools/turbo_benchmark.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#include "FluidSim/Scenario.hpp"
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include <ctime>
|
||||
using namespace fluidsim::port;
|
||||
int main() {
|
||||
std::cout << "engine_rpm,turbo,MAP_kPa,exhaust_kPa,shaft_kW,turbo_rpm,charge_kPa,charge_K,turbine_power_ripple_W,gate_mm,limited,energy_residual_J,substeps_per_frame,iterations_per_frame,wall_s,cpu_s\n";
|
||||
for (float rpm : {4000.f, 7000.f}) {
|
||||
for (bool enabled : {false, true}) {
|
||||
FourStrokeScenario s(44100, FourStrokePreset::Twin, enabled);
|
||||
s.throttle = .7f;
|
||||
double map = 0, exhaust = 0, power = 0, turbo_rpm = 0, charge_p = 0, charge_t = 0, gate = 0;
|
||||
double min_power = 1e30, max_power = -1e30;
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const auto cpu_start = std::clock();
|
||||
for (int i = 0; i < 2*44100; ++i) {
|
||||
// Ideal speed-holding dyno. The turbo remains completely free.
|
||||
s.crankshaft.omega = rpm*2*pi/60;
|
||||
s.process();
|
||||
if (i >= 3*44100/2) {
|
||||
constexpr double samples = 22050;
|
||||
map += s.intake_plenum.pressure()/samples/1000;
|
||||
exhaust += s.exhaust_collector.pressure()/samples/1000;
|
||||
power += s.crankshaft.average_power/samples/1000;
|
||||
if (s.turbo) {
|
||||
const auto t = s.turbo->snapshot();
|
||||
turbo_rpm += t.rpm/samples;
|
||||
charge_p += t.charge_pressure/samples/1000;
|
||||
charge_t += t.charge_temperature/samples;
|
||||
gate += t.wastegate_lift/samples*1000;
|
||||
min_power = std::min(min_power, t.turbine_power);
|
||||
max_power = std::max(max_power, t.turbine_power);
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto t = s.turbo ? s.turbo->snapshot() : TurboSnapshot{};
|
||||
const auto state = s.snapshot();
|
||||
const double cpu = double(std::clock()-cpu_start)/CLOCKS_PER_SEC;
|
||||
const auto wall = std::chrono::duration<double>(std::chrono::steady_clock::now()-start).count();
|
||||
std::cout << rpm << ',' << enabled << ',' << map << ',' << exhaust << ',' << power << ','
|
||||
<< turbo_rpm << ',' << charge_p << ',' << charge_t << ','
|
||||
<< (enabled ? max_power-min_power : 0) << ',' << gate << ',' << t.limited_transfers
|
||||
<< ',' << t.energy_residual << ',' << state.solver_substeps/88200.0 << ','
|
||||
<< state.boundary_iterations/88200.0 << ',' << wall << ',' << cpu << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
tools/two_stroke_benchmark.cpp
Normal file
37
tools/two_stroke_benchmark.cpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "FluidSim/TwoStrokeScenario.hpp"
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
|
||||
using namespace fluidsim::port;
|
||||
int main() {
|
||||
std::cout << "exhaust,rpm,shaft_kW,shaft_Nm,firings_per_revolution,repairs,wall_seconds\n";
|
||||
for (auto exhaust : {TwoStrokeExhaust::Straight, TwoStrokeExhaust::Racing}) {
|
||||
for (float rpm : {6000.f, 8000.f, 10000.f, 12000.f, 14000.f, 17000.f}) {
|
||||
TwoStrokeConfig config;
|
||||
config.exhaust = exhaust;
|
||||
TwoStrokeScenario s(44100, config);
|
||||
s.set_throttle(1);
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
double torque = 0, power = 0;
|
||||
std::uint64_t initial_firings = 0;
|
||||
// An ideal absorbing/motoring dyno imposes speed, exchanging shaft
|
||||
// work externally. It never changes gas energy or adds combustion.
|
||||
for (int i = 0; i < 44100; ++i) {
|
||||
s.crankshaft.omega = rpm * 2 * pi / 60;
|
||||
s.process();
|
||||
if (i == 22049) initial_firings = s.cylinder.combustion_count();
|
||||
if (i >= 22050) {
|
||||
torque += s.crankshaft.average_torque / 22050;
|
||||
power += s.crankshaft.average_power / (22050 * 1000.f);
|
||||
}
|
||||
}
|
||||
unsigned long long repairs = 0;
|
||||
for (const auto& pipe : s.pipes) repairs += pipe.positivity_repairs;
|
||||
const double wall = std::chrono::duration<double>(std::chrono::steady_clock::now()-start).count();
|
||||
std::cout << (exhaust == TwoStrokeExhaust::Racing ? "racing" : "straight") << ',' << rpm
|
||||
<< ',' << power << ',' << torque << ','
|
||||
<< (s.cylinder.combustion_count()-initial_firings) / (rpm / 120)
|
||||
<< ',' << repairs << ',' << wall << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user