SyntheticRealComparison.java
package org.hammer.audio.experimental.acoustic.feature.comparison;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.ToDoubleFunction;
import org.hammer.audio.experimental.acoustic.wingbeat.WingbeatFeatureVector;
/**
* Compares feature distributions between a synthetic and a real corpus of {@link
* WingbeatFeatureVector}s.
*
* <p>For every scalar feature the service computes the per-corpus mean and standard deviation and
* derives absolute difference, relative difference and a z-score. Features covering dominant
* frequency, harmonics, duration, SNR and modulation are compared directly from the available
* {@link WingbeatFeatureVector} fields.
*
* <p>This service is stateless and may be called concurrently.
*/
public final class SyntheticRealComparison {
private static final List<Map.Entry<String, ToDoubleFunction<WingbeatFeatureVector>>> EXTRACTORS =
buildExtractors();
private static List<Map.Entry<String, ToDoubleFunction<WingbeatFeatureVector>>>
buildExtractors() {
List<Map.Entry<String, ToDoubleFunction<WingbeatFeatureVector>>> list = new ArrayList<>();
list.add(Map.entry("fundamentalFrequencyHz", WingbeatFeatureVector::fundamentalFrequencyHz));
list.add(
Map.entry(
"harmonicAmplitude1",
v -> v.harmonicAmplitudes().isEmpty() ? 0.0 : v.harmonicAmplitudes().get(0)));
list.add(
Map.entry(
"harmonicRatio1", v -> v.harmonicRatios().isEmpty() ? 0.0 : v.harmonicRatios().get(0)));
list.add(Map.entry("trackDurationSeconds", WingbeatFeatureVector::trackDurationSeconds));
list.add(Map.entry("signalToNoiseRatio", WingbeatFeatureVector::signalToNoiseRatio));
list.add(Map.entry("amplitudeModulation", WingbeatFeatureVector::amplitudeModulation));
list.add(Map.entry("spectralCentroidHz", WingbeatFeatureVector::spectralCentroidHz));
list.add(Map.entry("spectralBandwidthHz", WingbeatFeatureVector::spectralBandwidthHz));
return List.copyOf(list);
}
/**
* Compare synthetic and real corpora using the {@link
* SyntheticRealComparisonReport#DEFAULT_WEAKNESS_THRESHOLD default weakness threshold}.
*
* @param synthetic synthetic corpus; must not be {@code null} or empty
* @param real real corpus; must not be {@code null} or empty
* @return comparison report; never {@code null}
*/
public SyntheticRealComparisonReport compare(
List<WingbeatFeatureVector> synthetic, List<WingbeatFeatureVector> real) {
return compare(synthetic, real, SyntheticRealComparisonReport.DEFAULT_WEAKNESS_THRESHOLD);
}
/**
* Compare synthetic and real corpora with a configurable weakness threshold.
*
* @param synthetic synthetic corpus; must not be {@code null} or empty
* @param real real corpus; must not be {@code null} or empty
* @param weaknessThreshold relative-difference threshold for flagging weaknesses; must be {@code
* > 0}
* @return comparison report; never {@code null}
*/
public SyntheticRealComparisonReport compare(
List<WingbeatFeatureVector> synthetic,
List<WingbeatFeatureVector> real,
double weaknessThreshold) {
Objects.requireNonNull(synthetic, "synthetic");
Objects.requireNonNull(real, "real");
if (synthetic.isEmpty()) {
throw new IllegalArgumentException("synthetic corpus must not be empty");
}
if (real.isEmpty()) {
throw new IllegalArgumentException("real corpus must not be empty");
}
List<FeatureDifference> differences = new ArrayList<>(EXTRACTORS.size());
for (Map.Entry<String, ToDoubleFunction<WingbeatFeatureVector>> fe : EXTRACTORS) {
double[] synthValues = extract(synthetic, fe.getValue());
double[] realValues = extract(real, fe.getValue());
differences.add(computeDifference(fe.getKey(), synthValues, realValues));
}
return new SyntheticRealComparisonReport(differences, weaknessThreshold);
}
private static double[] extract(
List<WingbeatFeatureVector> vectors, ToDoubleFunction<WingbeatFeatureVector> extractor) {
double[] values = new double[vectors.size()];
for (int i = 0; i < vectors.size(); i++) {
values[i] = extractor.applyAsDouble(vectors.get(i));
}
return values;
}
private static FeatureDifference computeDifference(
String name, double[] synthValues, double[] realValues) {
double synthMean = mean(synthValues);
double realMean = mean(realValues);
double synthStd = stdDev(synthValues, synthMean);
double absDiff = Math.abs(realMean - synthMean);
double relDiff = synthMean == 0.0 ? 0.0 : absDiff / Math.abs(synthMean);
double zScore = synthStd == 0.0 ? 0.0 : (realMean - synthMean) / synthStd;
return new FeatureDifference(name, synthMean, realMean, absDiff, relDiff, zScore);
}
private static double mean(double[] values) {
double sum = 0.0;
for (double v : values) {
sum += v;
}
return sum / values.length;
}
private static double stdDev(double[] values, double mean) {
double variance = 0.0;
for (double v : values) {
double diff = v - mean;
variance += diff * diff;
}
return Math.sqrt(variance / values.length);
}
}