Correct primitive source and radiation timeline laws

This commit is contained in:
Meta Vens 2026-05-29 16:58:13 +01:00
commit 073c4873e0
10 changed files with 41 additions and 13 deletions

View file

@ -243,11 +243,15 @@ PT/SndKit comparison report:
- `PinkTrombonePrimitiveTimelineComparisonWritesArtifacts` writes per-fixture
Aqua timelines, PT internal timelines, comparison CSVs, and a summary under
`artifacts/parity/primitive-vocal-timeline`.
- The first comparison-driven correction demoted hidden source unit gain into
explicit `SourcePort.FlowScale`, stopped aperture from rewriting
`RadiationLoad.Reflection`, and fixed `ProbeTimelineReport` radiation flow to
observe path wave flow rather than `aperture / impedance` alone.
The next implementation pass should harden the primitives further:
- use the artifact reports to choose the first physical-law correction in the
primitive machine;
- decide where gesture slew lives: inside primitive ports or in a separate
gesture/timeline owner;
- add optional VTL/ArtiSynth adapters only as artifact comparators, not default
CI dependencies;
- keep `mama`, `papa`, and `thrombosis` as downstream witnesses until primitive

View file

@ -598,10 +598,11 @@ public static class FaustEmitter
var tension = parameters.Expression(OwnerField(owner, "tension"), port.Tension);
var opening = parameters.Expression(OwnerField(owner, "opening"), port.Opening);
var impedance = parameters.Expression(OwnerField(owner, "impedance"), port.Impedance);
var flowScale = parameters.Expression(OwnerField(owner, "flow_scale"), port.FlowScale);
var load = $"{voiceName}_primitive_source_{safe}_load_pressure";
var flow = $"{voiceName}_primitive_source_{safe}_flow";
source.AppendLine($" {load} = {ProbeSignal(options, $"/debug/{voiceName}/source/{port.Name}/load_pressure", $"({impedance}) * ({pressure})", 0, 2)};");
source.AppendLine($" {flow} = {ProbeSignal(options, $"/debug/{voiceName}/source/{port.Name}/flow", $"ma.tanh((({pressure}) * max(0.0, {opening}) * (0.5 + {tension}) + 0.08 * no.noise * {parameters.Expression(OwnerField(owner, "noise"), port.Noise)}) / max(0.05, {impedance}))", -2, 2)};");
source.AppendLine($" {flow} = {ProbeSignal(options, $"/debug/{voiceName}/source/{port.Name}/flow", $"({flowScale}) * ma.tanh((({pressure}) * max(0.0, {opening}) * (0.5 + {tension}) + 0.08 * no.noise * {parameters.Expression(OwnerField(owner, "noise"), port.Noise)}) / max(0.05, {impedance}))", -2, 2)};");
keepAlive.Add(load);
keepAlive.Add(flow);
return flow;
@ -671,7 +672,7 @@ public static class FaustEmitter
var boundary = $"{voiceName}_primitive_radiation_{safe}_boundary_flow";
var flow = $"{voiceName}_primitive_radiation_{safe}_flow";
var output = $"{voiceName}_primitive_radiation_{safe}_output";
source.AppendLine($" {voiceName}_primitive_radiation_{safe}_reflection = {ProbeSignal(options, $"/debug/{voiceName}/radiation/{load.Name}/reflection", $"({reflection}) * (1.0 - 0.65 * {aperture})", -1, 1)};");
source.AppendLine($" {voiceName}_primitive_radiation_{safe}_reflection = {ProbeSignal(options, $"/debug/{voiceName}/radiation/{load.Name}/reflection", reflection, -1, 1)};");
source.AppendLine($" {boundary} = {ProbeSignal(options, $"/debug/{voiceName}/radiation/{load.Name}/boundary_flow", $"({wave}) * {aperture} / max(0.05, {impedance})", -4, 4)};");
source.AppendLine($" {flow} = {ProbeSignal(options, $"/debug/{voiceName}/radiation/{load.Name}/flow", boundary, -4, 4)};");
source.AppendLine($" {output} = {ProbeSignal(options, $"/debug/{voiceName}/radiation/{load.Name}/output", $"{flow} : fi.highpass(1, 40.0 + 500.0 * {aperture})", -4, 4)};");

