A 1D U-Net trained on paired clean / dirty spectra, with a peak-aware loss that refuses to smooth away the signal you actually care about. Secure PHP + Julia web workflow for CPU-only online inference, with NMRflux based preprocessing and token-protected result downloads.
RINSE trains a three stage 1D U-Net on paired clean / dirty spectra and runs inference on either simulated or experimental data, using a fixed trained model in the online server workflow.
Real and imaginary parts of an NMR spectrum carrying noise and artefacts, fed to the network as a (W, 2, N) WHCN tensor, normalised per-spectrum.
Encoder channels base → 2× → 4× → 8×. Two residual blocks at the bottleneck. Nearest-neighbour upsampling, skip-concat on channels.
A single 1×1 head convolution emits the predicted real-valued clean spectrum. Denormalised back to the original scale before saving.
Spatial dimension W is padded up to a multiple of 8 so three rounds of 2× mean-pooling stay exact. At each decoder stage the matching encoder feature is concatenated on the channel axis before the conv block. A final 1×1 head collapses to one output channel, and the result is cropped back to the original length.
Two Conv(3) + BatchNorm + relu stages with a skip: x + conv2(relu(bn2(conv1(relu(bn1(x)))))).
Encoder widths are 32 → 64 → 128 → 256 at the bottleneck. Decoder mirrors them on the way up.
Before the first encoder, the input is right-padded with zeros so W mod 8 == 0. After the head, the output is cropped back to the original width.
For training data the dirty spectrum is the clean one plus additive noise scaled by the inverse of a chosen SNR. Higher SNR means less noise; lower SNR means the network sees much harsher inputs.
NMR peaks are sparse, sharp, and carry almost all the useful information in the spectrum. A plain MSE objective is dominated by the much larger flat regions, and the network learns to suppress peaks along with the noise. RINSE weights the loss by the target's local curvature so peak apices matter more.
note: the curvature weight and integral scale are wrapped in Zygote.ignore — they depend only on y, which has no gradient, and the weight computation uses in-place writes that Zygote wouldn't handle cleanly anyway.
The online RINSE server accepts JEOL .jdf, Bruker fid + acqus, or numeric TXT/CSV inputs. The browser sends the upload to a secure PHP gatekeeper, which validates the files and parameters, creates a private job folder, writes parameters.toml, and launches CPU-only Julia inference. When processing completes, the result page displays a downsampled preview and provides one token-protected ZIP download containing CSV, NPZ, JLD2, preview JSON, and run metadata.
Users submit JEOL, Bruker, or TXT/CSV data with controlled file-size limits and server-side validation.
The backend applies zero filling, apodization, Fourier transform, and CPU-only model inference.
The result page shows a secure preview plot and provides a single ZIP package for download.
# from a julia 1.10+ repl in your default env using Pkg Pkg.add(["CUDA", "Flux", "JLD2", "BSON", "TOML", "Functors", "Plots", "Zygote"]) Pkg.develop(path="/path/to/NMRflux")
$ julia scripts/train.jl dataset.jld2 $ julia scripts/train.jl dataset.jld2 configs/training_config.toml # resume from a full checkpoint (state + optimizer) $ julia scripts/train.jl dataset.jld2 cfg.toml --restart model_last.bson # weight-only warm start (fresh optimizer, new lr) $ julia scripts/train.jl dataset.jld2 cfg.toml --init model_best.bson
$ julia scripts/infer.jl infer-simulated model.bson dataset.jld2
$ julia scripts/infer.jl infer-experimental model.bson dataset.jld2
# point at a directory to use every .bson as an ensemble
$ julia scripts/infer.jl infer-simulated models_dir/ dataset.jld2
$ julia scripts/infer.jl plot-simulated result.jld2 7 $ julia scripts/infer.jl plot-simulated-all result.jld2 $ julia scripts/infer.jl plot-experimental result.jld2 0 700.0 4.76 $ julia scripts/infer.jl plot-experimental-all result.jld2
Every infer.jl run creates a fresh, timestamped folder in the working directory. The result .jld2, all PNG plots produced by the plot-* commands, and a run.log all land inside it — nothing overwrites across runs.
Overwritten at every checkpoint step and again at exit. Use this to resume a run that was interrupted.
Overwritten whenever validation loss drops below the previous best. This is what the inference script loads by default.
A tagged archive snapshot each time the validation loss crosses a new decade threshold. These stack — point infer.jl at the directory to ensemble them.
This is the web application of the RINSE system for machine learning assisted restoration of one dimensional NMR spectra. It was developed by Manaz Kaleel at the KIT Institute of Microstructure Technology as part of the NMRflux project.
If you use RINSE in a publication, presentation, or report, please cite the associated NMRflux/RINSE work.