Skip to content

Commit b9c498e

Browse files
committed
Improve radiation damping convolution integral function
- include Savitzky–Golay smoothing - include tapering options to ensure RIRF convergence - expose tuning parameters to the user via hydro YAML input file - include option to save selected RIRFs to csv & F3OF dt3 RIRF plotting scripts
1 parent 845c91b commit b9c498e

8 files changed

Lines changed: 466 additions & 2 deletions

File tree

include/hydroc/hydro_forces.h

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,42 @@ class TestHydro {
230230
*/
231231
double GetRIRFval(int row, int col, int st);
232232

233+
// Convolution mode selection
234+
enum class RadiationConvolutionMode {
235+
Baseline,
236+
TaperedDirect
237+
};
238+
239+
/**
240+
* @brief Set the radiation convolution mode. Default is Baseline.
241+
*/
242+
void SetRadiationConvolutionMode(RadiationConvolutionMode mode) { convolution_mode_ = mode; }
243+
244+
struct TaperedDirectOptions {
245+
// smoothing: "sg" (Savitzky–Golay) or "moving_average"
246+
std::string smoothing = "sg";
247+
int window_length = 5; // odd, >= 3
248+
249+
// RIRF truncation
250+
double rirf_end_time = -1.0; // end RIRF at this time (seconds), -1.0 = use full length
251+
252+
// Simple taper control - sensible defaults for improved stability
253+
double taper_start_percent = 0.8; // start taper at 80% (taper last 20%)
254+
double taper_end_percent = 1.0; // end taper at 100% of total time series
255+
double taper_final_amplitude = 0.0; // final amplitude as fraction of original (0.0 = zero, 1.0 = no change)
256+
bool export_plot_csv = false; // dump before/after CSV summaries (false by default)
257+
};
258+
259+
/**
260+
* @brief Set options for TaperedDirect preprocessing.
261+
*/
262+
void SetTaperedDirectOptions(const TaperedDirectOptions& opts) { tapered_opts_ = opts; }
263+
264+
/**
265+
* @brief Set the directory where diagnostics (e.g., CSVs) should be written.
266+
*/
267+
void SetDiagnosticsOutputDirectory(const std::string& dir) { diagnostics_output_dir_ = dir; }
268+
233269
/**
234270
* @brief Calculates or retrieves the total force on a specific body in a particular degree of freedom.
235271
*
@@ -297,6 +333,15 @@ class TestHydro {
297333

298334
// Hydrodynamics profiling data (accumulated over run)
299335
HydroProfileStats profile_stats_;
336+
337+
// Convolution kernel preprocessing (optional)
338+
RadiationConvolutionMode convolution_mode_ = RadiationConvolutionMode::Baseline;
339+
bool rirf_processed_ready_ = false;
340+
std::vector<Eigen::Tensor<double, 3>> rirf_processed_; // per body [dof x col x step]
341+
TaperedDirectOptions tapered_opts_;
342+
std::string diagnostics_output_dir_;
343+
344+
void EnsureProcessedRIRF();
300345
};
301346

302347
#endif

src/hydro_forces.cpp

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <hydroc/chloadaddedmass.h>
1111
#include <hydroc/h5fileinfo.h>
1212
#include <hydroc/wave_types.h>
13+
#include <hydroc/logging.h>
1314

1415
#include <chrono/physics/ChLoad.h>
1516
#include <unsupported/Eigen/Splines>
@@ -25,6 +26,7 @@
2526
#include <stdexcept>
2627
#include <vector>
2728
#include <chrono>
29+
#include <filesystem>
2830

2931
#ifdef _OPENMP
3032
#include <omp.h>
@@ -376,13 +378,171 @@ inline bool AdvanceToBracket(const std::vector<double>& time_history,
376378
}
377379
} // namespace
378380

381+
// Preprocess the radiation kernel K(t) per body for TaperedDirect mode.
382+
void TestHydro::EnsureProcessedRIRF() {
383+
if (rirf_processed_ready_) {
384+
return;
385+
}
386+
387+
const int steps = file_info_.GetRIRFDims(2);
388+
const int cols = kDofPerBody * num_bodies_;
389+
const int rows = kDofPerBody;
390+
391+
rirf_processed_.clear();
392+
rirf_processed_.resize(num_bodies_);
393+
394+
// SG smoothing coefficients for 5-point quadratic: [-3, 12, 17, 12, -3] / 35
395+
const double sg5[5] = { -3.0 / 35.0, 12.0 / 35.0, 17.0 / 35.0, 12.0 / 35.0, -3.0 / 35.0 };
396+
397+
for (int b = 0; b < num_bodies_; ++b) {
398+
Eigen::Tensor<double, 3> processed(rows, cols, steps);
399+
400+
// Calculate effective steps for this body (same for all channels)
401+
int effective_steps = steps;
402+
if (tapered_opts_.rirf_end_time > 0.0) {
403+
// Calculate the step index corresponding to the end time
404+
double dt = rirf_time_vector[1] - rirf_time_vector[0]; // assume uniform time step
405+
int end_step = static_cast<int>(std::floor(tapered_opts_.rirf_end_time / dt));
406+
effective_steps = std::min(end_step, steps);
407+
}
408+
409+
// Diagnostic aggregation across channels
410+
int max_tc_index = 0;
411+
int max_taper_len = 0;
412+
int max_effective_len = steps;
413+
414+
for (int row_dof = 0; row_dof < rows; ++row_dof) {
415+
for (int col = 0; col < cols; ++col) {
416+
// Load raw (rho-scaled) kernel time series
417+
std::vector<double> k_raw(steps);
418+
for (int s = 0; s < steps; ++s) {
419+
k_raw[s] = file_info_.GetRIRFVal(b, row_dof, col, s);
420+
}
421+
422+
// Apply RIRF truncation if specified
423+
if (tapered_opts_.rirf_end_time > 0.0) {
424+
// Truncate the raw kernel
425+
k_raw.resize(effective_steps);
426+
}
427+
428+
// Light smoothing
429+
std::vector<double> k_smooth(effective_steps);
430+
if (tapered_opts_.smoothing == "moving_average") {
431+
const int w = std::max(3, tapered_opts_.window_length);
432+
const int half = w / 2;
433+
for (int s = 0; s < effective_steps; ++s) {
434+
int a = std::max(0, s - half);
435+
int b = std::min(effective_steps - 1, s + half);
436+
double sum = 0.0; int cnt = 0;
437+
for (int i = a; i <= b; ++i) { sum += k_raw[i]; ++cnt; }
438+
k_smooth[s] = (cnt > 0) ? (sum / cnt) : k_raw[s];
439+
}
440+
} else {
441+
// default: SG quadratic with window 5
442+
if (effective_steps >= 5) {
443+
// copy edges as-is for simplicity
444+
k_smooth[0] = k_raw[0];
445+
k_smooth[1] = k_raw[1];
446+
for (int s = 2; s <= effective_steps - 3; ++s) {
447+
k_smooth[s] = sg5[0] * k_raw[s - 2] + sg5[1] * k_raw[s - 1] + sg5[2] * k_raw[s] + sg5[3] * k_raw[s + 1] + sg5[4] * k_raw[s + 2];
448+
}
449+
k_smooth[effective_steps - 2] = k_raw[effective_steps - 2];
450+
k_smooth[effective_steps - 1] = k_raw[effective_steps - 1];
451+
} else {
452+
k_smooth = k_raw; // too short to smooth
453+
}
454+
}
455+
456+
// Simple taper control: start and end percentages
457+
int tc_index = static_cast<int>(std::floor(tapered_opts_.taper_start_percent * static_cast<double>(effective_steps)));
458+
int tc_end = static_cast<int>(std::floor(tapered_opts_.taper_end_percent * static_cast<double>(effective_steps)));
459+
tc_index = std::max(0, std::min(tc_index, effective_steps));
460+
tc_end = std::max(tc_index, std::min(tc_end, effective_steps));
461+
462+
// Apply half-cosine taper from start to end percentage
463+
int taper_len = tc_end - tc_index;
464+
int effective_len = tc_end;
465+
466+
const double pi_const = 3.14159265358979323846;
467+
for (int s = 0; s < effective_steps; ++s) {
468+
double val = k_smooth[s];
469+
if (s < tc_index) {
470+
// keep original value
471+
} else if (s < tc_end && taper_len > 0) {
472+
double t = (static_cast<double>(s - tc_index)) / static_cast<double>(taper_len);
473+
// Half-cosine taper: goes from 1.0 to taper_final_amplitude
474+
// taper_final_amplitude = 0.0 means complete zero, 1.0 means no change
475+
double w = tapered_opts_.taper_final_amplitude + (1.0 - tapered_opts_.taper_final_amplitude) * 0.5 * (1.0 + std::cos(pi_const * t));
476+
val *= w;
477+
} else {
478+
val = 0.0;
479+
}
480+
processed(row_dof, col, s) = val;
481+
}
482+
483+
// Zero out any remaining steps beyond effective_steps
484+
for (int s = effective_steps; s < steps; ++s) {
485+
processed(row_dof, col, s) = 0.0;
486+
}
487+
488+
if (tc_index > max_tc_index) max_tc_index = tc_index;
489+
if (taper_len > max_taper_len) max_taper_len = taper_len;
490+
if (effective_len > max_effective_len) max_effective_len = effective_len;
491+
}
492+
}
493+
494+
rirf_processed_[b] = std::move(processed);
495+
496+
LOG_DEBUG("TaperedDirect kernel (body " << b
497+
<< ") Start: " << tapered_opts_.taper_start_percent
498+
<< ", End: " << tapered_opts_.taper_end_percent
499+
<< ", Final Amp: " << tapered_opts_.taper_final_amplitude
500+
<< ", RIRF End Time: " << (tapered_opts_.rirf_end_time > 0.0 ? std::to_string(tapered_opts_.rirf_end_time) + "s" : "full")
501+
<< ", Max Tc index: " << max_tc_index
502+
<< ", Max taper length: " << max_taper_len
503+
<< ", Max effective length: " << max_effective_len
504+
<< "/" << steps);
505+
506+
if (tapered_opts_.export_plot_csv) {
507+
// Write small CSV summaries for inspection: times, representative channel before/after
508+
try {
509+
const std::string base = std::string("rirf_body") + std::to_string(b) + std::string("_summary.csv");
510+
std::filesystem::path out_dir = diagnostics_output_dir_.empty() ? std::filesystem::current_path() : std::filesystem::path(diagnostics_output_dir_);
511+
std::filesystem::path out_path = out_dir / base;
512+
std::ofstream ofs(out_path.string());
513+
ofs << "step,time,k_before,k_after\n";
514+
// pick row=0,col=0 as representative
515+
for (int s = 0; s < effective_steps; ++s) {
516+
double t = (s < rirf_time_vector.size()) ? rirf_time_vector[s] : static_cast<double>(s);
517+
double before = file_info_.GetRIRFVal(b, 0, 0, s);
518+
double after = rirf_processed_[b](0, 0, s);
519+
ofs << s << "," << t << "," << before << "," << after << "\n";
520+
}
521+
// Only log the directory once (body 0)
522+
if (b == 0) {
523+
LOG_INFO("RIRF CSVs written in " << out_dir.string());
524+
}
525+
} catch (...) {
526+
// ignore export errors
527+
}
528+
}
529+
}
530+
531+
rirf_processed_ready_ = true;
532+
}
533+
379534
std::vector<double> TestHydro::ComputeForceRadiationDampingConv() {
380535
auto __t0 = std::chrono::steady_clock::now();
381536
const int rirf_steps = file_info_.GetRIRFDims(2);
382537
const int total_dofs = kDofPerBody * num_bodies_;
383538

384539
assert(total_dofs > 0 && rirf_steps > 0);
385540

541+
// If using TaperedDirect, ensure processed kernel is ready before any parallel region
542+
if (convolution_mode_ == RadiationConvolutionMode::TaperedDirect) {
543+
EnsureProcessedRIRF();
544+
}
545+
386546
// Current time and minimum history time window required
387547
const double simulation_time = bodies_[0]->GetChTime();
388548
const int rirf_last_index = static_cast<int>(rirf_time_vector.size()) - 1;
@@ -537,6 +697,13 @@ double TestHydro::GetRIRFval(int row, int col, int st) {
537697
int col_dof = col % kDofPerBody;
538698
int row_dof = row % kDofPerBody;
539699

700+
if (convolution_mode_ == RadiationConvolutionMode::TaperedDirect) {
701+
EnsureProcessedRIRF();
702+
// processed tensor is scaled by rho already
703+
const auto& tensor = rirf_processed_[body_index];
704+
return tensor(row_dof, col, st);
705+
}
706+
540707
return file_info_.GetRIRFVal(body_index, row_dof, col, st);
541708
}
542709

src/hydro_types.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ struct HydroBody {
2222
bool include_excitation = true;
2323
bool include_radiation = true;
2424
std::string radiation_calculation = "convolution"; // "convolution" or "state_space"
25+
// Optional convolution mode for radiation kernel preprocessing: "Baseline" (default) or "TaperedDirect"
26+
std::string radiation_convolution_mode = "Baseline";
27+
// Optional TaperedDirect tuning
28+
std::string td_smoothing = "sg"; // "sg" or "moving_average"
29+
int td_window_length = 5; // odd >= 3
30+
double td_rms_threshold_factor = 0.02; // e.g. 0.02
31+
double td_taper_fraction_remaining = 0.25; // e.g. 0.25
32+
bool td_export_plot_csv = false; // export CSV for before/after
2533
// TODO: Add nonlinear buoyancy fields
2634
// TODO: Add drag coefficient fields
2735
};
@@ -48,6 +56,19 @@ struct WaveSettings {
4856
struct YAMLHydroData {
4957
std::vector<HydroBody> bodies;
5058
WaveSettings waves;
59+
// Optional system-wide convolution settings
60+
std::string radiation_convolution_mode = "Baseline"; // Baseline | TaperedDirect
61+
std::string td_smoothing = "sg";
62+
int td_window_length = 5;
63+
64+
// RIRF truncation
65+
double td_rirf_end_time = -1.0;
66+
67+
// Simple taper control - sensible defaults for improved stability
68+
double td_taper_start_percent = 0.8; // start taper at 80% (taper last 20%)
69+
double td_taper_end_percent = 1.0; // end taper at 100% of total time series
70+
double td_taper_final_amplitude = 0.0; // final amplitude as fraction of original (0.0 = zero, 1.0 = no change)
71+
bool td_export_plot_csv = false; // dump before/after CSV summaries (false by default)
5172
};
5273

5374
#endif // HYDRO_TYPES_H

0 commit comments

Comments
 (0)