View file

@ -466,7 +466,8 @@ public sealed record SourcePort(
float Tension = 0.6f,
float Opening = 0.5f,
float Noise = 0.08f,
float Impedance = 0.35f);
float Impedance = 0.35f,
float FlowScale = 0.02f);
public sealed record ConstrictionContact(
string Name,

View file

@ -781,7 +781,8 @@ public static class PatchScript
GetBoundFloat(fields, line, 0.6f, $"/vocal/sources/{_sourcePorts.Count}/tension", "tension", "tenseness", "tense"),
GetBoundFloat(fields, line, 0.5f, $"/vocal/sources/{_sourcePorts.Count}/opening", "opening", "open"),
GetBoundFloat(fields, line, 0.08f, $"/vocal/sources/{_sourcePorts.Count}/noise", "noise", "aspiration"),
GetBoundFloat(fields, line, 0.35f, $"/vocal/sources/{_sourcePorts.Count}/impedance", "impedance", "load", "source_impedance"));
GetBoundFloat(fields, line, 0.35f, $"/vocal/sources/{_sourcePorts.Count}/impedance", "impedance", "load", "source_impedance"),
GetBoundFloat(fields, line, 0.02f, $"/vocal/sources/{_sourcePorts.Count}/flow_scale", "flow_scale", "scale"));
_sourcePorts.Add(port);
_sourcePortsByName[name] = port;
}
@ -1692,7 +1693,8 @@ public static class PatchScript
tract.Tenseness,
tract.Glottis?.Skew ?? 0.42f,
tract.Glottis?.Aspiration ?? 0.08f,
0.10f);
0.10f,
0.02f);
if (!_sourcePortsByName.ContainsKey(source.Name))
{
_sourcePorts.Add(source);

View file

@ -20,6 +20,7 @@ public static class ProbeTimelineReport
for (var block = 0; block < Math.Max(1, blocks); block++)
{
var outgoingByPath = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
foreach (var pathName in network.Paths)
{
var path = paths[pathName];
@ -28,6 +29,7 @@ public static class ProbeTimelineReport
var delay = area.Shape.LengthMeters / Math.Max(1, path.PropagationSpeedMetersPerSecond) * 48000;
var incoming = PathIncomingFlow(network, sources, contacts, branches, path.Name);
var outgoing = incoming * Math.Clamp(path.Loss, 0, 1);
outgoingByPath[path.Name] = outgoing;
var energyIn = pathArea * incoming * incoming;
var energyOut = pathArea * outgoing * outgoing;
Add(samples, block, $"path:{path.Name}", "area", pathArea);
@ -43,8 +45,9 @@ public static class ProbeTimelineReport
foreach (var sourceName in network.Sources)
{
var source = sources[sourceName];
var flow = MathF.Tanh(source.Pressure * Math.Max(0, source.Opening) * (0.5f + source.Tension) / Math.Max(0.05f, source.Impedance));
var flow = SourceFlow(source);
Add(samples, block, $"source:{source.Name}", "load_pressure", source.Pressure * source.Impedance);
Add(samples, block, $"source:{source.Name}", "flow_scale", source.FlowScale);
Add(samples, block, $"source:{source.Name}", "flow", flow);
}
@ -71,8 +74,9 @@ public static class ProbeTimelineReport
{
var load = radiation[loadName];
var aperture = Math.Clamp(load.Aperture, 0, 1);
var flow = aperture / Math.Max(0.05f, load.Impedance);
Add(samples, block, $"radiation:{load.Name}", "reflection", load.Reflection * (1 - 0.65f * aperture));
var pathWave = outgoingByPath.GetValueOrDefault(load.Path, 0);
var flow = pathWave * aperture / Math.Max(0.05f, load.Impedance);
Add(samples, block, $"radiation:{load.Name}", "reflection", load.Reflection);
Add(samples, block, $"radiation:{load.Name}", "boundary_flow", flow);
Add(samples, block, $"radiation:{load.Name}", "flow", flow);
Add(samples, block, $"radiation:{load.Name}", "output", flow);
@ -113,7 +117,7 @@ public static class ProbeTimelineReport
var sourceFlow = network.Sources
.Select(name => sources[name])
.Where(source => source.Path.Equals(pathName, StringComparison.OrdinalIgnoreCase))
.Sum(source => MathF.Tanh(source.Pressure * Math.Max(0, source.Opening) * (0.5f + source.Tension) / Math.Max(0.05f, source.Impedance)));
.Sum(SourceFlow);
var contactFlow = network.Contacts
.Select(name => contacts[name])
.Where(contact => contact.Path.Equals(pathName, StringComparison.OrdinalIgnoreCase))
@ -130,6 +134,9 @@ public static class ProbeTimelineReport
return sourceFlow + contactFlow + branchFlow;
}
private static float SourceFlow(SourcePort source) =>
source.FlowScale * MathF.Tanh(source.Pressure * Math.Max(0, source.Opening) * (0.5f + source.Tension) / Math.Max(0.05f, source.Impedance));
private static string Escape(string value) =>
value.Contains(',') || value.Contains('"') ? $"\"{value.Replace("\"", "\"\"", StringComparison.Ordinal)}\"" : value;
}

