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: object

Which 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 – a AttenuationFilter or OutputEQ is trained unless it was built with requires_grad=False.

A baked SOS bank taken from an FDNBuild is 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.

direct: bool = False#
feedback: bool = True#
input_gain: bool = True#
output_gain: bool = True#
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 Shell from a config.

Parameters:
  • delays (ndarray | None) – Explicit integer delay lengths in samples. If omitted, N coprime delays are sampled (pyFDN.sample_delay_lengths()).

  • N (int | None) – Number of delay lines when delays is omitted.

  • rt (float | tuple[float, float] | None) – Reverberation time in seconds, realized as an AttenuationFilter with design="first_order_shelf". None builds a lossless FDN. For any other design, build the module yourself and pass it to trainable_from_build() as post_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)) and C ((n_out, N)); default ones / sqrt(N).

  • output_gain (ndarray | None) – B ((N, n_in)) and C ((n_out, N)); default ones / sqrt(N).

  • direct (float | ndarray) – Direct path D; a scalar fills (n_out, n_in).

  • trainable (Trainable | None) – Which gain groups are trained (default Trainable).

  • train_rt (bool) – Whether rt is 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, see trainable_from_build(). None (default) picks it from rt: LOSSLESS_ALIAS_DECAY_DB when rt 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 within nfft samples and needs no nudge. Pass 0.0 to opt out.

  • fs (float)

  • nfft (int)

  • device (Any)

  • dtype (Any)

  • rng (Generator | int | None) – Seed for the sampled delays / default feedback matrix.

Return type:

Any

pyFDN.train.build.build_set_decay(build, rt, *, rt_crossover=None)[source]#

Return a copy of build with homogeneous decay matching rt.

Sets the post_delay hook to per-delay first-order attenuation (pyFDN.decay_to_first_order_shelf()) for rt (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:

FDNBuild

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 Shell initialized from an FDNBuild.

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); pass requires_grad=False for a designed filter that must not move.

Parameters:
  • build (FDNBuild) – Initial FDN (A/B/C/D/delays/fs + optional post_delay/post_output SOS banks).

  • trainable (Trainable | None) – Which gain groups are trained (default Trainable). It says nothing about the filter hooks: each module below carries its own requires_grad, and is wired in exactly as it was built.

  • matrix (Literal['orthogonal', 'random']) – Feedback-matrix parametrization.

  • post_delay (Any) – In-loop filter, replacing build.post_delay. A AttenuationFilter here 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, replacing build.post_matrix.

  • post_output (Any) – Output EQ, replacing build.post_output; typically an OutputEQ. 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 – b and c are 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; use dtype=torch.float64 beyond that.

    Leave at 0 for a decaying FDN, which damps itself within nfft samples. 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 parameter map, so pyFDN.extract_build() still returns the undamped A/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:

Any

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 AttenuationFilter or OutputEQ only 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 in trainable_from_build().

trainable_hooks selects which recovered design targets require gradients. It does not make raw baked SOS coefficients trainable.

Return type:

Any

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: object

Per-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.

loss_log: dict[str, list[float]]#
steps_run: int = 0#
stopped_early: bool = False#
train_loss: list[float]#
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 model on loss in place and return a TrainLog.

Read the trained result back with pyFDN.extract_build().

Parameters:
  • model (Any) – A trainable model from pyFDN.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).

  • rng (int | None) – Integer seed for torch.manual_seed.

  • log (bool) – If True, log/checkpoint to train_dir.

  • train_dir (str | None) – Checkpoint directory (used when log=True).

Return type:

TrainLog

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: _DesignedSOS

Parallel in-loop SOS bank parametrized by reverberation time.

For graphic_eq, rt is the ten-band target. First-order shelves and one-pole filters use rt at DC and the separately named rt_nyquist target; omitting the latter creates a flat target. Targets may additionally carry one value per delay line. The filter is implemented with FLAMO’s parallelSOSFilter because an FDN applies one SOS cascade to each delay line in parallel.

rt_to_sos(rt)[source]#
Return type:

Any

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: _DesignedSOS

Parallel SOS bank parametrized by gain in dB.

For graphic_eq, gain_db is the ten-band target. First-order shelves and one-pole filters use gain_db at DC and the separately named gain_db_nyquist target; omitting it creates a flat target.

gain_to_sos(gain_db)[source]#
Return type:

Any

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: object

A 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.

module: Any#
name: str#
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:

Tensor

property shape: tuple[int, ...]#

Shape of the mapped value (what a penalty actually sees).

property trainable: bool#
value()[source]#

The parameter’s mapped value, still attached to the autograd graph.

FLAMO stores a raw parameter and a map onto 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:

Tensor

pyFDN.train.params.param(model, name=None)[source]#

Reference the parameter called name in model.

Parameters:
  • model (Any) – The model to resolve against. Pass a module directly (with name omitted) 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’s fB), "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:

ParamRef

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)
Return type:

list[ParamRef]

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: object

The 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 as pyFDN.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, so h is accurate to alias_decay_db (see pyFDN.trainable_from_build()) and needs no further correction. Differentiable during training.

  • fs (float) – Sample rate in Hz.

flamo_layout()[source]#

h permuted to FLAMO’s (batch, n_samples, n_out) layout.

FLAMO’s loss functions take a batched time signal whose batch axis is the excited input, which is exactly h’s input axis moved to front.

Return type:

Tensor

fs: float#
h: Tensor#
property magnitude: Tensor[source]#

|spectrum|, shape (n_samples // 2 + 1, n_out, n_in).

property n_in: int#
property n_out: int#
property n_samples: int#
property spectrum: Tensor[source]#

rfft(h) over time – shape (n_samples // 2 + 1, n_out, n_in).

The DFT of the truncated impulse response, i.e. of h under a rectangular window of n_samples. Computed once per response and shared by every loss that asks for it.

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 matrix H[out, in], which model_response() permutes into (nfft, n_out, n_in). For a single-input FDN this is one impulse and one batch row.

Return type:

Tensor

pyFDN.train.response.model_fs(model)[source]#

Sample rate read from the model’s delay module.

Return type:

float

pyFDN.train.response.model_response(model, excitation=None)[source]#

Run model on an impulse and wrap the result in a Response.

Pass excitation to 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:

Response

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’s alias_decay_db – the domain is a property of the model, not something a caller sets afterwards. A Shell assembled 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:

None

Module contents#

Training pipeline for FDNs – an explicit three-step API over flamo.

  1. build a trainable flamo model from a config (build_fdn(), or trainable_from_build() from an existing FDNBuild).

  2. 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).

  3. extract an FDNBuild back out (pyFDN.extract_build()), plus a TrainLog.