pyFDN.train package#
Subpackages#
Submodules#
pyFDN.train.build module#
Build a trainable flamo FDN model from a config.
build_fdn() turns a config (delays/N, decay, which gains train) into a
trainable flamo Shell you can render, train, and extract.
trainable_from_build() does the same starting from an existing
FDNBuild.
Both are conveniences over assembling flamo modules yourself with
pyFDN.assemble_fdn_core(); a bare build no longer knows anything about
filter design. trainable_from_preset() bridges that gap when an
FDNPreset records the target and design name. A trainable filter
is a module – AttenuationFilter or OutputEQ –
initialized with that target and handed to whichever hook it belongs in.
- class pyFDN.train.build.Trainable(feedback=True, input_gain=True, output_gain=True, direct=False)[source]#
Bases:
objectWhich of the FDN’s gain groups are trained. Delays are always fixed.
These four are plain arrays: they have no module of their own to carry the flag, so it is named here. The three filter hooks are not in this class, because a filter is a module and a module carries its own
requires_grad– aAttenuationFilterorOutputEQis trained unless it was built withrequires_grad=False.A baked SOS bank taken from an
FDNBuildis always frozen. Raw biquad coefficients have nothing keeping them inside the unit circle, so a fit that wants more energy raises the loop gain past 1 and the network diverges; training one is therefore a module you build on purpose (pyFDN.sos_filter_module()), not a flag.
- pyFDN.train.build.build_fdn(*, delays=None, N=None, rt=2.0, matrix='orthogonal', feedback=None, input_gain=None, output_gain=None, direct=0.0, trainable=None, train_rt=False, fs=48000.0, nfft=16384, alias_decay_db=None, device=None, dtype=None, rng=None)[source]#
Build a trainable flamo
Shellfrom a config.- Parameters:
delays (
ndarray|None) – Explicit integer delay lengths in samples. If omitted,Ncoprime delays are sampled (pyFDN.sample_delay_lengths()).N (
int|None) – Number of delay lines whendelaysis omitted.rt (
float|tuple[float,float] |None) – Reverberation time in seconds, realized as anAttenuationFilterwithdesign="first_order_shelf".Nonebuilds a lossless FDN. For any other design, build the module yourself and pass it totrainable_from_build()aspost_delay=.matrix (
Literal['orthogonal','random']) – Feedback-matrix parametrization.feedback (
ndarray|None) – Initial(N, N)feedback matrix; defaults to a random SO(N) matrix.input_gain (
ndarray|None) –B((N, n_in)) andC((n_out, N)); default ones / sqrt(N).output_gain (
ndarray|None) –B((N, n_in)) andC((n_out, N)); default ones / sqrt(N).direct (
float|ndarray) – Direct pathD; a scalar fills(n_out, n_in).trainable (
Trainable|None) – Which gain groups are trained (defaultTrainable).train_rt (
bool) – Whetherrtis a parameter rather than a design. Off by default, since the decay is usually designed from a measured reverberation time. What trains is the reverberation time itself, which keeps the loop contractive for every value it can take – unlike raw filter coefficients, which nothing holds inside the unit circle.alias_decay_db (
float|None) – Anti-time-aliasing decay, seetrainable_from_build().None(default) picks it fromrt:LOSSLESS_ALIAS_DECAY_DBwhenrt is None, else 0. A lossless FDN has every pole exactly on the unit circle, where the FFT-domain evaluation breaks down entirely; a decaying FDN damps itself withinnfftsamples and needs no nudge. Pass0.0to opt out.fs (
float)nfft (
int)device (
Any)dtype (
Any)rng (
Generator|int|None) – Seed for the sampled delays / default feedback matrix.
- Return type:
- pyFDN.train.build.build_set_decay(build, rt, *, rt_crossover=None)[source]#
Return a copy of
buildwith homogeneous decay matchingrt.Sets the
post_delayhook to per-delay first-order attenuation (pyFDN.decay_to_first_order_shelf()) forrt(a single value, or(rt_dc, rt_nyquist)). Decay does not change colouration, so this is the natural way to add a tail to a colorless build.- Return type:
- pyFDN.train.build.trainable_from_build(build, *, trainable=None, matrix='orthogonal', post_delay=None, post_matrix=None, post_output=None, nfft=16384, alias_decay_db=0.0, device=None, dtype=None)[source]#
Build a trainable flamo
Shellinitialized from anFDNBuild.The gains and the feedback matrix come from the build. The three filter hooks are the build’s own baked SOS banks, frozen, unless you hand in a module for that position – which is how a designed, trainable filter gets in, since a baked build no longer remembers the reverberation time or the EQ curve it was designed from:
model = pyFDN.trainable_from_build( build, post_delay=pyFDN.AttenuationFilter( 1.0, build.delays, build.fs, rt_nyquist=1.0, design="first_order_shelf", nfft=nfft), post_output=pyFDN.OutputEQ( 0.0, build.C.shape[0], build.fs, design="first_order_shelf", nfft=nfft), )
Each of those modules is trained because it says so itself (both default to
requires_grad=True); passrequires_grad=Falsefor a designed filter that must not move.- Parameters:
build (
FDNBuild) – Initial FDN (A/B/C/D/delays/fs+ optionalpost_delay/post_outputSOS banks).trainable (
Trainable|None) – Which gain groups are trained (defaultTrainable). It says nothing about the filter hooks: each module below carries its ownrequires_grad, and is wired in exactly as it was built.matrix (
Literal['orthogonal','random']) – Feedback-matrix parametrization.post_delay (
Any) – In-loop filter, replacingbuild.post_delay. AAttenuationFilterhere makes the trained parameter the reverberation time itself, which keeps the loop contractive for every value it can take.post_matrix (
Any) – Filter on the feedback path, replacingbuild.post_matrix.post_output (
Any) – Output EQ, replacingbuild.post_output; typically anOutputEQ. It sits outside the recursion, which makes it the only part of an FDN that can shape the response’s spectral envelope without touching the decay –bandcare single numbers per delay line, with no frequency dependence at all.nfft (
int) – FFT size.alias_decay_db (
float) –The accuracy of the rendered impulse response, in dB. Applies a \(\gamma^n\) envelope to every module (evaluating the system on a circle of radius \(\gamma < 1\)); the shell’s output layer removes it again, so the response is the true one and only the time-aliased wrap-around remains, suppressed by exactly
alias_decay_db. In float32 the reconstruction amplifies round-off by the same factor, so ~60 dB is the practical ceiling; usedtype=torch.float64beyond that.Leave at 0 for a decaying FDN, which damps itself within
nfftsamples. A lossless FDN needs it: with its poles exactly on the unit circle the FFT-domain evaluation is near-singular and the response comes out wrong, not merely aliased. It does not affect the extracted build (it enters the frequency-domain evaluation, not the parametermap, sopyFDN.extract_build()still returns the undampedA/B/C). A module you pass into a hook must have been built with the same value: it is a change of evaluation radius for the whole system, not a per-module gain.device (
Any) – Torch device / dtype (default cpu-or-cuda / float32).dtype (
Any) – Torch device / dtype (default cpu-or-cuda / float32).
- Return type:
- pyFDN.train.build.trainable_from_preset(preset, *, trainable=None, matrix='orthogonal', trainable_hooks=(), nfft=16384, alias_decay_db=0.0, device=None, dtype=None)[source]#
Build a FLAMO model while recovering designed filter parameters.
The baked build is always the source of truth. A hook is recreated as a
AttenuationFilterorOutputEQonly when its design record contains a target and the recreated SOS bank matches the baked one. Otherwise the baked coefficients remain a frozen filter, exactly as intrainable_from_build().trainable_hooksselects which recovered design targets require gradients. It does not make raw baked SOS coefficients trainable.- Return type:
pyFDN.train.engine module#
Train an FDN toward an objective.
train_fdn() fits a model from pyFDN.build_fdn() to a loss built from
pyFDN.train.losses, in place, and returns a TrainLog. Read the
result back with pyFDN.extract_build().
The engine knows nothing about any particular objective. Its whole job is: run
the model on an impulse, hand the resulting Response
to each loss term, and let the optimizer do the rest.
- class pyFDN.train.engine.TrainLog(train_loss=<factory>, loss_log=<factory>, steps_run=0, stopped_early=False)[source]#
Bases:
objectPer-step loss history and stopping info from a training run.
- Variables:
train_loss (list of float) – Total (weighted) loss at each step.
loss_log (dict of str to list of float) – Per-term loss history, keyed by each term’s name and stored unweighted, so terms stay comparable to their own scale.
steps_run (int) – Steps actually run.
stopped_early (bool) – Whether a plateau stopped it before
max_steps.
- pyFDN.train.engine.train_fdn(model, loss, *, max_steps=2000, lr=0.001, optimizer='adam', patience=10, tol=1e-06, device=None, dtype=None, rng=None, log=False, train_dir=None)[source]#
Train
modelonlossin place and return aTrainLog.Read the trained result back with
pyFDN.extract_build().- Parameters:
model (
Any) – A trainable model frompyFDN.build_fdn()/trainable_from_build. It must return its impulse response – every loss is a function of it – which every pyFDN shell does by construction.loss (
Loss) –The objective, e.g.:
pyFDN.FlatMagnitude() + 0.2 * pyFDN.Sparsity(pyFDN.param(model, "feedback"))
A loss holds whatever reference data it needs (e.g.
MatchSpectrogram(target)), so one objective can compare against more than one reference.max_steps (
int)lr (
float)patience (
int)optimizer (
str) –"adam"(default) or"lbfgs".tol (
float) – Relative-improvement threshold for the plateau early stop.device (
Any) – Torch device / dtype (default cpu / float32).dtype (
Any) – Torch device / dtype (default cpu / float32).log (
bool) – If True, log/checkpoint totrain_dir.train_dir (
str|None) – Checkpoint directory (used whenlog=True).
- Return type:
pyFDN.train.filters module#
FLAMO modules whose parameters are meaningful EQ targets.
AttenuationFilter maps reverberation time to an in-loop SOS bank;
OutputEQ maps gains in dB to a post-output SOS bank. Both use the
same static design functions as NumPy callers.
- class pyFDN.train.filters.AttenuationFilter(rt, delays, fs, *, rt_nyquist=None, design='graphic_eq', rt_crossover=None, nfft=16384, alias_decay_db=0.0, device=None, dtype=None, requires_grad=True)[source]#
Bases:
_DesignedSOSParallel in-loop SOS bank parametrized by reverberation time.
For
graphic_eq,rtis the ten-band target. First-order shelves and one-pole filters usertat DC and the separately namedrt_nyquisttarget; omitting the latter creates a flat target. Targets may additionally carry one value per delay line. The filter is implemented with FLAMO’sparallelSOSFilterbecause an FDN applies one SOS cascade to each delay line in parallel.
- class pyFDN.train.filters.OutputEQ(gain_db, n_channels, fs, *, gain_db_nyquist=None, design='graphic_eq', crossover=None, nfft=16384, alias_decay_db=0.0, device=None, dtype=None, requires_grad=True)[source]#
Bases:
_DesignedSOSParallel SOS bank parametrized by gain in dB.
For
graphic_eq,gain_dbis the ten-band target. First-order shelves and one-pole filters usegain_dbat DC and the separately namedgain_db_nyquisttarget; omitting it creates a flat target.
pyFDN.train.params module#
Name a single trainable parameter of a model, so a penalty can point at it.
A cost on model parameters has to say which parameter. param() resolves
a name against a model’s FLAMO graph once, at the line you write it, and returns
a ParamRef whose value() is the live, differentiable
tensor. params() lists what a model offers – the answer to “what can I put
a cost on?” for a non-standard FDN, where guessing the feedback matrix from the
graph structure would be wrong.
- class pyFDN.train.params.ParamRef(name, module)[source]#
Bases:
objectA reference to one parameter of one model.
- Variables:
name (str) – The name it was resolved under.
module (flamo module) – The module holding the parameter. Bound at construction, so the ref keeps pointing at the same parameter no matter what a loss does with it.
- raw()[source]#
The parameter before the map – what the optimizer actually steps.
Usually the mapped
value()is what you want. The pre-image is, when the map is the point: the RT in seconds behind an attenuation filter (AttenuationFilter), where the mapped value is the SOS bank designed from it.- Return type:
- value()[source]#
The parameter’s mapped value, still attached to the autograd graph.
FLAMO stores a raw parameter and a
maponto the value the system uses – e.g. a skew-symmetric matrix mapped onto SO(N). A penalty wants the mapped value (the actual feedback matrix), not the raw parameter.- Return type:
- pyFDN.train.params.param(model, name=None)[source]#
Reference the parameter called
nameinmodel.- Parameters:
model (
Any) – The model to resolve against. Pass a module directly (withnameomitted) to reference it without any lookup – the escape hatch for a graph whose leaf names you do not control.name (
str|None) – A leaf name, or one of the semantic aliases"feedback"(the feedback matrix, FLAMO’sfB),"delay"(fF),"direct"(brB), and the three filter hooks"post_delay"(the in-loop filter, i.e. the decay),"post_matrix"and"post_output"(the output EQ)."absorption"and"post_eq"also resolve to the first and last of those.
- Raises:
ValueError – If the name matches no parameter, or more than one. The message lists what the model does offer.
- Return type:
- pyFDN.train.params.params(model)[source]#
Every parameter of
model, in graph order.Use it to see what a model exposes before writing a penalty:
>>> for p in pyFDN.params(model): ... print(p) ParamRef('input_gain', (8, 1), trainable) ParamRef('fF', (8,), frozen) ParamRef('fB', (8, 8), trainable) ParamRef('output_gain', (1, 8), trainable)
pyFDN.train.response module#
What the trainer sees: the FDN’s impulse response.
Response is the single object every loss is written against. It holds
the impulse response in the time domain; the frequency-domain views
(spectrum, magnitude) are derived from it and
cached, so several spectral losses in one objective share a single FFT.
- class pyFDN.train.response.Response(h, fs)[source]#
Bases:
objectThe impulse response of an FDN, as a loss sees it.
- Variables:
h (torch.Tensor) – Impulse response of shape
(n_samples, n_out, n_in)– the same convention aspyFDN.build_to_impz(). This is the response itself: any anti-aliasing envelope the model was built with has already been removed by the shell’s output layer, sohis accurate toalias_decay_db(seepyFDN.trainable_from_build()) and needs no further correction. Differentiable during training.fs (float) – Sample rate in Hz.
- pyFDN.train.response.impulse_excitation(n_in, nfft, device=None, dtype=None)[source]#
The excitation that makes a model’s output the full IR matrix.
One Dirac per input channel, each on its own batch row, so a model’s output is
(n_in, nfft, n_out)– the transfer matrixH[out, in], whichmodel_response()permutes into(nfft, n_out, n_in). For a single-input FDN this is one impulse and one batch row.- Return type:
- pyFDN.train.response.model_fs(model)[source]#
Sample rate read from the model’s delay module.
- Return type:
- pyFDN.train.response.model_response(model, excitation=None)[source]#
Run
modelon an impulse and wrap the result in aResponse.Pass
excitationto reuse a tensor across steps;impulse_excitation()builds one.Gradients flow through the returned response, so this is what the trainer calls each step – and, detached, what you can call to inspect a model.
- Return type:
- pyFDN.train.response.require_time_output(model)[source]#
Raise unless
model’s output layer returns the impulse response.Every pyFDN shell does, because
pyFDN.wrap_fdn_shell()builds the inverse-FFT layer that matches the core’salias_decay_db– the domain is a property of the model, not something a caller sets afterwards. AShellassembled by hand can still hand back a frequency-domain tensor, which every loss would silently read as a time signal; hence this check.- Return type:
Module contents#
Training pipeline for FDNs – an explicit three-step API over flamo.
build a trainable flamo model from a config (
build_fdn(), ortrainable_from_build()from an existingFDNBuild).train it toward an objective (
train_fdn()), built by composing losses with+and*:loss = pyFDN.FlatMagnitude() + 0.2 * pyFDN.Sparsity(pyFDN.param(model, "feedback"))
Losses come in two families: those that read the model’s impulse response (
ResponseLoss) and those that put a cost on one of its parameters (ParameterLoss).extract an
FDNBuildback out (pyFDN.extract_build()), plus aTrainLog.