View file

@ -230,3 +230,4 @@
{"date":"2026-05-29","area":"primitive-vocal-telemetry","summary":"Primitive timelines now expose path incoming/outgoing wave variables, area-weighted energy in/out, passivity ratio, and contact resistance. Faust debug probes expose matching path wave/passivity fields. `PrimitiveReferenceReport.ComparePinkTrombone` adds a PT/SndKit-facing static comparison report for area sections/length/reflection count, delay estimate, branch admittance, contact state, and radiation fields. PatchScriptTests pass 86/86.","evidence":"src/AquaSynth.Core/FaustExport.cs; src/AquaSynth.Core/ProbeTimelineReport.cs; src/AquaSynth.Core/PrimitiveReferenceReport.cs; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; docs/acoustic-graph-proprioception.md"}
{"date":"2026-05-29","area":"primitive-vocal-reference-timeline","summary":"PT/SndKit local reference internals are now observable through `PinkTromboneReferenceRenderer.RenderTimeline`, exposing oral/nasal path waves and energy, velopharynx branch reflections/admittance, obstruction contact state, and lip radiation flow/output from the renderer's own arrays. `PrimitiveReferenceReport.ComparePinkTromboneTimeline` role-normalizes Aqua primitive timeline samples against PT timeline samples. PatchScriptTests pass 88/88.","evidence":"src/AquaSynth.Core/PinkTromboneReferenceRenderer.cs; src/AquaSynth.Core/PrimitiveReferenceReport.cs; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; docs/acoustic-graph-proprioception.md"}
{"date":"2026-05-29","area":"primitive-vocal-artifacts","summary":"Added a PT fixture-suite primitive timeline artifact test. Each fixture writes Aqua primitive timeline CSV, PT internal timeline CSV, role-normalized comparison CSV, and a summary under `artifacts/parity/primitive-vocal-timeline/<timestamp>`. The focused artifact test passes without native Faust or external reference binaries.","evidence":"tests/AquaSynth.Dsl.Tests/PinkTromboneLogMelParityTests.cs; src/AquaSynth.Core/PinkTromboneReferenceRenderer.cs; src/AquaSynth.Core/PrimitiveReferenceReport.cs; docs/acoustic-graph-proprioception.md"}
{"date":"2026-05-29","area":"primitive-vocal-law-correction","summary":"Used primitive timeline artifacts to make the first non-fork physical-law correction. `SourcePort` now owns `FlowScale` defaulting to `.02`; primitive radiation reflection now uses declared `RadiationLoad.Reflection` directly; `ProbeTimelineReport` radiation flow now depends on outgoing path wave like Faust. The focused primitive/artifact tests and PatchScriptTests pass. New artifact `artifacts/parity/primitive-vocal-timeline/20260529T155613895` reduces fixture-suite mean absolute timeline error from roughly `1.2-1.3` to `0.15-0.27`. Remaining drift points to branch/contact motion ownership.","evidence":"src/AquaSynth.Core/Model.cs; src/AquaSynth.Core/PatchScript.cs; src/AquaSynth.Core/FaustExport.cs; src/AquaSynth.Core/ProbeTimelineReport.cs; artifacts/parity/primitive-vocal-timeline/20260529T155613895; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; tests/AquaSynth.Dsl.Tests/PinkTromboneLogMelParityTests.cs"}

View file

@ -121,7 +121,7 @@
},
"last_verified": {
"command": "dotnet test tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-restore --disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false --filter \"FullyQualifiedName~PatchScriptTests\" -v minimal",
"result": "Primitive vocal telemetry/reference cut passes PatchScriptTests 88/88 and the focused PT primitive timeline artifact test. Path timelines now include wave/passivity fields, PT/SndKit static primitive comparison is covered, the local PT renderer exposes internal primitive timeline samples, and per-fixture comparison artifacts are written under artifacts/parity/primitive-vocal-timeline. Earlier full unfiltered run hit the five-minute wall after leaving orphan dotnet workers; focused suites are the current reliable verification path.",
"result": "Primitive vocal telemetry/reference cut passes PatchScriptTests 88/88 and the focused PT primitive timeline artifact test. Path timelines now include wave/passivity fields, PT/SndKit static primitive comparison is covered, the local PT renderer exposes internal primitive timeline samples, and per-fixture comparison artifacts are written under artifacts/parity/primitive-vocal-timeline. First comparison-driven law correction added SourcePort.FlowScale, direct RadiationLoad.Reflection ownership, and wave-dependent timeline radiation flow. Earlier full unfiltered run hit the five-minute wall after leaving orphan dotnet workers; focused suites are the current reliable verification path.",
"commit": "pending primitive vocal teardown"
}
}

View file

@ -45,6 +45,15 @@ Current slice:
`comparison.csv`, and `summary.txt` under
`artifacts/parity/primitive-vocal-timeline/<timestamp>`. Focused artifact
test passes.
- First comparison-driven law correction: `SourcePort` now owns `FlowScale`
(default `.02`) instead of primitive source flow silently emitting unit-scale
waves; primitive radiation reflection now reports/uses declared
`RadiationLoad.Reflection` instead of aperture-modulating the coefficient;
`ProbeTimelineReport` radiation flow now uses the path outgoing wave, matching
the Faust law. New artifact `20260529T155613895` drops suite mean absolute
timeline error from roughly `1.2-1.3` to `0.15-0.27`. Remaining obvious drift
is branch/contact dynamics, which raises a real owner question: gesture slew
inside ports or separate gesture/timeline primitive.
- Proprioception correction after user rejected baby-word golf as the rebuild
owner. Diagnosis is now durable in

View file

@ -138,6 +138,7 @@ live_invariants:
- Primitive path telemetry now reports `incoming_wave`, `outgoing_wave`, `energy_in`, `energy_out`, and `passivity_ratio` in both Faust debug probes and `ProbeTimelineReport`; contact telemetry includes resistance. `PrimitiveReferenceReport.ComparePinkTrombone` compares static primitive area sections, length, reflection count, delay estimate, branch admittance, contact state, and radiation fields against local PT/SndKit fixture controls without adding a native reference dependency. Next pressure is per-block comparison against PT/SndKit internals, not static control matching alone.
- `PinkTromboneReferenceRenderer.RenderTimeline` now exposes PT internal oral/nasal path, branch, contact, and radiation samples from the renderer's own diameter/wave/reflection/transient arrays. `PrimitiveReferenceReport.ComparePinkTromboneTimeline` role-normalizes Aqua primitive samples against PT timeline samples for path/branch/contact/radiation signals. This is the first local reference-internal comparison surface for the primitive rebuild; next pressure is fixture-suite artifact reporting and then tightening laws where the timeline drift points.
- `PinkTrombonePrimitiveTimelineComparisonWritesArtifacts` writes per-fixture Aqua primitive timelines, PT internal timelines, comparison CSVs, and a summary under `artifacts/parity/primitive-vocal-timeline/<timestamp>`. This makes primitive drift inspectable across the PT fixture suite without native Faust or external VTL/ArtiSynth dependencies. Use these artifacts to choose the next physical-law correction.
- First comparison-driven primitive law correction: `SourcePort` owns `FlowScale` (default `.02`) so pressure/tension/opening no longer emit hidden unit-scale waves; `RadiationLoad.Reflection` owns reflection directly and aperture no longer rewrites that coefficient; `ProbeTimelineReport` radiation flow now uses path outgoing wave like Faust instead of reporting `aperture / impedance`. Artifact `artifacts/parity/primitive-vocal-timeline/20260529T155613895` drops fixture-suite mean absolute timeline error from roughly `1.2-1.3` to `0.15-0.27`. Remaining large drift is branch/contact dynamics, which needs an owner decision about gesture slew versus primitive-local smoothing.
- TurbulenceJet now includes a release-timed burst-noise component in addition to the deterministic pressure pulse, but a stronger trial barely moved papa; the source is not the main remaining articulation bottleneck. Graph radiation now mixes 30% raw boundary flow with 70% high-passed flow after local admittance weighting, down from the over-bright 15/85 blend. Latest radiation/source artifact under `artifacts/parity/pink-trombone-utterance-logmel/20260527T174236049`: mama/papa/thrombosis cosine 0.6578/0.7692/0.5585, rmsRatio 3.0006/2.6153/2.3488, articulation 0.1437/0.1173/0.1191. Static `closure-release` under `artifacts/parity/pink-trombone-logmel/20260527T174139661` has rmsRatio 0.9900 and speechBandRatio 0.0842. This is still not speaking; next pressure is true tract resonance/passive scattering and pressure storage, not more burst gain.
- Same-path two-port area discontinuities now emit `graph_area_energy_in_*` and `graph_area_energy_out_*` probes alongside the three-port branch `graph_connection_energy_*` probes. Use these to inspect passivity and formant-body failures at the scatter sites rather than judging only final output loudness.
- `FaustExportOptions.DebugProbeUi` wraps graph probe signals in Faust `vbargraph` UI controls under `/debug/...` without changing audio output arity. Native streaming Faust patches now separate writable controls from read-only probe zones and can read/snapshot probe values after `ProcessBlock`; use this for acoustic graph passivity/radiation inspection, leave it off for normal parity renders.

View file

@ -254,7 +254,7 @@ public sealed class PatchScriptTests
param path=/mouth/open default=.9 min=0 max=1 step=.001
morphology name=human length_cm=17 diameters=.6,.8,1.2,1.6,1.3,.9 emit_sections=6 tongue_diameter=1.4 constriction_diameter=.8 lip_opening=@/mouth/open
waveguide_path name=oral morphology=human strategy=thiran order=1 max_delay=4096 loss=.998
source_port name=modal path=oral kind=glottal position=0 pressure=.72 tension=.58 opening=.45 noise=.04 impedance=.32
source_port name=modal path=oral kind=glottal position=0 pressure=.72 tension=.58 opening=.45 noise=.04 impedance=.32 flow_scale=.02
constriction_contact name=lip_stop path=oral position=.94 opening=@/mouth/open resistance=.45 stored_pressure=.8
radiation_load name=mouth path=oral kind=lip position=1 aperture=@/mouth/open reflection=-.82 impedance=.28
probe_timeline name=flow network=human blocks=2 block_size=32
@ -271,6 +271,7 @@ public sealed class PatchScriptTests
Assert.Equal("human", path.AreaFunction);
Assert.Equal(WaveClockDelayStrategy.FractionalThiran, path.DelayStrategy);
Assert.Single(patch.SourcePorts);
Assert.Equal(.02f, Assert.Single(patch.SourcePorts).FlowScale, 5);
Assert.Single(patch.ConstrictionContacts);
Assert.Single(patch.RadiationLoads);
Assert.Single(patch.ProbeTimelines);
@ -327,6 +328,7 @@ public sealed class PatchScriptTests
Assert.Contains(samples, sample => sample.Primitive == "path:oral" && sample.Signal == "energy_out");
Assert.Contains(samples, sample => sample.Primitive == "path:oral" && sample.Signal == "passivity_ratio");
Assert.Contains(samples, sample => sample.Primitive == "source:modal" && sample.Signal == "flow");
Assert.Contains(samples, sample => sample.Primitive == "source:modal" && sample.Signal == "flow_scale");
Assert.Contains(samples, sample => sample.Primitive == "contact:lip_stop" && sample.Signal == "resistance");
Assert.Contains(samples, sample => sample.Primitive == "contact:lip_stop" && sample.Signal == "released_flow");
Assert.Contains(samples, sample => sample.Primitive == "radiation:mouth" && sample.Signal == "output");