69 Commits

Author SHA1 Message Date
Silas Oettinghaus
cae81c0dae Sync with Mac 2026-07-16 21:52:26 +02:00
Silas Oettinghaus
2e667d6ec3 MPI plotting stuff 2026-07-15 23:02:05 +02:00
Silas Oettinghaus
9455084711 Plots here now 2026-07-15 17:54:48 +02:00
Silas Oettinghaus
eef5d2f2ea MPI plots made with this commit 2026-07-15 17:54:31 +02:00
b068400cb5 Add Mac functionality 2026-07-14 12:20:21 +02:00
Silas Oettinghaus
8c4edf2490 Final MPI Calculations here 2026-07-14 11:50:22 +02:00
Silas Oettinghaus
f421348e5b before new database 2026-07-13 19:59:57 +02:00
Silas Oettinghaus
ef0a74cb7f work on MPI 2026-07-13 15:25:45 +02:00
Silas Oettinghaus
a6cf742121 MPI FFE with A1 and A2 algorithms 2026-07-10 09:14:34 +02:00
Silas Oettinghaus
aa210e5352 more on MPI tracking 2026-07-09 14:23:16 +02:00
Silas Oettinghaus
753bc582b9 MPI analysis, work on DC tracking 2026-07-09 12:29:41 +02:00
Silas Oettinghaus
3cafb06c4f FFE / MPI algorithm 2026-07-09 08:46:35 +02:00
Silas Oettinghaus
1879441999 more and more 2026-07-08 21:36:08 +02:00
Silas Oettinghaus
699d146fb5 plenty of new code and stuff - mostly AI slop and Dissertation. I will move to my own git now 2026-07-08 21:35:53 +02:00
Silas Oettinghaus
33ec5b3116 restructure and organize 2026-06-22 22:58:58 +02:00
Silas Oettinghaus
c4f75b7ec4 restructure and organize 2026-06-22 22:58:35 +02:00
Silas Oettinghaus
3603fd7853 Dies und Das (MPI revisit) 2026-06-21 23:11:13 +02:00
Silas Oettinghaus
5c2e27687d Scattered stuff from Silas during Dissertation 2026-06-03 09:05:33 +02:00
Silas Oettinghaus
a91da3b97c FFE with DC tap. But RLS does not work anymore 2026-05-22 15:28:37 +02:00
Silas Oettinghaus
f883071dec FFE now has a automatic optimization of the mu-parameters -> enables the fast comparison of LMS, NLMS and RLS and also good for very long filters (1000+ taps) which might be unstable else.
Minor changes here and there
2026-05-12 10:40:03 +02:00
Silas Oettinghaus
56b48095db PR stuff 2026-04-29 10:17:09 +02:00
Silas Oettinghaus
910486f536 Add partial response symbol mapping and IM/DD system example
- Implemented a new MATLAB script for partial response symbol mapping using Duobinary precoding.
- Added functions for printing constellation bit mapping and partial response state tables.
- Created a minimal example for an IM/DD system, including signal generation, modulation, and equalization processes.
- Integrated various components such as pulse shaping, optical modulation, and receiver processing.
- Included detailed configurations for parameters like bias, link length, and filter settings.
2026-04-24 09:04:44 +02:00
Silas Oettinghaus
952178341e Füge PRMS-Fehlerbehandlung hinzu und implementiere Partialresponse-Kodierung 2026-04-23 21:56:47 +02:00
Silas Oettinghaus
456fd02186 Ordnerstruktur 2026-04-23 16:16:58 +02:00
Silas Oettinghaus
8fd68c0a54 PR and DB fummelei 2026-04-23 16:16:45 +02:00
Silas Oettinghaus
5446275a5b Merge branch 'main' of cau-git.rz.uni-kiel.de:nt/mitarbeiter/silas/imdd_simulation 2026-04-21 15:24:15 +02:00
Silas Oettinghaus
904eee0604 PR filter viz
Zürich data
Refactoring/ Organization
2026-04-21 15:24:01 +02:00
0ef8129a2c Add new signal generator method that only creates the PRBS 2026-04-21 14:20:48 +02:00
Silas Oettinghaus
20b23dc37e Add stuff to zürich plasmonic project 2026-04-10 15:02:49 +02:00
Silas Oettinghaus
5e5648d11a Fix histogram scaling and expand IMDD modulation sweep 2026-03-27 22:53:49 +01:00
Silas Oettinghaus
10a205f231 Add PAMmapper bit mapping and Gray penalty visualization 2026-03-26 16:04:30 +01:00
Silas Oettinghaus
c135cb76b1 Add AWGN channel models and route IMDD simulation through them 2026-03-26 11:23:07 +01:00
Silas Oettinghaus
d3235a5c9c Unify scope preprocessing and simulation DSP handoff 2026-03-25 14:52:43 +01:00
Silas Oettinghaus
8c55b25112 Refactor preprocessSignal usage across DSP workflows 2026-03-25 14:36:46 +01:00
Silas Oettinghaus
4cef384d15 Split dsp_runid into run preprocessing and scope DSP pipeline 2026-03-25 12:55:50 +01:00
Silas Oettinghaus
2691b90d65 Unify batch job processing with a shared runBatch core 2026-03-25 11:51:26 +01:00
Silas Oettinghaus
0ae846d3c3 CLEANUP - changes to folder structure 2026-03-25 10:57:48 +01:00
Silas Oettinghaus
0c5ad28f0a Merge branch 'main' of cau-git.rz.uni-kiel.de:nt/mitarbeiter/silas/imdd_simulation 2026-03-25 10:26:59 +01:00
Silas Oettinghaus
ed7c68d78b PMD theory; S21 measures; NGMI theory 2026-03-25 10:26:50 +01:00
Silas Oettinghaus
f811ab65cd Merge branch 'main' of cau-git.rz.uni-kiel.de:nt/mitarbeiter/silas/imdd_simulation 2026-03-25 09:48:46 +01:00
Silas Oettinghaus
c6b902d346 final touch on test_dashboard 2026-03-25 09:48:43 +01:00
7d856bf124 Merge branch 'main' of cau-git:nt/mitarbeiter/silas/imdd_simulation 2026-03-25 09:45:49 +01:00
b4e735148e Add some older Theory projects from the past years 2026-03-25 09:45:42 +01:00
Silas Oettinghaus
b291e0a7bb Fix dashboard run metadata tracking per test 2026-03-25 09:38:48 +01:00
Silas Oettinghaus
69578b3c43 Merge branch 'main' of cau-git.rz.uni-kiel.de:nt/mitarbeiter/silas/imdd_simulation 2026-03-25 09:33:44 +01:00
Silas Oettinghaus
e16806b520 Improve test dashboard and update WDM test planning 2026-03-25 09:33:33 +01:00
Silas Oettinghaus
76be57515d Strengthen WDM and IM/DD optical tests 2026-03-25 08:07:31 +01:00
Silas Oettinghaus
5b90bc11a0 Merge branch 'main' of cau-git.rz.uni-kiel.de:nt/mitarbeiter/silas/imdd_simulation 2026-03-24 18:01:13 +01:00
Silas Oettinghaus
0e6be4a584 Infos für Overlord II 2026-03-24 17:58:35 +01:00
Silas Oettinghaus
ac374ee5c4 Merge branch 'main' of cau-git.rz.uni-kiel.de:nt/mitarbeiter/silas/imdd_simulation 2026-03-24 17:58:19 +01:00
Silas Oettinghaus
96dc0a8b6a Infos für Overlord 2026-03-24 17:58:11 +01:00
Silas Oettinghaus
e88a3359eb add AIR stuff from TUM
minor changes here and there
2026-03-24 17:58:05 +01:00
Silas Oettinghaus
a592ebefda Build out MATLAB test framework and core integration coverage 2026-03-24 17:23:02 +01:00
Silas Oettinghaus
9446dfb888 theory stuff for dissertation 2026-03-24 15:09:15 +01:00
Silas Oettinghaus
9dccca8ff9 Merge branch 'main' of cau-git.rz.uni-kiel.de:nt/mitarbeiter/silas/imdd_simulation 2026-03-13 09:10:47 +01:00
Silas Oettinghaus
0577ffe7f6 Merge branch 'main' of cau-git.rz.uni-kiel.de:nt/mitarbeiter/silas/imdd_simulation 2026-03-12 17:27:23 +01:00
Silas Oettinghaus
4c78a1e0b1 mainly tests (empty) and some theroy plots 2026-03-12 17:26:13 +01:00
magf
3676d92b30 +++ Changes +++
+ duobinary_target now supports memoryless decoding (FFE targets DB response without MLSE)
+ PAMmapper.quantize now supports custom constellations for quantization
+ Added a new folder 'Documentations' for pdfs, slides, etc.
+ Added new FSO evaluation scripts in projects/FSO transmission/Evaluation Scripts
+ Added ffe_db (rudimentary module, not important anymore)
2026-03-05 10:41:36 +01:00
Silas Oettinghaus
c0a0a415a8 theory silas diss 2026-02-20 09:51:01 +01:00
Silas Oettinghaus
2a724b833f Theory stuff from sials work pc; minor stuff here and there 2026-02-19 15:34:53 +01:00
Silas Oettinghaus
1b8d83dec0 theory stuff für Diss 2026-02-19 15:06:40 +01:00
magf
3edd64d365 Additions:
- Correction of the weighted DFE function in EQ.m
- Some evaluation scripts for FSO Data
2026-02-09 13:04:38 +01:00
Silas Oettinghaus
58e72070d9 Merge branch 'main' of cau-git.rz.uni-kiel.de:nt/mitarbeiter/silas/imdd_simulation 2026-02-05 15:50:57 +01:00
Silas Oettinghaus
bc8ebc044f Minimal changes 2026-02-05 15:50:49 +01:00
Silas Oettinghaus
3d47813f1e Silas work PC
eye diagram working for all signals again...
A problem: I found that the Tx eyes look weird (i.e. PAM-8)
vibecoded ML_MLSE_GPU ... a bit faster :-)
2026-02-05 13:36:33 +01:00
magf
46172bbd5d SourceGit Test 2026-02-02 15:19:29 +01:00
Silas Oettinghaus
7c9577791d Merge branch 'main' of cau-git.rz.uni-kiel.de:nt/mitarbeiter/silas/imdd_simulation 2026-02-02 14:51:04 +01:00
Silas Oettinghaus
4e233bde95 BER calculation now correct :-/ 2026-02-02 14:50:56 +01:00
magf
7565ed0cf1 Restore lost changes from pre-merge state 2026-02-02 13:38:56 +01:00
611 changed files with 47722 additions and 6876 deletions

4
.gitignore vendored
View File

@@ -23,3 +23,7 @@ codegen/
.mat
# Local test dashboard status tracking
Tests/.last_test_run.json
Tests/reports/

12
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "matlab",
"name": "Debug MATLAB"
}
]
}

344
AGENT_OVERVIEW.md Normal file
View File

@@ -0,0 +1,344 @@
# IM/DD Simulation Framework Overview
This file is for agents that encounter this repository for the first time.
Its goal is to explain what the codebase is for, where the main building
blocks live, and how a typical IM/DD workflow is assembled.
It is not a testing guide. The focus here is the simulation framework itself.
## What This Repository Is
This repository is a MATLAB-based simulation and analysis framework for
IM/DD optical communication systems.
At a high level, the codebase supports:
- generation of digital symbol and bit streams
- electrical transmitter modeling
- electro-optical modulation
- optical channel modeling
- photodetection and receiver front-end processing
- digital signal processing and sequence detection
- metric evaluation such as BER, GMI, AIR, EVM, and SNR
- project-specific simulation scripts and experimental analysis workflows
The codebase is not just one monolithic simulator. It is a toolkit of
reusable classes plus many project scripts that assemble those classes in
different ways.
## Mental Model
The easiest way to understand the repository is as a staged signal chain:
1. Information signal generation
2. Symbol mapping and pulse shaping
3. AWG / electrical drive path
4. Optical modulation
5. Fiber / channel propagation
6. Receiver front-end
7. Synchronization and DSP
8. Performance evaluation
The classes in `Classes/` implement the reusable blocks.
The scripts in `projects/` wire those blocks together into concrete systems.
## Suggested First File To Read
For a compact end-to-end example, start here:
- `projects/IMDD_base_system/minimal_example.m`
That file shows a reduced but representative full workflow:
- `PAMsource`
- `AWG`
- `EML`
- `Fiber`
- `Amplifier`
- `Photodiode`
- `Filter`
- `Scope`
- matched filtering
- synchronization
- equalization / MLSE
If you need to understand “how the system is intended to be used”, this is
one of the best entry points.
## Top-Level Structure
### `Classes/`
This is the core reusable framework.
Most simulation building blocks live here.
Subfolders are organized by role:
- `00_signals`
Base signal classes and core signal behavior.
This is the semantic foundation used by most of the rest of the codebase.
- `01_transmit`
Digital TX-side blocks such as bit/symbol generation, mapping, pulse
shaping, and AWG-related functionality.
- `02_etc`
General-purpose support blocks, especially filtering and amplification.
- `02_optical`
Optical-domain components such as modulators, multiplexing, fiber/channel
propagation, and related physical effects.
- `03_electrical`
Electrical-domain helper blocks and traces.
- `03_receive`
RX-side front-end blocks such as photodiodes and scopes/ADC behavior.
- `04_DSP`
Equalizers, timing recovery, postfilters, coding blocks, and sequence
detection such as MLSE.
- `05_Lab`
Instrument-control classes for real hardware.
These are not just simulations. They can talk to reachable devices.
- `DataBaseHandler`
Query/filter/result structures and database-facing helper classes.
- `Warehouse_class`
More specialized storage / plotting / result-handling infrastructure.
### `Functions/`
This holds free functions and workflow helpers that are not packaged as
classes.
Important subareas:
- `EQ_structures`
Higher-level DSP driver functions such as `ffe`, `vnle_postfilter_mlse`,
and related end-stage evaluation helpers.
- `Metrics`
BER, EVM, GMI, AIR, SNR, and related performance calculations.
- `EQ_visuals`
Diagnostic and visualization helpers for equalizers and result analysis.
- `channel_structures`
Channel-model helpers.
- `Job_Processing`
Scriptable processing helpers for bigger job/result workflows.
- `Theory`
Supporting theory calculations and one-off analytic utilities.
### `Datatypes/`
This contains enums and small type definitions used throughout the codebase.
Examples:
- normalization modes
- power notation
- filter types
- modulation / adaptation enums
When a class constructor takes a symbolic mode value, it is often defined here.
### `projects/`
This is where the reusable framework gets turned into concrete systems,
experiments, and studies.
The `projects/` folder is broad and includes:
- minimal examples
- paper-specific studies
- lab analysis scripts
- offline DSP pipelines
- experiment-specific workflows
These scripts are often the best place to understand intended usage patterns.
### `Tests/`
This contains the current MATLAB unit/integration test framework.
It validates parts of the reusable framework, not the full meaning of the
repository.
Use it as a quality tool, not as the primary documentation source for what
the simulation does.
## Core Signal Classes
The repository uses class-based signal objects instead of raw arrays whenever
possible.
Key signal classes:
- `Signal`
Base class with common signal behavior, metadata, logbook, arithmetic,
normalization, resampling, plotting, and utility methods.
- `Informationsignal`
Discrete/digital information-level representation.
- `Electricalsignal`
Electrical-domain signal representation.
- `Opticalsignal`
Optical-domain signal representation including optical metadata such as
wavelength and ASE-related quantities.
These classes are central because many other blocks accept or return them.
## Typical Workflow Composition
A representative IM/DD chain often looks like this:
1. `PAMsource`
Creates bits, mapped symbols, and shaped digital transmit signals.
2. `AWG`
Applies DAC-like processing, upsampling, quantization, and optional
filtering.
3. `EML`
Converts the electrical drive signal to an optical signal.
4. `Fiber`
Applies optical channel propagation.
5. `Amplifier`
Sets optical power / gain and can manipulate ASE handling.
6. `Photodiode`
Performs square-law detection and creates an electrical RX signal.
7. `Filter`
Applies electrical filtering.
8. `Scope`
Models ADC / sampling / quantization / optional RX-side LPF behavior.
9. `Pulseformer` in matched-filter mode
Used again on the RX side as matched filtering.
10. `tsynch`
Synchronizes the RX signal against transmitted symbols.
11. DSP blocks
Examples:
- `FFE`
- `EQ`
- `Postfilter`
- `MLSE`
- timing-recovery variants
12. Metrics
Performance is evaluated via helper functions and result structures.
## Two Important Layers
There are two distinct abstraction levels in the repo:
### 1. Block-level classes
Examples:
- `PAMsource`
- `Pulseformer`
- `AWG`
- `EML`
- `Fiber`
- `Photodiode`
- `Scope`
- `FFE`
- `MLSE`
These are the reusable simulation primitives.
### 2. Workflow-level functions/scripts
Examples:
- `projects/.../minimal_example.m`
- `Functions/EQ_structures/ffe.m`
- `Functions/EQ_structures/vnle_postfilter_mlse.m`
These assemble the primitives into practical runs and performance outputs.
When debugging behavior, it matters which layer you are in:
- if a signal object has the wrong shape or `fs`, look at block-level classes
- if BER/GMI pipelines behave unexpectedly, also inspect workflow-level DSP helpers
## Where To Look For What
If you want to understand:
- base signal semantics:
read `Classes/00_signals/*`
- transmitter generation:
read `Classes/01_transmit/*`
- optical propagation:
read `Classes/02_optical/*`
- receiver modeling:
read `Classes/03_receive/*`
- equalization and detection:
read `Classes/04_DSP/*` and `Functions/EQ_structures/*`
- metric definitions:
read `Functions/Metrics/*`
- intended end-to-end usage:
read `projects/IMDD_base_system/minimal_example.m`
## How To Approach The Repo As A New Agent
A good first-pass reading order is:
1. `projects/IMDD_base_system/minimal_example.m`
2. `Classes/00_signals/Signal.m`
3. `Classes/01_transmit/PAMsource.m`
4. `Classes/01_transmit/Pulseformer.m`
5. `Classes/01_transmit/AWG.m`
6. `Classes/02_optical/EML.m`
7. `Classes/02_optical/Fiber.m`
8. `Classes/03_receive/Photodiode.m`
9. `Classes/03_receive/Scope.m`
10. `Functions/EQ_structures/ffe.m`
11. `Functions/EQ_structures/vnle_postfilter_mlse.m`
12. `Classes/04_DSP/Equalizer/FFE.m`
13. `Classes/04_DSP/Sequence Detection/MLSE.m`
This gives both the architectural view and the runtime path.
## Practical Safety Note
Do not casually execute or test `Classes/05_Lab/*`.
Those are lab-device control classes, not harmless simulations.
In this environment, reachable device IPs can exist, and executing those
classes may change instrument set points and interfere with active experiments.
Treat `05_Lab` as operational code, not as a normal simulation subfolder.
## Final Summary
The repository is best understood as:
- a reusable class library for IM/DD system building blocks
- plus workflow helpers for DSP and analysis
- plus many project scripts that instantiate those blocks for concrete studies
If you are lost, do not start from `Tests/`.
Start from the minimal project workflow, then map each stage back to the
relevant class folder in `Classes/`.

View File

@@ -26,11 +26,11 @@ classdef Informationsignal < Signal
end
% function pow = power(obj)
%
% pow = mean(abs(obj.signal.^2),"all") ;
%
% end
function pow = power(obj)
pow = mean(abs(obj.signal.^2),"all") ;
end
end

View File

@@ -24,9 +24,9 @@ classdef Signal
obj.signal = signal;
obj.signal = obj.signal;
obj.fs = options.fs;
[~,obj.gitSHA] = system('git rev-parse HEAD');
[~,obj.gitStatus] = system('git status --porcelain');
%
% [~,obj.gitSHA] = system('git rev-parse HEAD');
% [~,obj.gitStatus] = system('git status --porcelain');
% [~,obj.gitPatch] = system('git diff');
%%% Stuff for Logbook %%%
@@ -200,12 +200,15 @@ classdef Signal
%% Add signals from one signal to another, the first object will sustain
function Sum = plus(X,y)
if isa(y,'Signal')
if isa(X,'Signal') && isa(y,'Signal')
Sum = X;
Sum.signal = X.signal + y.signal;
elseif isnumeric(y)
elseif isa(X,'Signal') && isnumeric(y)
Sum = X;
Sum.signal = X.signal + y;
elseif isnumeric(X) && isa(y,'Signal')
Sum = y;
Sum.signal = X + y.signal;
end
end
@@ -213,24 +216,42 @@ classdef Signal
%% Add signals from one signal to another, the first object will sustain
function Diff = minus(X,y)
if isa(y,'Signal')
if isa(X,'Signal') && isa(y,'Signal')
Diff = X;
Diff.signal = X.signal - y.signal;
elseif isnumeric(y)
elseif isa(X,'Signal') && isnumeric(y)
Diff = X;
Diff.signal = X.signal - y;
elseif isnumeric(X) && isa(y,'Signal')
Diff = y;
Diff.signal = X - y.signal;
end
end
function Product = times(X,y)
if isa(y,'Signal')
if isa(X,'Signal') && isa(y,'Signal')
Product = X;
Product.signal = X.signal .* y.signal;
elseif isnumeric(y)
elseif isa(X,'Signal') && isnumeric(y)
Product = X;
Product.signal = X.signal .* y;
elseif isnumeric(X) && isa(y,'Signal')
Product = y;
Product.signal = X .* y.signal;
end
end
function Product = mtimes(X,y)
if (isa(X,'Signal') && isnumeric(y) && isscalar(y)) || ...
(isnumeric(X) && isscalar(X) && isa(y,'Signal'))
Product = times(X,y);
else
error('Signal:mtimes:UnsupportedOperands', ...
'Use element-wise .* for Signal multiplication, or scalar * Signal for scaling.');
end
end
@@ -325,7 +346,8 @@ classdef Signal
else
obj.signal = resample(obj.signal,options.fs_out,options.fs_in,options.n,options.beta);
[p, q] = rat(options.fs_out / options.fs_in);
obj.signal = resample(obj.signal,p,q,options.n,options.beta);
desc = ['resample signal from ', num2str(options.fs_in*1e-9), ' GHz to ', num2str(options.fs_out*1e-9), ' GHz' ];
@@ -346,10 +368,13 @@ classdef Signal
options.displayname = "";
options.color = [];
options.linestyle = '-';
options.HandleVisibility (1,1) string {mustBeMember(options.HandleVisibility, ["on","off"])} = "on";
options.normalizeToNyquist = 0;
options.normalizeToSamplingRate = 0;
options.addDCoffset = 0;
options.normalizeToDC = 0;
options.normalizeTo0dB = 0;
options.show_onesided = false;
options.max_num_lines = []; % Leave empty or omit to disable line rotation
options.fft_length = [];
% --- NEW options ---
@@ -362,7 +387,10 @@ classdef Signal
end
if options.normalizeToNyquist == 0
useSamplingRateAxis = options.normalizeToSamplingRate ~= 0;
useRadPerSampleAxis = options.normalizeToNyquist ~= 0 && ~useSamplingRateAxis;
if ~useRadPerSampleAxis && ~useSamplingRateAxis
[p_lin,f_Hz] = pwelch(obj.signal, hanning(options.fft_length), ...
options.fft_length/2, options.fft_length, ...
obj.fs, "centered", "power", "mean");
@@ -371,11 +399,11 @@ classdef Signal
[p_lin,f_rad] = pwelch(obj.signal, hanning(options.fft_length), ...
options.fft_length/2, options.fft_length, ...
"centered", "power", "mean");
% In normalized mode, pwelch returns rad/sample centered on 0.
% We'll keep f_rad for the x-axis in that mode.
% In normalized modes, pwelch returns rad/sample centered on 0.
% Divide by 2*pi for the f/fs axis where Nyquist is 0.5.
end
% p_lin = movmean(p_lin,4);
p_lin = movmean(p_lin,10);
if options.normalizeTo0dB
p_lin = p_lin ./ max(p_lin);
@@ -387,7 +415,7 @@ classdef Signal
end
% --- If requested, build wavelength axis from frequency offset ---
if options.useWavelengthAxis && options.normalizeToNyquist == 0
if options.useWavelengthAxis && ~useRadPerSampleAxis && ~useSamplingRateAxis
c = physconst('LightSpeed'); % [m/s]
lambda0_m = options.lambda0_nm*1e-9; % center wavelength [m]
f_c = c / lambda0_m; % carrier frequency [Hz]
@@ -400,21 +428,36 @@ classdef Signal
% assign axis
x_vec = lambda_nm(:);
x_label = "Wavelength [nm]";
dc_axis = f_Hz;
% Sort to ensure axis is ascending
[x_vec, sortIdx] = sort(x_vec, 'ascend');
p_dbm = p_dbm(sortIdx, :);
dc_axis = dc_axis(sortIdx);
else
% Frequency or normalized axes
if options.normalizeToNyquist == 0
if ~useRadPerSampleAxis && ~useSamplingRateAxis
x_vec = f_GHz;
x_label = "Frequency in GHz";
dc_axis = f_GHz;
elseif useSamplingRateAxis
x_vec = f_rad ./ (2*pi);
x_label = "Normalized Frequency f/fs";
dc_axis = x_vec;
else
x_vec = f_rad; % normalized frequency in rad/sample
x_label = "Normalized Frequency";
x_label = "Normalized Frequency [rad/sample]";
dc_axis = x_vec;
end
end
if options.show_onesided
keep_idx = dc_axis >= 0;
x_vec = x_vec(keep_idx);
dc_axis = dc_axis(keep_idx);
p_dbm = p_dbm(keep_idx, :);
end
figure(options.fignum);
ax = gca;
hold on
@@ -422,7 +465,7 @@ classdef Signal
p_dbm = p_dbm+options.addDCoffset;
if options.normalizeToDC
[~,min_idx]=min(abs(f_GHz));
[~,min_idx]=min(abs(dc_axis));
pow_at_dc = p_dbm(min_idx);
p_dbm = p_dbm-pow_at_dc;
end
@@ -430,9 +473,9 @@ classdef Signal
for s = 1:min(size(p_dbm))
if isempty(options.color)
plot(x_vec, p_dbm(:,s), 'DisplayName', options.displayname, 'LineWidth', 1);
plot(x_vec, p_dbm(:,s), 'DisplayName', options.displayname, 'LineWidth', 1, 'LineStyle', options.linestyle, 'HandleVisibility', options.HandleVisibility);
else
plot(x_vec, p_dbm(:,s), 'DisplayName', options.displayname, 'LineWidth', 1, 'Color', options.color,'LineStyle',options.linestyle);
plot(x_vec, p_dbm(:,s), 'DisplayName', options.displayname, 'LineWidth', 1, 'Color', options.color,'LineStyle',options.linestyle, 'HandleVisibility', options.HandleVisibility);
end
end
@@ -448,15 +491,25 @@ classdef Signal
% Axis labels and limits
xlabel(x_label);
if options.useWavelengthAxis && options.normalizeToNyquist == 0
if options.useWavelengthAxis && ~useRadPerSampleAxis && ~useSamplingRateAxis
xlim([min(x_vec) max(x_vec)]);
else
if options.normalizeToNyquist == 0
if ~useRadPerSampleAxis && ~useSamplingRateAxis
% Keep your existing freq handling (you can fine-tune as needed)
% xlim([-128 128]); % example for 256 GSa/s if desired
xlim([min(x_vec) max(x_vec)]);
elseif useSamplingRateAxis
if options.show_onesided
xlim([0, 0.5]);
else
xlim([-0.5, 0.5]);
end
else
xlim([-pi, pi]);
if options.show_onesided
xlim([0, pi]);
else
xlim([-pi, pi]);
end
end
end
@@ -473,13 +526,10 @@ classdef Signal
end
y_margin = 0.05 * y_range;
ylim([y_min - y_margin, y_max + y_margin]);
% Set ticks automatically, avoid overpopulation
try
yticks(round(linspace(y_min, y_max, min(10, max(4, ceil(y_range/10))))));
end
grid on;
ylim([y_min - y_margin, y_max + y_margin]);
yticks(-200:10:200);
grid on;
% Add legend if not already present
if isempty(get(gca, 'Legend'))
@@ -762,11 +812,18 @@ classdef Signal
pkpos = sort(pkpos);
% if mean(w) > 15 || mean(p) > 15
% return
% else
% sequenceFound = 1;
% end
if isempty(pks)
warning(['Error in findpeaks, ususally the seuqnece is too short. No Peaks detected']);
return
end
if max(p) < 0.3 || median(w) > 15
%median(w) > 15 part means reject if the detected correlation peaks are too broad. That can be sensible: a true sync peak should often be sharp.
warning(['Error in findpeaks, ususally the seuqnece is too short. max(p) = ',num2str(max(p)),'; median(w)=',num2str(median(w)),'']);
return
end
sequenceFound = 1;
if options.debug_plots
figure(121212);clf
@@ -803,6 +860,10 @@ classdef Signal
S{c}.logbook = [];
end
if ~isempty(S)
obj = S{1};
end
else
%do nothing when shifts are negative or there are none...
@@ -925,9 +986,10 @@ classdef Signal
M
options.fignum = 100;
options.displayname = "";
options.mode = 1; %1= histogram method; 2= intuitive "line based" eye
end
mode = 2;
mode = options.mode;
histpoints = 2048; %% verticale resolution
histpoints = floor(histpoints/2)*2+1; %% to have the eye digram centered around one point make the vertical resolution uneven
@@ -966,8 +1028,9 @@ classdef Signal
col = cbrewer2('Set1',2);
for n=1:1000
hold on
plot(eye_mat(:,n),'LineStyle',':','LineWidth',0.1,'Color',col(2,:));
plot(eye_mat(:,n),'LineStyle','-','LineWidth',0.1,'Color',col(2,:));
end
xlabel('Samples','Interpreter','latex')
ylabel('Amplitude of Signal','Interpreter','latex');
xlim([0 histpoints_horizontal])
@@ -975,15 +1038,47 @@ classdef Signal
elseif mode == 1
% generate eye diagram using histogram
maxA = max(sig(100:end-100))*1.3;
minA = min(sig(100:end-100))*1.3;
finite_eye = eye_mat(isfinite(eye_mat));
if isempty(finite_eye)
finite_eye = sig(isfinite(sig));
end
amp_min = min(finite_eye);
amp_max = max(finite_eye);
amp_center = (amp_max + amp_min) / 2;
amp_span = amp_max - amp_min;
if amp_span == 0
amp_span = max(abs(amp_center),1);
end
amp_margin = 0.08 * amp_span;
maxA = amp_center + amp_span/2 + amp_margin;
minA = amp_center - amp_span/2 - amp_margin;
if ~isa(obj,'Opticalsignal') && minA < 0 && maxA > 0
targetStep = max(abs([minA maxA])) / 2;
if targetStep > 0
stepMagnitude = 10^floor(log10(targetStep));
normalizedStep = targetStep / stepMagnitude;
if normalizedStep <= 1
tickStep = stepMagnitude;
elseif normalizedStep <= 2
tickStep = 2 * stepMagnitude;
elseif normalizedStep <= 5
tickStep = 5 * stepMagnitude;
else
tickStep = 10 * stepMagnitude;
end
axisLimit = 2 * tickStep;
maxA = axisLimit;
minA = -axisLimit;
end
end
maxA = 0.12;
minA = -0.08;
% maxA = 0.12;
% minA = -0.08;
difference= maxA-minA;
data_ind_y=round((eye_mat-minA)/difference*(histpoints-1)) +1;
data_ind_y = min(max(data_ind_y,1),histpoints);
for n=1:size(data_ind_y,1)
nn=histcounts(data_ind_y(n,:),1:histpoints+1);
@@ -1009,19 +1104,19 @@ classdef Signal
if isa(obj,'Opticalsignal')
title(['Optical Eye ',options.displayname])
ylabel("Power in mW");
y_tickstring = string(linspace(maxA.*1e3,minA.*1e3,6));
yTickValues = linspace(maxA.*1e3,minA.*1e3,5);
min_ = min(abs(obj.signal(100:end-100)).^2);
max_ = abs(max(obj.signal(100:end-100)).^2);
elseif isa(obj,'Electricalsignal')
title(['Electrical Eye ',options.displayname])
ylabel("Voltage in V");
y_tickstring = string(linspace(maxA,minA,6));
yTickValues = linspace(maxA,minA,5);
min_ = min(obj.signal(100:end-100));
max_ = abs(max(obj.signal(100:end-100)));
else
title(['Digital Eye ',options.displayname])
ylabel("Digital Signal Amplitude");
y_tickstring = string(linspace(maxA,minA,6));
yTickValues = linspace(maxA,minA,5);
min_ = min(obj.signal(100:end-100));
max_ = abs(max(obj.signal(100:end-100)));
end
@@ -1062,7 +1157,14 @@ classdef Signal
hist_interest_smoth = smooth(hist_interest,20);
a = scatter(hist_interest_smoth+posxall,1:length(hist_interest_smoth),4,'.','MarkerEdgeColor','red');
[pk,loc] = findpeaks(hist_interest_smoth,"MinPeakDistance",10,"NPeaks",M,"MinPeakHeight",30,"MinPeakProminence",10);
minPeakDistance = max(10, floor(histpoints / (2*M)));
minPeakProminence = max(3, 0.05 * max(hist_interest_smoth));
[pk,loc] = findpeaks(hist_interest_smoth, ...
"MinPeakDistance",minPeakDistance, ...
"NPeaks",M, ...
"MinPeakProminence",minPeakProminence, ...
"SortStr","descend");
loc = sort(loc);
scatter(posxall,loc,'red','Marker','x','LineWidth',2);
@@ -1145,12 +1247,13 @@ classdef Signal
end
yticks(linspace(0,histpoints,6));
y_tickstring = sprintfc('%.2f', y_tickstring);
yticklabels(y_tickstring);
yTickPositions = linspace(1,histpoints,numel(yTickValues));
yticks(yTickPositions);
yticklabels(sprintfc('%.2f', yTickValues));
xticks(linspace(0,histpoints_horizontal,6))
x_tickstring = sprintfc('%.2f', linspace(0, 2/fsym, 8) .* 1e12);
xTickValues = linspace(0, 2/fsym, 6) .* 1e12;
xticks(linspace(1,histpoints_horizontal,numel(xTickValues)))
x_tickstring = sprintfc('%.2f', xTickValues);
xticklabels(x_tickstring);
%

View File

@@ -254,7 +254,7 @@ classdef ChannelFreqResp < handle
xlim([0.2 .5*max(obj.faxis)*1e-9]); grid on;
%%% plot for publication
figure(101);
figure(1996);
hold all;
box on;
title('Magnitude Freq. Response');

View File

@@ -462,10 +462,57 @@ classdef PAMmapper
end
function [Signal_out] = quantize(obj,Signal_in)
function [out, levels] = splitByReferenceLevels(obj, data_in, reference_in, options)
% Split received samples by the transmitted/reference PAM level.
% Unlike separate_pamlevels, this does not decide the RX level.
arguments
obj
data_in
reference_in
options.levels = []
options.tolerance (1,1) double {mustBeNonnegative} = 0
end
constellation = obj.get_levels();
constellation = constellation ./ obj.scaling;
data = obj.toNumericVector(data_in);
reference = obj.toNumericVector(reference_in);
if numel(data) ~= numel(reference)
error("PAMmapper:LengthMismatch", ...
"data_in and reference_in must have the same number of samples.");
end
if isempty(options.levels)
levels = unique(reference);
else
levels = options.levels(:).';
end
out = NaN(numel(levels), numel(reference));
for levelIdx = 1:numel(levels)
if options.tolerance == 0
levelMask = reference == levels(levelIdx);
else
levelMask = abs(reference - levels(levelIdx)) <= options.tolerance;
end
out(levelIdx, levelMask) = data(levelMask);
end
end
function [Signal_out] = quantize(obj,Signal_in,options)
arguments
obj
Signal_in Signal
options.custom_const = [];
end
if isempty(options.custom_const)
constellation = obj.get_levels();
constellation = constellation ./ obj.scaling;
else
constellation = options.custom_const;
end
issignalclass = 0;
if isa(Signal_in,'Signal')
@@ -495,10 +542,133 @@ classdef PAMmapper
end
function [ax, mapping_table] = plotBitMapping(obj)
[constellation, bitmap] = obj.getConstellationBitMapping();
constellation = constellation .* obj.get_scaling;
bit_labels = string(cellstr(char(bitmap + '0')));
if obj.M == 6
[~, order] = sortrows([-constellation(:,2), constellation(:,1)]);
constellation = constellation(order, :);
bit_labels = bit_labels(order);
mapping_table = table(constellation(:,1), constellation(:,2), bit_labels, ...
'VariableNames', {'symbol_1', 'symbol_2', 'bits'});
fig = figure("Name", sprintf("PAM-%d Bit Mapping", obj.M));
ax = axes(fig);
scatter(ax, constellation(:,1), constellation(:,2), 60, "filled");
hold(ax, "on");
grid(ax, "on");
box(ax, "on");
x_step = min(diff(unique(constellation(:,1))));
y_step = min(diff(unique(constellation(:,2))));
text(ax, constellation(:,1), constellation(:,2) + 0.18*y_step, bit_labels, ...
"HorizontalAlignment", "center", "FontName", "Consolas");
xlabel(ax, "Symbol 1");
ylabel(ax, "Symbol 2");
title(ax, sprintf("PAM-%d Mapping", obj.M));
axis(ax, "equal");
xticks(ax, unique(constellation(:,1)));
yticks(ax, unique(constellation(:,2)));
xlim(ax, [min(constellation(:,1)) - 0.8*x_step, max(constellation(:,1)) + 1.4*x_step]);
ylim(ax, [min(constellation(:,2)) - 0.6*y_step, max(constellation(:,2)) + 0.9*y_step]);
else
constellation = constellation(:);
mapping_table = table(constellation, bit_labels, ...
'VariableNames', {'symbol', 'bits'});
fig = figure("Name", sprintf("PAM-%d Bit Mapping", obj.M));
ax = axes(fig);
scatter(ax, constellation, zeros(size(constellation)), 60, "filled");
hold(ax, "on");
grid(ax, "on");
box(ax, "on");
text(ax, constellation, 0.08*ones(size(constellation)), bit_labels, ...
"HorizontalAlignment", "center", "FontName", "Consolas");
xlabel(ax, "Symbol");
title(ax, sprintf("PAM-%d Mapping", obj.M));
yticks(ax, []);
ylim(ax, [-0.2 0.2]);
xticks(ax, constellation);
if numel(constellation) > 1
x_step = min(diff(unique(constellation)));
else
x_step = 1;
end
xlim(ax, [min(constellation) - 0.8*x_step, max(constellation) + 0.8*x_step]);
end
if nargout == 0
disp(mapping_table);
end
end
function bitmap = showBitMapping(obj)
[~, bitmap] = obj.getConstellationBitMapping();
bitmap = obj.demap((obj.levels ./ obj.scaling)');
end
function [Gp, details] = graypenalty(obj)
[constellation, bit_labels] = obj.getConstellationBitMapping();
num_points = size(constellation, 1);
dist2 = inf(num_points);
for idx = 1:num_points
delta = constellation - constellation(idx, :);
dist2(idx, :) = sum(delta.^2, 2).';
end
dist2(1:num_points+1:end) = inf;
min_dist2 = min(dist2(:));
is_neighbor = abs(dist2 - min_dist2) < 1e-12;
hamming_dist = zeros(num_points);
for idx = 1:num_points
hamming_dist(idx, :) = sum(bit_labels ~= bit_labels(idx, :), 2).';
end
Gp = sum(hamming_dist(is_neighbor)) / nnz(is_neighbor);
if nargout > 1
details = struct( ...
"constellation", constellation, ...
"bit_labels", bit_labels, ...
"neighbor_mask", is_neighbor, ...
"nearest_distance", sqrt(min_dist2));
end
end
function [constellation, bitmap] = getConstellationBitMapping(obj)
if obj.M == 6
constellation = obj.thresholds ./ obj.scaling;
bitmap = obj.demap(reshape(constellation.', [], 1));
bitmap = reshape(bitmap, 5, []).';
else
constellation = (obj.levels ./ obj.scaling)';
bitmap = obj.demap(constellation);
end
end
end
methods (Access = private)
function values = toNumericVector(~, signalLike)
if isa(signalLike, "Signal")
values = signalLike.signal;
else
values = signalLike;
end
values = values(:).';
end
end

View File

@@ -136,6 +136,7 @@ classdef PAMsource
%%%%%% Duobinary %%%%%%%%%%%
% this is translation from user input to db_mode which was added later... "precode" und "encode" sind auch besser zu verstehen an dieser stelle daher hab ichs gelassen
if obj.db_precode
obj.duobinary_mode = db_mode.db_precoded;
end
@@ -148,10 +149,13 @@ classdef PAMsource
case db_mode.no_db
case db_mode.db_precoded
symbols = Duobinary().precode(symbols);
% symbols = Duobinary().precode(symbols);
symbols = Partialresponse().precode(symbols);
case db_mode.db_encoded
symbols = Duobinary().precode(symbols);
symbols = Duobinary().encode(symbols);
% symbols = Duobinary().precode(symbols);
% symbols = Duobinary().encode(symbols);
symbols = Partialresponse().precode(symbols);
symbols = Partialresponse().encode(symbols);
end

View File

@@ -1,12 +1,65 @@
classdef Signalgenerator
%NAME Summary of this class goes here
% Detailed explanation goes here
%SIGNALGENERATOR Generate simple information-level test sequences.
%
% This class creates Informationsignal objects for quick simulations,
% mapper checks, and theory scripts. Use the "form" option to select
% the generated sequence type.
%
% Examples:
%
% % Sine wave
% sig = Signalgenerator( ...
% "form", signalform.sine, ...
% "length", 1024, ...
% "fs", 1000, ...
% "fsig", 50).process();
%
% % Deterministic random bit matrix, 2 bits per row
% bits = Signalgenerator( ...
% "form", signalform.random, ...
% "length", 1024, ...
% "dimension", 2, ...
% "randkey", 3).process();
%
% % PRMS bit matrix for PAM-4 mapping. M selects the mapper-ready bit
% % shape. If length is omitted, the output length is derived from
% % order, as in the old PAMsource.
% bits = Signalgenerator( ...
% "form", signalform.prms, ...
% "M", 4, ...
% "order", 7).process();
% symbols = PAMmapper(4, 0).map(bits);
%
% % PRMS for PAM-6: 5 bits map to 2 PAM-6 symbols. M = 6 returns
% % the 1-D bit vector expected by PAMmapper.
% bits = Signalgenerator( ...
% "form", signalform.prms, ...
% "M", 6, ...
% "order", 7).process();
% symbols = PAMmapper(6, 0).map(bits);
%
% % Map, demap, and check BER
% mapper = PAMmapper(8, 0);
% bits = Signalgenerator( ...
% "form", signalform.prms, ...
% "length", 1024, ... % explicit output length override
% "dimension", 3, ...
% "order", 5).process();
% symbols = mapper.map(bits);
% rxBits = mapper.demap(symbols);
% [checkedBits, errors, ber] = calc_ber(rxBits, bits.signal);
properties(Access=public)
form
length
fs
fsig
dimension
M
order
randkey
skip
bruijn
end
@@ -17,9 +70,15 @@ classdef Signalgenerator
arguments
options.form signalform = signalform.sine
options.length double = 1024
options.length double = []
options.fs double = 1000 %Hz sampling
options.fsig double = 50 % Hz fundamental frex e.g. of the sine or sawtooth
options.dimension double = 1
options.M double = []
options.order double = 7
options.randkey double = 0
options.skip double = 0
options.bruijn logical = false
end
%
@@ -30,6 +89,15 @@ classdef Signalgenerator
end
end
if ~isempty(obj.M)
obj.validate_pam_format();
obj.dimension = obj.mapper_bit_dimension();
end
if isempty(obj.length)
obj.length = obj.default_length();
end
end
function signalclass_out = process(obj)
@@ -69,9 +137,15 @@ classdef Signalgenerator
% Generate sine wave
signal = A * sin(2*pi*f*t);
case signalform.noise
s = RandStream('twister','Seed',obj.randkey);
signal = randn(s, 1, obj.length);
case signalform.random
signal = obj.build_random_data();
case signalform.prms
signal = obj.build_prms_data();
end
signal = obj.format_for_mapper(signal);
end
@@ -82,6 +156,361 @@ classdef Signalgenerator
% Cant be seen from outside! So put all your functions here that can/
% shall not be called from outside
function signal = build_random_data(obj)
arguments(Input)
obj
end
s = RandStream('twister','Seed',obj.randkey);
signal = randi(s, [0 1], obj.length, obj.dimension);
end
%BUILD_PRMS_DATA Generate pseudo-random multi-level sequence bits.
%
% The output is a binary matrix with size:
% obj.length x obj.dimension
%
% Each row is one generated bit group / symbol time. Each column is
% one parallel PRMS bit stream. For conventional PAM formats this
% means:
% PAM-2: dimension = 1
% PAM-4: dimension = 2
% PAM-8: dimension = 3
%
% PAM-6 is special in this codebase: the mapper consumes 5 bits and
% maps them to two PAM-6 symbols. Prefer M = 6 to generate the 1-D
% vector expected by PAMmapper(6, ...).map(...). The legacy
% dimension = 5 path still returns the unflattened bit matrix.
%
% Relevant Signalgenerator options:
% length Number of generated PRMS bit groups / rows.
% dimension Number of parallel bit streams per row.
% M Optional PAM format. When set, dimension is selected
% automatically and PAM-6 output is mapper-ready.
% order User-facing sequence order, matching the old
% PAMsource behavior. If length is omitted, it sets the
% output length. Internally it is converted to the PRMS
% order per stream.
% Default output length is 2^(order-1) for all mapper
% shapes. PAM-6 still reduces the internal PRMS order
% to keep the register length manageable.
% skip Number of PRMS symbols to advance before output. This
% is useful when different blocks should use shifted
% sections of the same deterministic sequence.
% bruijn If true, enables de Bruijn-style zero-symbol insertion
% as in the legacy MOVE-IT generator.
%
% The implementation mirrors the legacy MOVE-IT/prms_c register
% logic in plain MATLAB, so no mex build is required.
function signal = build_prms_data(obj)
arguments(Input)
obj
end
obj.validate_prms_parameters();
state.dimension = obj.dimension;
state.order = obj.internal_prms_order();
state.periodicity = state.dimension * state.order;
state.bl = obj.length;
state.bruijn = obj.bruijn;
state.bruijn_counter = -1;
state.srgtaps = Signalgenerator.srgtap_masks(state.periodicity);
mat_table = Signalgenerator.build_prms_mapping(state.dimension);
state.mat_table = mat_table * 2.^(0:state.dimension-1).';
state.reg_mask = 2^state.periodicity - 1;
state.data_mask = 2^(state.periodicity - 1);
tmp_reg = 2^49 - 1;
tmp_reg = Signalgenerator.advance_prbs_register(tmp_reg, state.srgtaps, 1, state.reg_mask);
state.reg = zeros(state.dimension, 1);
state.reg(1) = tmp_reg;
modulo_mask = 2^state.dimension - 1;
offset = (2^state.periodicity - 1) / modulo_mask;
for n = 2:state.dimension
tmp_reg = Signalgenerator.advance_prbs_register(tmp_reg, state.srgtaps, offset, state.reg_mask);
state.reg(n) = tmp_reg;
end
if obj.skip > 0
state = Signalgenerator.advance_prms_state(state, obj.skip);
end
[data_out, ~] = Signalgenerator.generate_prms_block(state, state.bl);
signal = data_out.';
end
function validate_prms_parameters(obj)
if obj.length < 1 || fix(obj.length) ~= obj.length
error("Signalgenerator:InvalidLength", ...
"PRMS length must be a positive integer.");
end
if obj.dimension < 1 || fix(obj.dimension) ~= obj.dimension
error("Signalgenerator:InvalidDimension", ...
"PRMS dimension must be a positive integer.");
end
if obj.order < 1 || fix(obj.order) ~= obj.order
error("Signalgenerator:InvalidOrder", ...
"PRMS order must be a positive integer.");
end
if obj.skip < 0 || fix(obj.skip) ~= obj.skip
error("Signalgenerator:InvalidSkip", ...
"PRMS skip must be a nonnegative integer.");
end
if obj.dimension * obj.internal_prms_order() > 48
error("Signalgenerator:UnsupportedOrder", ...
"PRMS dimension * internal PRMS order must be <= 48.");
end
end
function validate_pam_format(obj)
if ~ismember(obj.M, [2 4 6 8 16])
error("Signalgenerator:InvalidPAMFormat", ...
"M must be one of 2, 4, 6, 8, or 16.");
end
end
function length = default_length(obj)
switch obj.form
case {signalform.random, signalform.prms}
if obj.is_pam6_shape()
% PAM-6 consumes 5 input bits for 2 output symbols.
% To keep the symbol count aligned with the other
% PAM formats, reduce the number of generated rows
% before flattening to the mapper-ready bit vector.
length = 2^max(0, obj.order - 2);
else
length = 2^max(0, obj.order - 1);
end
otherwise
length = 1024;
end
if obj.form == signalform.prms && length > 2^20
error("Signalgenerator:AutoLengthTooLarge", ...
"Auto-derived PRMS length would be %d samples, which is too large for an implicit default. Pass an explicit length if you really want a longer sequence.", ...
length);
end
end
function prms_order = internal_prms_order(obj)
bits_per_symbol = obj.prms_parallel_width();
prms_order = max(1, floor(obj.order / bits_per_symbol));
end
function width = prms_parallel_width(obj)
if ~isempty(obj.M)
width = obj.mapper_bit_dimension();
else
width = obj.dimension;
end
end
function dimension = mapper_bit_dimension(obj)
if obj.M == 6
dimension = 5;
else
dimension = log2(obj.M);
end
end
function tf = is_pam6_shape(obj)
tf = (~isempty(obj.M) && obj.M == 6) || (isempty(obj.M) && obj.dimension == 5);
end
function signal = format_for_mapper(obj, signal)
if ~isempty(obj.M) && obj.M == 6
signal = reshape(signal.', [], 1);
signal = signal(1:end - mod(numel(signal), 5));
end
end
end
methods (Static, Access=private)
function state = advance_prms_state(state, number_symbols)
[~, state] = Signalgenerator.generate_prms_block(state, number_symbols);
end
function [data_out, state] = generate_prms_block(state, block_length)
data_out = zeros(state.dimension, block_length);
counter = 0;
while counter < block_length
if state.bruijn == 2
counter = counter + 1;
state.bruijn = 1;
continue;
end
for n = 1:state.dimension
prms_tmp_value = false;
for m = 1:state.dimension
prms_tmp_value = xor(prms_tmp_value, ...
state.reg(m) >= state.data_mask && ...
bitand(state.mat_table(m), 2^(n-1)) ~= 0);
end
data_out(n, counter + 1) = prms_tmp_value;
end
for n = 1:state.dimension
state.reg(n) = Signalgenerator.advance_prbs_register( ...
state.reg(n), state.srgtaps, 1, state.reg_mask);
end
counter = counter + 1;
if state.bruijn == 1
if state.bruijn_counter < 0
if any([~sum(data_out(:, counter)) state.order == 1])
state.bruijn_counter = state.bruijn_counter - 1;
if any([state.bruijn_counter == -state.order state.order == 1])
state.bruijn_counter = 2^state.periodicity - 1;
state.bruijn = 2;
end
else
state.bruijn_counter = -1;
end
else
state.bruijn_counter = state.bruijn_counter - 1;
if state.bruijn_counter == 0
state.bruijn_counter = 2^state.periodicity - 1;
state.bruijn = 2;
end
end
end
end
end
function reg = advance_prbs_register(reg, tap_masks, number_steps, reg_mask)
for idx = 1:number_steps
feedback = false;
for tap_idx = 1:numel(tap_masks)
if tap_masks(tap_idx) ~= 0
feedback = xor(feedback, bitand(reg, tap_masks(tap_idx)) ~= 0);
end
end
reg = bitand(reg * 2 + double(feedback), reg_mask);
end
end
function mat_table = build_prms_mapping(dimension)
if dimension > 2
bchpolynomial = zeros(1, dimension + 1);
bchpolynomial([1 Signalgenerator.srgtaps(dimension) + 1]) = 1;
betatable = fliplr(Signalgenerator.cyclgen_local(2^dimension - 1, bchpolynomial).');
elseif dimension == 2
betatable = [0 1; 1 0; 1 1];
else
betatable = 1;
end
binvec = 2.^(dimension-1:-1:0);
modulo_mask = 2^dimension - 1;
betatable_sort_forward = sum(repmat(binvec, 2^dimension - 1, 1) .* betatable, 2);
[~, betatable_sort_inverse] = sort(betatable_sort_forward);
betatable_sort_inverse = betatable_sort_inverse - 1;
dividend = zeros(dimension + 1, 2);
dividend([dimension - Signalgenerator.srgtaps(dimension) + 1 end], 1) = 1;
h = zeros(dimension, 2);
for digit = 1:dimension
h(digit, 1:2) = dividend(digit, 1:2);
dividend(digit, 1:2) = [0 0];
newdiv_binary = xor( ...
betatable(dividend(digit + 1, 2) + 1, :) * dividend(digit + 1, 1), ...
betatable(rem(h(digit, 2) + 1, modulo_mask) + 1, :) * h(digit, 1));
dividend(digit + 1, 1) = sum(newdiv_binary) > 0;
if (digit ~= dimension) && dividend(digit + 1, 1)
dividend(digit + 1, 2) = dividend(digit + 1, 1) * ...
betatable_sort_inverse(sum(newdiv_binary .* binvec));
else
dividend(digit + 1, 2) = 0;
end
end
mat_table = betatable(h(:, 2) + 1, :);
end
function code = cyclgen_local(columns, polynomial)
rows = log2(columns + 1);
code = zeros(rows, columns);
code(1, 1) = 1;
for s = 2:columns
code(:, s) = [0; code(1:end-1, s-1)];
if code(end, s-1)
code(:, s) = rem(code(:, s) + polynomial(1:end-1).', 2);
end
end
end
function taps = srgtaps(inx)
data = {[1] ...
[2 1] ...
[3 1] ...
[4 1] ...
[5 2] ...
[6 1] ...
[7 1] ...
[8 7 2 1] ...
[9 4] ...
[10 3] ...
[11 2] ...
[12 10 2 1] ...
[13 8 5 3] ...
[14 12 11 1] ...
[15 1] ...
[16 15 12 10] ...
[17 3] ...
[18 7] ...
[19 10 9 3] ...
[20 3] ...
[21 2] ...
[22 1] ...
[23 5] ...
[24 11 5 2] ...
[25 3] ...
[26 23 15 13] ...
[27 23 22 17] ...
[28 3] ...
[29 2] ...
[30 27 10 9] ...
[31 3] ...
[32 16 7 2] ...
[33 13] ...
[34 17 12 8] ...
[35 2] ...
[36 11] ...
[37 22 14 2] ...
[38 27 6 5] ...
[39 4] ...
[40 29 27 23] ...
[41 3] ...
[42 34 31 30] ...
[43 27 22 5] ...
[44 39 35 18] ...
[45 39 28 4] ...
[46 40 31 18] ...
[47 5] ...
[48 19 9 1]};
taps = data{inx};
end
function masks = srgtap_masks(inx)
taps = Signalgenerator.srgtaps(inx);
masks = zeros(1, 4);
masks(1:numel(taps)) = 2.^(taps - 1);
end
end
end

View File

@@ -300,8 +300,8 @@ classdef Filter < handle
xline(fcut_,'LineStyle',':','LineWidth',1,'HandleVisibility','off','Color',p.Color);
yline([-3, -6, -9],'LineStyle',':','LineWidth',1,'HandleVisibility','off');
xlim([0 fc.*2].*1e-9);
ylim([ninedB-6, 2]);
% xlim([0 fc.*2].*1e-9);
% ylim([ninedB-6, 2]);
legend
end

View File

@@ -46,7 +46,7 @@ classdef Optical_Demultiplex < handle
if isempty(obj.wavelengthplan)
obj.wavelengthplan = signalclass_in.lambda; %meter
else
if all(500e-9 < obj.wavelengthplan) && all(obj.wavelengthplan < 1500e-9) %check if given in nm
if all(500 < obj.wavelengthplan) && all(obj.wavelengthplan < 1500) %check if given in nm
obj.wavelengthplan = obj.wavelengthplan.*1e-9;
end
end

View File

@@ -0,0 +1,82 @@
classdef CTLE < handle
properties(Access=public)
Aac_dB
Adc_dB
f_p1
f_p2
plot
end
methods(Access=public)
function obj = CTLE(options)
arguments
options.Aac_dB = 0;
options.Adc_dB = -6;
options.f_p1 = 1.5e9;
options.f_p2 = 5e9;
options.plot = 0;
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
end
function data_out = process(obj, data_in)
x = data_in.signal(:);
Fs = data_in.fs;
N = length(x);
% CTLE Transfer Function Parameters
A_ac = 10^(obj.Aac_dB/20);
A_dc = 10^(obj.Adc_dB/20);
w1 = 2*pi*obj.f_p1;
w2 = 2*pi*obj.f_p2;
% Frequency Domain Conversion
X = fft(x);
% Generating Frequency Axis In The Range Of [-Fs/2,Fs/2]
f_fft = (0:N-1).' * (Fs/N);
f_signed = f_fft;
idxNeg = f_signed > Fs/2;
f_signed(idxNeg) = f_signed(idxNeg) - Fs;
f_pos = abs(f_signed);
w_pos = 2*pi*f_pos;
s_pos = 1j*w_pos;
% Calculate CTLE Transfer Function
H_pos = A_ac*w2 .* (s_pos + (A_dc/A_ac)*w1) ./ ((s_pos + w1).*(s_pos + w2));
% Enforce H(-f) = conj(H(f)) For Negative Bins
H = H_pos;
H(idxNeg) = conj(H_pos(idxNeg));
% Apply CTLE
Y = H .* X;
% Calculate Time Domain Signal
y = ifft(Y, 'symmetric');
data_out = data_in;
data_out.signal = y;
data_out.fs = Fs;
% Plot
if obj.plot
% plot only positive frequencies up to Fs/2
k = 1:floor(N/2)+1;
fplot = f_fft(k);
Hplot = H(k);
figure;
semilogx(fplot, 20*log10(abs(Hplot)+1e-15));
grid on; xlabel('frequency (Hz)'); ylabel('magnitude (dB)');
title('CTLE magnitude on FFT grid');
end
end
end
end

View File

@@ -0,0 +1,130 @@
classdef Electrical_Hybrid < handle
properties(Access=public)
file_path
plot = 0
% If true: perform digital residual-echo cancellation using known TX
cancel_echo = 1
% If true: in addition return v_hyb and v_echo_est
return_intermediates = 1
end
methods(Access=public)
function obj = Electrical_Hybrid(options)
arguments
options.file_path = ''
options.plot = 0
options.cancel_echo = 1
options.return_intermediates = 1
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
end
function [v_fe_rec, v_hyb, v_echo_est] = process(obj, v_ne_tx, v_fe_tx)
% v_ne_tx : Near-End TX signal object (Port 1)
% v_fe_tx : Far-End TX signal object (Port 2)
%
% Output:
% v_hyb : physical hybrid differential output (Port4 - Port3)
% v_echo_est : estimated residual echo due to local TX only
% v_fe_rec : v_hyb - v_echo_est (if cancel_echo enabled), else v_hyb
% --- Load S-Parameters (.s4p) ---
net = sparameters(obj.file_path);
f_s = net.Frequencies(:);
S4 = net.Parameters;
% Extract needed S-parameters for differential output:
% V3 = S31*V1 + S32*V2
% V4 = S41*V1 + S42*V2
% Vhyb = V4 - V3 = (S41-S31)*V1 + (S42-S32)*V2
S31_s = squeeze(S4(3,1,:));
S41_s = squeeze(S4(4,1,:));
S32_s = squeeze(S4(3,2,:));
S42_s = squeeze(S4(4,2,:));
% --- Time-domain signals ---
x1 = v_ne_tx.signal(:);
x2 = v_fe_tx.signal(:);
if length(x2) ~= length(x1)
error('Near-end and far-end signals must have the same length.');
end
N = length(x1);
Fs = v_ne_tx.fs;
% --- Use zero padding to avoid circular convolution artifacts ---
Nfft = 2^nextpow2(2*N); % robust choice
% --- FFT ---
V1 = fft(x1, Nfft);
V2 = fft(x2, Nfft);
% --- Frequency axis for interpolation (signed then abs) ---
f_fft = (0:Nfft-1).' * (Fs/Nfft);
f_signed = f_fft;
idxNeg = f_signed > Fs/2;
f_signed(idxNeg) = f_signed(idxNeg) - Fs; % (-Fs/2, Fs/2]
f_pos = abs(f_signed);
% --- Interpolate S-parameters onto f_pos ---
S31 = interp1(f_s, S31_s, f_pos, 'linear', 'extrap');
S41 = interp1(f_s, S41_s, f_pos, 'linear', 'extrap');
S32 = interp1(f_s, S32_s, f_pos, 'linear', 'extrap');
S42 = interp1(f_s, S42_s, f_pos, 'linear', 'extrap');
% Hermitian symmetry for real time-domain response:
% For negative frequencies enforce conj symmetry.
S31(idxNeg) = conj(S31(idxNeg));
S41(idxNeg) = conj(S41(idxNeg));
S32(idxNeg) = conj(S32(idxNeg));
S42(idxNeg) = conj(S42(idxNeg));
% --- Physical hybrid differential output ---
% Vhyb = (S41-S31)*V1 + (S42-S32)*V2
He = (S41 - S31); % residual echo transfer from local TX
Hr = (S42 - S32); % transfer from far-end TX to output
V_hyb = He .* V1 + Hr .* V2;
% --- Residual echo estimate (digital canceller model) ---
V_echo = He .* V1;
% --- Back to time-domain (take first N samples after padding) ---
v_hyb_full = ifft(V_hyb, 'symmetric');
v_echo_full = ifft(V_echo, 'symmetric');
v_hyb = v_hyb_full(1:N);
v_echo_est = v_echo_full(1:N);
% --- Optional cancellation ---
if obj.cancel_echo
v_fe_rec = v_hyb - v_echo_est;
else
v_fe_rec = v_hyb;
end
if ~obj.return_intermediates
v_hyb = [];
v_echo_est = [];
end
% --- Bring output in the correct form ---
v_fe_rec = Informationsignal(v_fe_rec,"fs",v_fe_tx.fs);
v_hyb = Informationsignal(v_hyb,"fs",v_fe_tx.fs);
v_echo_est = Informationsignal(v_echo_est,"fs",v_fe_tx.fs);
if obj.plot
rfplot(net)
end
end
end
end

View File

@@ -0,0 +1,31 @@
classdef Electrical_Trace < handle
properties(Access=public)
file_path
end
methods(Access=public)
function obj = Electrical_Trace(options)
arguments(Input)
options.file_path = 'C:\Users\magf\Desktop\Desktop\MATLAB-Zeugs\COM Test\Mellitzz\TA_6002_6003_FX_B6_C6_B7_C7_Terminated.s4p'
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
end
function [data_out,timing_error] = process(obj, data_in)
S =
end
end
end

View File

@@ -0,0 +1,114 @@
classdef Electrical_Trace_BiDi < handle
properties(Access=public)
file_path
fsym
rolloff
K_over
plot
test
S_test
end
methods(Access=public)
function obj = Electrical_Trace_BiDi(options)
arguments(Input)
options.file_path = 'C:\Users\magf\Desktop\Desktop\MATLAB-Zeugs\COM Test\Mellitzz\TA_6002_6003_FX_B6_C6_B7_C7_Terminated.s4p'
options.fsym = 0;
options.rolloff = 0;
options.K_over = 1;
options.plot = 0;
options.test = 0;
options.S_test = [0,1;1,0];
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
end
function [b_1,b_2] = process(obj, a_1, a_2)
% Rx Signal Calculation Using S-Paramters For a 2-Port Network
% [B_1(f); B_2(f)] = [S_11(f), S_12(f); S_21(f), S_22(f)] * [A_1(f); A_2(f)]
% Initialize Rx Time Domain Signals
b_1 = a_2;
b_2 = a_1;
% Loading and Extract S-Paramters
net = sparameters(obj.file_path);
f_s = net.Frequencies(:);
if ~obj.test
S2 = net.Parameters(1:2, 1:2, :);
else
S2 = repmat(obj.S_test,1,1,size(f_s,1));
end
S11_s = squeeze(S2(1,1,:));
S12_s = squeeze(S2(1,2,:));
S21_s = squeeze(S2(2,1,:));
S22_s = squeeze(S2(2,2,:));
% Setup Time Domain Signals
x1 = a_1.signal(:);
x2 = a_2.signal(:);
N = length(x1);
assert(length(x2)==N, 'a_1 and a_2 must have same length');
% Extract Time Domain Signal Frequencies
if obj.fsym == 0 && obj.rolloff == 0 && obj.K_over == 1
Fs = a_1.fs;
else
Fs = (1+obj.rolloff)*obj.fsym;
end
% Calculate Frequency Domain Signals
A1 = fft(x1);
A2 = fft(x2);
% Calculate Frequency Axis [-Fs/2,...,Fs/2]
f_fft = (0:N-1).' * (Fs/N);
f_signed = f_fft;
idxNeg = f_signed > Fs/2;
f_signed(idxNeg) = f_signed(idxNeg) - Fs; % Now in (-Fs/2, Fs/2]
% Absolute Value Used for Interpolation
f_pos = abs(f_signed);
% Interpolate S-Parameters onto f_pos
S11_p = interp1(f_s, S11_s, f_pos, 'linear', 'extrap');
S12_p = interp1(f_s, S12_s, f_pos, 'linear', 'extrap');
S21_p = interp1(f_s, S21_s, f_pos, 'linear', 'extrap');
S22_p = interp1(f_s, S22_s, f_pos, 'linear', 'extrap');
% Apply Hermitian Symmetry: S(-f) = conj(S(f))
S11 = S11_p; S12 = S12_p; S21 = S21_p; S22 = S22_p;
S11(idxNeg) = conj(S11_p(idxNeg));
S12(idxNeg) = conj(S12_p(idxNeg));
S21(idxNeg) = conj(S21_p(idxNeg));
S22(idxNeg) = conj(S22_p(idxNeg));
% Compute Rx Frequency Domain Signals
B1 = S11 .* A1 + S12 .* A2;
B2 = S21 .* A1 + S22 .* A2;
% Calculate Rx Time Domain Signals
b_1.signal = ifft(B1, 'symmetric');
b_2.signal = ifft(B2, 'symmetric');
if obj.plot
rfplot(net)
end
end
end
end

View File

@@ -22,7 +22,7 @@ classdef Photodiode
options.responsivity = 1;
options.dark_current = 0;
options.temperature = 20;
options.nep = 0; %(Moveit IMDD Standard: 1.8e-11) noise effective power in pA/sqrt(Hz); usually between 10-20 pA; see J.Leibrich Diss/ S. Pachnicke Slides
options.nep = 0; %(Moveit IMDD Standard: 1.8e-11 == current noise density in A/sqrt(Hz)! ); usually between 10-20 pA/sqrt(Hz); see J.Leibrich Diss/ S. Pachnicke Slides
options.randomkey = 1;
end
@@ -72,9 +72,16 @@ classdef Photodiode
yout = yout + shot_noise;
% Thermal Noise
% 2026 comment: obj.nep is not the correct naming, but math-wise everything is fine!
% (2 * k * T / R ) -> A^2/Hz -> is the psd of thermal noise
% earlier: move it's 1.8e-11 is current noise density -> A/sqrt(Hz)
% power (of white process) is simple multiplication of PSD(f) and B
% NEP is noise equivalent power, see Dissertation j. Leibrich
% P. 121 or Stephan Pachnicke Optical Comm. Lecture Slides
if obj.nep == 0
if obj.nep == 0 %
nep_squared = (2 * k * T / R ) ; %squared
else
nep_squared = obj.nep^2;

View File

@@ -0,0 +1,210 @@
classdef Partialresponse
%PARTIALRESPONSE Generalized symbol-domain partial-response coding.
properties
M = []
order = 1
end
methods
function obj = Partialresponse(options)
arguments
options.M = [];
options.order = 1;
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
end
function signal = precode(obj, signal, options)
arguments
obj
signal
options.M = []
end
[data, issignal, signalclass] = obj.unpackSignal(signal);
M = obj.resolveM(data, options.M);
a = obj.amplitudeToIndex(data, M);
h = arrayfun(@(k) nchoosek(obj.order, k), 0:obj.order);
u = zeros(size(a));
state = zeros(1, obj.order);
start_idx = 1;
if obj.order == 1
% Match the legacy Duobinary class exactly: keep the first
% precoded symbol at zero state and start the recursion at k=2.
start_idx = 2;
end
for k = start_idx:numel(a)
u(k) = mod(a(k) - sum(h(2:end).*state), M);
state = [u(k) state(1:end-1)];
end
data_out = obj.indexToPamAmplitude(u, M);
signal = obj.packSignal(data_out, issignal, signalclass);
end
function signal = encode(obj, signal, options)
arguments
obj
signal
options.M = []
end
[data, issignal, signalclass] = obj.unpackSignal(signal);
M = obj.resolveM(data, options.M);
u = obj.amplitudeToIndex(data, M);
h = arrayfun(@(k) nchoosek(obj.order, k), 0:obj.order);
y = zeros(size(u));
state = zeros(1, obj.order);
center = ((M - 1) * sum(h)) / 2;
for k = 1:numel(u)
pr_state = [u(k) state];
y(k) = sum(h .* pr_state) - center;
state = pr_state(1:end-1);
end
y = y ./ obj.encodedScaling(M, obj.order);
signal = obj.packSignal(y, issignal, signalclass);
end
function signal = decode(obj, signal, options)
arguments
obj
signal
options.M = []
end
[data, issignal, signalclass] = obj.unpackSignal(signal);
M = obj.resolveEncodedM(data, options.M);
h = arrayfun(@(k) nchoosek(obj.order, k), 0:obj.order);
idx_to_amp = nan(1, (M-1)*sum(h) + 1);
center = ((M - 1) * sum(h)) / 2;
scaling = obj.encodedScaling(M, obj.order);
for n = 0:(M^(obj.order+1)-1)
state = zeros(1, obj.order+1);
tmp = n;
for k = 1:numel(state)
state(k) = mod(tmp, M);
tmp = floor(tmp/M);
end
y_idx = sum(h .* state);
idx_to_amp(y_idx + 1) = (y_idx - center) / scaling;
end
alphabet = unique(idx_to_amp);
a = zeros(size(data));
for k = 1:numel(data)
[~, pos] = min(abs(data(k) - alphabet));
y_idx = find(idx_to_amp == alphabet(pos), 1) - 1;
a(k) = mod(y_idx, M);
end
data_out = obj.indexToPamAmplitude(a, M);
signal = obj.packSignal(data_out, issignal, signalclass);
end
end
methods (Access=private)
function [data, issignal, signalclass] = unpackSignal(~, signal)
issignal = isa(signal, 'Signal');
if issignal
signalclass = signal;
data = signal.signal;
else
signalclass = [];
data = signal;
end
data = double(data(:));
end
function signal = packSignal(~, data, issignal, signalclass)
if issignal
signalclass.signal = data;
signal = signalclass;
else
signal = data;
end
end
function M = resolveM(obj, data, M)
if isempty(M)
M = obj.M;
end
if isempty(M)
M = numel(unique(round(data, 12)));
end
end
function M = resolveEncodedM(obj, data, M)
if isempty(M)
M = obj.M;
end
if isempty(M)
I = numel(unique(round(data, 12)));
if obj.order == 1
M = (I + 1) / 2;
else
error('Partialresponse:NeedM', ...
'Specify M when decoding higher-order partial-response signals.');
end
end
end
function a = amplitudeToIndex(obj, data, M)
levels = obj.pamLevels(M);
scaling = obj.pamScaling(M);
amp = round(data(:) * scaling);
a = (amp + (M - 1)) / 2;
end
function data = indexToPamAmplitude(obj, a, M)
scaling = obj.pamScaling(M);
data = (2*a(:) - (M - 1)) / scaling;
end
function levels = pamLevels(~, M)
levels = -(M-1):2:(M-1);
end
function scaling = pamScaling(~, M)
try
mapper = PAMmapper(M, 0);
scaling = mapper.scaling;
catch ME
error('Partialresponse:UnsupportedM', ...
'Unsupported PAM order for Partialresponse: %s', ME.message);
end
end
function scaling = encodedScaling(~, M, order)
h = arrayfun(@(k) nchoosek(order, k), 0:order);
center = ((M - 1) * sum(h)) / 2;
y = zeros(M^(order+1), 1);
for n = 0:(numel(y)-1)
state = zeros(1, order+1);
tmp = n;
for k = 1:numel(state)
state(k) = mod(tmp, M);
tmp = floor(tmp/M);
end
y(n + 1) = sum(h .* state) - center;
end
scaling = sqrt(mean(y.^2));
end
end
end

View File

@@ -0,0 +1,394 @@
classdef Copy_of_VNLE < handle
% Implementation of plain and simple FFE.
% 1) Training mode (stable performance when you use NLMS)
% 2) Decision directed mode
% Eq = VNLE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",[0.0004 0.0005 0.0006],"mu_tr",0,"order",[25,2,2],"sps",2,"decide",1);
% Somehow it is not possible to use only 1 nonlinear order
properties
sps % usually 2
order
e
e_dc
error
len_tr
mu_tr
epochs_tr
mu_dd
epochs_dd
mu_dc
constellation
decide
save_debug = 0;
debug_struct
optmize_mus = 0;
mu_optimization
mu_optimization_iter = 0;
x_norm
ce
ie2
ie3
end
methods
function obj = Copy_of_VNLE(options)
arguments(Input)
options.sps = 2;
options.order = [15,2,2];
options.len_tr = 4096;
options.mu_tr = 0;
options.epochs_tr = 5;
options.mu_dd = 1e-5;
options.epochs_dd = 5;
options.mu_dc = 0;
options.decide = false;
options.save_debug = 0;
options.optmize_mus = 0;
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
obj.error = 0;
obj.e_dc = 0;
end
function [X,N] = process(obj, X, D)
% actual processing of the signal (steps 1. - 3.)
% 1 normalize RMS
X = X.normalize("mode","rms");
obj.constellation = unique(D.signal);
obj.x_norm = obj.calcPowerNormalization(X.signal);
obj.ce = obj.calcVNLEMemoryLength(obj.order);
[obj.ie2,obj.ie3] = obj.calcIndiceVectors(obj.order);
obj.e = zeros( sum(obj.ce) ,1);
obj.e_dc = 0;
if obj.optmize_mus
obj.optimizeMus(X.signal,D.signal);
obj.e = zeros(sum(obj.ce),1);
obj.e_dc = 0;
end
% Training Mode
training = 1;
showviz = 0;
obj.equalize(X.signal, D.signal,obj.mu_tr,obj.epochs_tr,obj.len_tr,training,showviz);
% Decision Directed Mode
N = X.length;
training = 0;
showviz = 0;
[signal,decision]=obj.equalize(X.signal, D.signal,obj.mu_dd,obj.epochs_dd,N,training,showviz);
% Output Signal
if obj.decide
X.signal = decision;
else
X.signal = signal;
end
X.fs = D.fs; %change sampling frequency of outgoing signal from fdac e.g. 2 sps to symbol spaced = fsym
lbdesc = [num2str(obj.order),' tap FFE'];
X = X.logbookentry(lbdesc); % append to logbook
N = X;
N = X - D;
end
function [y,d_hat] = equalize(obj,x,d,mu,epochs,N,training,showviz)
arguments
obj
x
d
mu
epochs
N
training
showviz
end
if all(mu == mu(1))
% mu = mu(1);
mu = diag(ones(1,sum(obj.ce))*mu(1));
else
mu = diag([ones(1,obj.ce(1))*mu(1) ...
ones(1,obj.ce(2))*mu(2) ...
ones(1,obj.ce(3))*mu(3) ]);
end
x = [zeros(floor(obj.order(1)/2),1); x; zeros(obj.order(1),1)];
if showviz
f = figure(111);
subplot(2,2,1:2);
hold on
a = scatter(1:numel(x),x,1,'.');
a2 = scatter(1,1,1,'.');
a3 = scatter(1,1,2,'.');
a4 = xline(1);
ylim([-3 3])
xlim([0 length(x)]);
subplot(2,2,3:4)
c = stem(obj.e);
ylim([-1 1])
drawnow
end
for epoch = 1 : epochs
symbol = 0;
for sample = 1 : obj.sps : N
symbol = symbol+1;
% x_in = x(obj.order(1)+sample+(obj.sps-1):-1:sample+obj.sps);
x_in = x(obj.order(1)+sample-1:-1:sample);
x_in = obj.calcVNLENonlinVecs(x_in,obj.ie2,obj.ie3,obj.order,obj.x_norm);
y(symbol,1) = obj.e_dc + obj.e.' * x_in; % Calculating output of LMS __ * |
if training
err = y(symbol) - d(symbol); % Instantaneous error
else
[~,symbol_idx] = min(abs(y(symbol) - obj.constellation)); % decision for closest constellation point
d_hat(symbol,1) = obj.constellation(symbol_idx);
err = y(symbol) - d_hat(symbol); % Instantaneous error
end
if ~all(mu==0,'all') %mu has not only zeros
obj.e = obj.e - ( (mu * x_in) * err ) ; % Weight update rule of LMS
else
normalizationfactor = (x_in.' * x_in);
obj.e = obj.e - err * x_in / normalizationfactor; % Weight update rule of NLMS
end
if obj.mu_dc ~= 0
obj.e_dc = obj.e_dc - obj.mu_dc * err;
end
if mod(sample,100) == 1 && showviz
a2.XData = 1:2*numel(y);
a2.YData = repelem(y, 2);
a3.XData = 1:2*numel(d_hat);
a3.YData = repelem(d_hat, 2);
a4.Value = sample;
% b.YData = x(symbol:symbol+500);
c.YData = obj.e;
drawnow;
end
obj.error(epoch,symbol) = err * err'; % Instantaneous square error
if obj.save_debug
obj.debug_struct.error(epoch,symbol) = err * err';
if training
obj.debug_struct.error_tr(epoch,symbol) = err * err';
end
end
end
end
end
function optimizeMus(obj,x,d)
mu_range = [1e-5, 1e-2];
mu_dc_range = [1e-5, 1e-1];
vars = [optimizableVariable("mu_tr",mu_range,"Transform","log"), ...
optimizableVariable("mu_dd",mu_range,"Transform","log")];
optimize_mu_dc = obj.mu_dc ~= 0;
if optimize_mu_dc
vars = [vars, optimizableVariable("mu_dc",mu_dc_range,"Transform","log")];
end
obj.mu_optimization_iter = 0;
obj.mu_optimization = bayesopt(@(p)obj.muObjective(p,x,d),vars, ...
"MaxObjectiveEvaluations",10, ...
"AcquisitionFunctionName","expected-improvement-plus", ...
"IsObjectiveDeterministic",false, ...
"Verbose",0, ...
"PlotFcn",[]);
obj.mu_tr = obj.mu_optimization.XAtMinObjective.mu_tr;
obj.mu_dd = obj.mu_optimization.XAtMinObjective.mu_dd;
if optimize_mu_dc
obj.mu_dc = obj.mu_optimization.XAtMinObjective.mu_dc;
end
objective_db = 10*log10(obj.mu_optimization.MinObjective);
if optimize_mu_dc
fprintf("\nVNLE mu opt done: mu_tr=%9.3e, mu_dd=%9.3e, mu_dc=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB\n", ...
obj.mu_tr,obj.mu_dd,obj.mu_dc,obj.mu_optimization.MinObjective,objective_db);
else
fprintf("\nVNLE mu opt done: mu_tr=%9.3e, mu_dd=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB\n", ...
obj.mu_tr,obj.mu_dd,obj.mu_optimization.MinObjective,objective_db);
end
end
function objective = muObjective(obj,params,x,d)
old_debug = obj.save_debug;
old_mu_dc = obj.mu_dc;
obj.save_debug = 1;
optimize_mu_dc = ismember("mu_dc",string(params.Properties.VariableNames));
if optimize_mu_dc
obj.mu_dc = params.mu_dc;
end
obj.e = zeros(sum(obj.ce),1);
obj.e_dc = 0;
obj.debug_struct = struct();
obj.equalize(x,d,params.mu_tr,obj.epochs_tr,obj.len_tr,1,0);
obj.equalize(x,d,params.mu_dd,obj.epochs_dd,numel(x),0,0);
objective = mean(obj.debug_struct.error(end,:),"omitnan");
if ~isfinite(objective)
objective = inf;
end
objective_db = 10*log10(objective);
obj.mu_optimization_iter = obj.mu_optimization_iter + 1;
if optimize_mu_dc
fprintf("\rVNLE mu opt %02d: mu_tr=%9.3e, mu_dd=%9.3e, mu_dc=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB", ...
obj.mu_optimization_iter,params.mu_tr,params.mu_dd,params.mu_dc,objective,objective_db);
else
fprintf("\rVNLE mu opt %02d: mu_tr=%9.3e, mu_dd=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB", ...
obj.mu_optimization_iter,params.mu_tr,params.mu_dd,objective,objective_db);
end
obj.save_debug = old_debug;
obj.mu_dc = old_mu_dc;
end
%% Functions needed During Adaption
function x_in_vnle_format = calcVNLENonlinVecs(~,x_in_block,I_2,I_3,N_,norm_)
% These are the second and third order input signal products of the VNLE EQ
% h1 x_in(k-n1) + h2 x_in(k-n1)*x_in(k-n2) + h3 x_in(k-n1)*x_in(k-n2)*x_in(k-n3)
l1=length(x_in_block);
l2=length(I_2);
l3=length(I_3);
final_length = l1+l2+l3;
x_in_vnle_format = zeros(final_length,1);
idx = l1;
x_in_vnle_format(1:idx) = x_in_block;
if N_(2) > 0
delta_2 = round((N_(1)-N_(2)) / 2);
input_vec_se = x_in_block(delta_2:end) / norm_(2); %TODO normalization step
% Extract columns from I_2
col1 = input_vec_se(I_2(:,1));
col2 = input_vec_se(I_2(:,2));
x2 = col1 .* col2;
x_in_vnle_format(idx+1:idx+l2) = x2;
end
if N_(3) > 0
delta_3 = round((N_(1)-N_(3))/2);
input_vec_th = x_in_block(delta_3:end) / norm_(3);
% Extract columns from I_3
col1 = input_vec_th(I_3(:,1));
col2 = input_vec_th(I_3(:,2));
col3 = input_vec_th(I_3(:,3));
% Perform matrix multiplication
x3 = col1 .* col2 .* col3;
idx = idx+l2;
x_in_vnle_format(idx+1:idx+l3) = x3;
end
end
%% Functions needed for Preparation
function [C] = calcVNLEMemoryLength(~,N)
%calculates the memory length of VNLE
C = zeros(size(N));
for o = 1:numel(N)
switch o
case 1
C(o) = N(o);
case 2
C(o) = N(o)*(N(o)+1) / 2;
case 3
C(o) = N(o)*(N(o)+1)*(N(o)+2) / 6;
end
end
end
function [indvec2nd, indvec3rd] = calcIndiceVectors(~,N)
% Init vectors of 2nd and 3rd order coefficient indices ->
% yield combination with
indvec2nd=[];
indvec3rd=[];
for o = 2:numel(N)
n = N(o);
v = 1:n; % Ursprünglicher Vektor
row = 1;
% Schleifen zur Generierung des Indize Vektors
switch o
case 2
indvec2nd = zeros(n*(n+1)/2, o);
for i = 1:n
for j = i:n
indvec2nd(row, :) = [v(i) v(j)];
row = row + 1;
end
end
case 3
indvec3rd = zeros(n*(n+1)*(n+2)/6, 3);
for i = 1:n
for j = i:n
for k = j:n
indvec3rd(row, :) = [v(i) v(j) v(k)];
row = row + 1;
end
end
end
end
end
end
function powerNorm = calcPowerNormalization(~,v)
powerNorm(1) = sqrt(mean(abs(v ).^2));
powerNorm(2) = sqrt(mean(abs(v.^2).^2));
powerNorm(3) = sqrt(mean(abs(v.^3).^2));
end
end
end

View File

@@ -10,6 +10,12 @@ classdef EQ < handle
training_length %Number of training symbols
training_loops %Number of loops through sequence for training mode
ideal_dfe %Error free DFE decisions
weighted_DFE %Weighted DFE off (0)/on (1)/PDFE (2)
weighted_DFE_mode %Weighted DFE mode
weighted_DFE_d_min %d_min threshold parameter for weighted DFE mode 1
weighted_DFE_I_mode %[a_s, b_s, I_max]-parameters for the weighted DFE
PDFE_coefficient %Coefficient for PDFE
weighted_error %error based on hard- or weighted decision
DB_aim %Aim at duobinary output sequence
@@ -39,7 +45,6 @@ classdef EQ < handle
save_taps
%during simulation
k0
b
@@ -68,6 +73,12 @@ classdef EQ < handle
options.training_length = 1024 %Number of training symbols
options.training_loops = 1 %Number of loops through sequence for training mode
options.ideal_dfe = 0 %Error free DFE decisions
options.weighted_DFE = 0;
options.weighted_DFE_mode = 'R1';
options.weighted_DFE_d_min = 0.5;
options.weighted_DFE_I_mode = [5,0.5,0.6];
options.PDFE_coefficient = 0.5;
options.weighted_error = 0; %0: Error based on hard-decision. 1: Error based on weighted decision.
options.DB_aim %Aim at duobinary output sequence
@@ -112,7 +123,7 @@ classdef EQ < handle
signalclass_in.signal = signalclass_in.signal';
signalclass_in.fs = reference_signalclass_in.fs;
% append to logbook
lbdesc = ['EQ '];
signalclass_in = signalclass_in.logbookentry(lbdesc);
@@ -221,7 +232,7 @@ classdef EQ < handle
obj.k0 = obj.delay; % input delay compared to training sequence
error_log = [];
if 1 % obj.active
%% Calculation of the filter coefficients in training based LMS mode
@@ -235,19 +246,11 @@ classdef EQ < handle
cnt = 1;
m = obj.k0+1; % starting symbol index at the delay compared to the training sequence
% n => index in rx data sequence
%Step From: Oversampling(=2) * Startdelay + 1
%Step Width: Oversampling(=2)
%Step To: Oversampling(=2) * Training Length
for n = obj.K*obj.k0+1:obj.K:obj.K*obj.training_length
% m => index in reference sequence
m = m+1;
%
%dc_ = mean(data(obj.Ne(1)+n+(obj.K-1):-1:n+obj.K).');
% cut symbols from rx data sequence
X_1 = data(obj.Ne(1)+n+(obj.K-1):-1:n+obj.K).';
@@ -266,9 +269,7 @@ classdef EQ < handle
e_dfe = b_.'*reference_vec;
error = e_dc + e_ffe - e_dfe - ref_in(m-obj.k0);
%error = e_dc + e_.'*input_vec - b_.'*reference_vec - ref_in(m-obj.k0);
if real(obj.FFEmu)
if obj.l1act
sgn_e = e_;
@@ -289,28 +290,13 @@ classdef EQ < handle
e_dc = e_dc - obj.DCmu*error;
% obj.error_log.e_ffe(cnt,trainloops) = e_ffe;
% obj.error_log.e_dfe(cnt,trainloops) = e_dfe;
% obj.error_log.e_(cnt,trainloops) = error;
cnt = cnt+1;
if obj.Nb(1) > 0
b_ = b_ + obj.DFEmu*error*reference_vec; % Seems like normalized DFE has worse performance
end
% figure(111);stem((e_),'Markersize',2);ylim([-1 1]);title('FFE Filter Taps');
%
% figure(222);
% stem(input_vec);ylim([-3 3]);
% hold on;
% stem(reference_vec);ylim([-3 3]);
% yline(error,'LineWidth',2); title('Input Vector');
% hold off
end
end
%%
% Plot the intermediate coefficients after training mode
obj.b = b_(1:obj.Nb(1));
@@ -341,10 +327,8 @@ classdef EQ < handle
subplot(2,3,6);stem(obj.b3,'Markersize',2);
title('DFE coeff nl 3rd')
xlabel('coefficient index'); ylabel('value'); set(gca,'Fontsize',12)
%set(gcf,'Position',[200 500 700 400])
end
if obj.l1act
neg_lin = find(abs(obj.e) < obj.thres(1));
neg_2nd = find(abs(obj.e2) < obj.thres(2));
@@ -393,6 +377,7 @@ classdef EQ < handle
cnt = 1;
m = 0;
output_vec = zeros(1,floor(length(data_in)/obj.K)); % initilaization of the output vector
output_vec_weighted = zeros(size(output_vec));
dd_DFE = zeros(obj.Nb(1),1);
D_2 = zeros(Nb2,1);
D_3 = zeros(Nb3,1);
@@ -410,7 +395,6 @@ classdef EQ < handle
if obj.load_decisions
pathn = evalin('base','modeldir');
temp = load([pathn, 'MLSE_out', '.mat']) ;
%eval(['dd_out_vals = temp.', 'a', ';']) ;
dd_out_vals=temp.a;
dd_out = zeros(size(data_in));
dd_out(1:2:length(data_in)) = dd_out_vals;
@@ -432,42 +416,103 @@ classdef EQ < handle
end
output_vec(m) = e_dc + input_vec.'*coeff;
y_raw = output_vec(m); % ungeweighteter Equalizer-Ausgang
if ~obj.load_decisions
[~,dd_idx] = min(abs(output_vec(m) - constellation_in_)); % decision for closest constellation point
dd_out(k) = constellation_in_(dd_idx);
end
% ---------- WDFE / PDFE ----------
fb_sym = dd_out(k);
if obj.weighted_DFE ~= 0
% Get constellation
const = obj.constellation_in;
% Half minimum symbol distance (for normalization of |x - xhat|)
const_s = sort(const(:).');
d = diff(const_s);
dmin = min(d(d>0));
halfStep = dmin/2;
% Reliability gamma_k
if output_vec(m) > min(const) && output_vec(m) < max(const)
gamma_k = 1 - abs(output_vec(m) - dd_out(k))/halfStep;
gamma_k = max(0, min(1, gamma_k)); % clamp to [0,1]
else
gamma_k = 1;
end
% f(gamma_k)
if obj.weighted_DFE == 1
switch obj.weighted_DFE_mode
case 'R1'
f_gamma_k = double(gamma_k >= obj.weighted_DFE_d_min);
case 'R2'
f_gamma_k = gamma_k;
case 'I1'
a_s = obj.weighted_DFE_I_mode(1);
b_s = obj.weighted_DFE_I_mode(2);
t = a_s*((gamma_k/b_s) - 1);
f_gamma_k = 0.5*(tanh(t) + 1);
case 'I2'
a_s = obj.weighted_DFE_I_mode(1);
b_s = obj.weighted_DFE_I_mode(2);
Imax = obj.weighted_DFE_I_mode(3);
t = a_s*((gamma_k/b_s) - 1);
f_gamma_k = (Imax/2)*(tanh(t) + 1);
otherwise
error('Unknown weighted_DFE_mode');
end
% Weighted decision
xbar = f_gamma_k*dd_out(k) + (1 - f_gamma_k)*output_vec(m);
elseif obj.weighted_DFE == 2
% PDFE
xbar = obj.PDFE_coefficient*dd_out(k) + (1 - obj.PDFE_coefficient)*output_vec(m);
end
output_vec_weighted(m) = xbar;
fb_sym = xbar;
% output_vec(m) = xbar;
end
if obj.Nb(1) > 0
dd_DFE(2:end) = dd_DFE(1:end-1);
dd_DFE(1) = dd_out(k);
dd_DFE(1) = fb_sym;
if obj.ideal_dfe && m > obj.k0
dd_DFE(1) = ref_in(m-obj.k0);
end
[D_2,D_3] = obj.calc_nl_vecs(dd_DFE,ind_mat_DFE_2nd,ind_mat_DFE_3rd,norm_fac_DFE2,norm_fac_DFE3,delta_DFE2,delta_DFE3,cplx);
[D_2,D_3] = obj.calc_nl_vecs(dd_DFE,ind_mat_DFE_2nd,ind_mat_DFE_3rd,...
norm_fac_DFE2,norm_fac_DFE3,delta_DFE2,delta_DFE3,cplx);
end
if obj.weighted_DFE ~= 0 && obj.weighted_error
% Error weighted decision
error = y_raw - output_vec(m);
else
% Error hard decision
error = y_raw - dd_out(k);
end
% if dd_loop ~= 21
error = output_vec(m) - dd_out(k);
% else
% error = 0;
% end
coeff = coeff - mu_mat*error*conj(input_vec);
% e_save(:,save_ind) = coeff;
% save_ind = save_ind+1;
if 1%mu_mat ~= 0
e_dc = e_dc - obj.DCmu*error;
% error_log(cnt,dd_loop) = e_dc;
cnt = cnt+1;
if 1 % mu_mat ~= 0
e_dc = e_dc - obj.DCmu*error;
cnt = cnt+1;
end
end
end
%figure(2023);plot(error_log(:,1))
% shifting the output sequence by k0 symbols
yout = (circshift(output_vec.',-(obj.k0))).'; %(circshift(dd_out.',-(obj.k0))).';
@@ -536,12 +581,6 @@ classdef EQ < handle
% save frequency response to the work space
if obj.save_taps
% save the FFE coefficients to the work space
% pathn = evalin('base','modeldir');
% eval([obj.field_ffe, ' = obj.e ;']) ;
% eval([obj.field_dfe, ' = b ;']) ;
% eval(['save(''', pathn, '\',obj.filen,''', ''', obj.field_ffe,''', ''',obj.field_dfe,''') ;']) ;
save("coefficients",obj.e, obj.b);
end
@@ -681,19 +720,11 @@ classdef EQ < handle
ind_mat_3rd2 = NaN(N3,3);
count = 1;
% for t = 1:Ne3
% ind_mat_3rd2(count,:) = [t t t];
% count = count + 1;
% end
for t = 1:Ne3
% ind_mat_3rd2(count,:) = [t t t];
% count = count + 1;
for u = t:min(Ne3,t+len_3rd)
for v = unique([t u])
ind_mat_3rd2(count,:) = [t v u];
count = count + 1;
% ind_mat_3rd2(count,:) = [t u u];
% count = count + 1;
end
end
end
@@ -739,3 +770,6 @@ classdef EQ < handle
end
end
% References
% [1] T. J. Wettlin, Experimental Evaluation of Advanced Digital Signal Processing for Intra-Datacenter Systems using Direct-Detection, 2023.
% [Online]. Available: https://nbn-resolving.org/urn:nbn:de:gbv:8:3-2023-00703-8

File diff suppressed because it is too large Load Diff

View File

@@ -1,351 +0,0 @@
classdef FFE_DCremoval_adaptive_mu < handle
% Implementation of plain and simple FFE.
% 1) Training mode (stable performance when you use NLMS)
% 2) Decision directed mode
% Eq = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-4,"mu_tr",0,"order",25,"sps",2,"decide",0);
properties
sps % usually 2
order
e
e_tr
error
len_tr
mu_tr
epochs_tr
mu_dd
epochs_dd
mu_dc
dc_buffer_len
adaptive_mu_mode
ffe_buffer_len
smoothing_buffer_length
smoothing_buffer_update
constellation
decide
end
methods
function obj = FFE_DCremoval_adaptive_mu(options)
arguments(Input)
options.sps = 2;
options.order = 15;
options.len_tr = 4096;
options.mu_tr = 0;
options.epochs_tr = 5;
options.mu_dd = 1e-5;
options.epochs_dd = 5;
options.mu_dc = 0.05;
options.dc_buffer_len = 1;
options.ffe_buffer_len = 1;
options.adaptive_mu_mode = 1;
options.smoothing_buffer_length = 0;
options.smoothing_buffer_update = 0;
options.decide = false;
end
assert(options.dc_buffer_len>0);
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
obj.e = zeros(obj.order,1);
obj.error = 0;
obj.dc_buffer_len = floor(obj.dc_buffer_len);
end
function [X,Noi] = process(obj, X, D)
% actual processing of the signal (steps 1. - 3.)
% 1 normalize RMS
X = X.normalize("mode","rms");
obj.constellation = unique(D.signal);
% if obj.smoothing_buffer_length > 0
% % Apply A1 filter smoothing
% % Calculate the moving sum with the window size N1
% moving_sum = movsum(X.signal, [obj.smoothing_buffer_length,0]);
%
% % Initialize the output smoothed signal
% X.signal = X.signal - (1 / obj.smoothing_buffer_length) * moving_sum;
% end
% Training Mode
training = 1;
obj.equalize(X.signal, D.signal,obj.mu_tr,obj.epochs_tr,obj.len_tr,training);
obj.e_tr = obj.e;
% Decision Directed Mode
N = X.length;
training = 0;
[signal,decision]=obj.equalize(X.signal, D.signal,obj.mu_dd,obj.epochs_dd,N,training);
% Output Signal
if obj.decide
X.signal = decision;
else
X.signal = signal;
end
X.fs = D.fs; %change sampling frequency of outgoing signal from fdac e.g. 2 sps to symbol spaced = fsym
lbdesc = [num2str(obj.order),' tap FFE'];
X = X.logbookentry(lbdesc); % append to logbook
Noi = X - D;
end
function [y,d_hat] = equalize(obj, x, d, mu_lms, epochs, N, training)
% Equalize with adaptive DC-removal, VSS, and parallel-buffered DC updates
% Added: FFE gradient buffering in DD mode (error buffer) with update every obj.dc_buffer_len symbols
arguments
obj
x
d
mu_lms % LMS step-size (or 0 for NLMS)
epochs % number of training/DD epochs
N % number of samples to process
training % boolean flag: true->training mode, false->DD mode
end
if isempty(obj.e)
obj.e = zeros(obj.order,1);
end
% Zero-padding for filter memory
x = [zeros(floor(obj.order/2),1); x; zeros(obj.order,1)];
% Initialize storage
numSymbols = ceil(N/obj.sps);
y = zeros(numSymbols,1);
d_hat = zeros(numSymbols,1);
err = NaN(numSymbols,numel(obj.constellation));
e_dc_save= zeros(numSymbols,1);
% DC-adaptation parameters
P_err = 0; % running error power
alpha = 0.98; % forgetting factor for error power
err_prev = 0; % previous error sample for VSS correlation
gamma_dc = 1e-6; % meta step-size for DC VSS
mu_min = 1e-6; % lower bound for mu_dc
mu_max = 3e-1; % upper bound for mu_dc
% DC removal buffer
L = obj.dc_buffer_len; % buffer length
e_dc_buf = NaN(L,1);
e_dc_est = 0;
% FFE gradient buffer (DD mode only)
L_grad = obj.ffe_buffer_len; % buffer length
if ~training
% each column holds one past gradient of length obj.order
grad_buf = NaN(obj.order, L_grad);
end
smth_buffer = zeros(1, obj.smoothing_buffer_length);
smth_mean = 0;
% Main loop
for epoch = 1:epochs
s = 0;
for sample = 1:obj.sps:N
s = s + 1;
if obj.smoothing_buffer_length > 0
smth_buffer = circshift(smth_buffer,1,2);
smth_buffer(1) = x(sample);
if mod(s, obj.smoothing_buffer_update) == 0
smth_mean = mean(smth_buffer);
end
x(sample:sample+obj.sps-1) = x(sample:sample+obj.sps-1)-smth_mean;
end
U = x(obj.order+sample-1:-1:sample);
%-- 1) filter output with DC correction
y(s) = e_dc_est + obj.e.'*U;
%-- 2) decision
if training
[~, idx] = min(abs(d(s) - obj.constellation));
else
[~, idx] = min(abs(y(s) - obj.constellation));
end
d_hat(s) = obj.constellation(idx);
%-- 3) error
e_val = y(s) - d_hat(s);
if epoch == epochs
err(s,idx) = e_val;
true_err(s,idx) = y(s) - d(s);
end
%-- 4) tap-weight update: training immediate, DD buffered
if training
% immediate update (LMS or NLMS)
if mu_lms ~= 0
obj.e = obj.e - mu_lms * e_val * U;
else
normU = (U.'*U) + eps;
obj.e = obj.e - e_val * U / normU;
end
else
if 0
% buffer gradient
if mu_lms ~= 0
grad = e_val * U;
else
normU = (U.'*U) + eps;
grad = e_val * U / normU;
end
% shift and insert
grad_buf = circshift(grad_buf, 1, 2);
grad_buf(:,1) = grad;
% update once every L symbols
if mod(s, L_grad) == 0
avg_grad = mean(grad_buf, 2, 'omitnan');
if mu_lms ~= 0
obj.e = obj.e - mu_lms * avg_grad;
else
obj.e = obj.e - avg_grad;
end
end
end
end
%-- 5) DC adaptation
if obj.mu_dc ~= 0
if obj.adaptive_mu_mode
% VSS for mu_dc
delta_mu = gamma_dc * e_val * err_prev * (U.'*U);
obj.mu_dc = min(max(obj.mu_dc + delta_mu, mu_min), mu_max);
err_prev = e_val;
% DC buffer update & periodic estimate
P_err = alpha*P_err + (1-alpha)*e_val^2;
mu_dc_norm = obj.mu_dc / (P_err + eps);
else
% DC buffer update & periodic estimate
% P_err = alpha*P_err + (1-alpha)*e_val^2;
% mu_dc_norm = obj.mu_dc / (P_err + eps);
mu_dc_norm = obj.mu_dc;
end
e_dc_buf = circshift(e_dc_buf, 1);
e_dc_buf(1) = e_dc_est - mu_dc_norm * e_val;
if mod(s, L) == 0
e_dc_est = median(e_dc_buf, 'omitnan');
end
P_err_save(s) = P_err;
% Pcorr_save(s) = e_val * err_prev;
Ucorr_save(s) = (U.'*U);
mu_dc_save(s) = mu_dc_norm;
e_dc_save(s) = e_dc_est;
end
% store instantaneous squared error
obj.error(epoch, s) = e_val^2;
end
end
% Optional plotting in DD mode (uncomment if needed)
if 0%~training
constellation = unique(d);
lvlcol = cbrewer2('Paired', numel(constellation)*2);
lvlcol = lvlcol(2:2:end, :);
true_err(true_err==0) = NaN;
true_errmoverr = movsum(true_err, 4096, 'omitnan');
true_errmoverr = true_errmoverr./rms(true_errmoverr);
moverr = movsum(err, [100,100], 'omitnan');
moverr = moverr./rms(moverr);
figure(500); clf
hold on
% 1st subplot: true_errmoverr
% subplot(2,2,1); hold on
% for k = 1:4
% scatter(1:numSymbols, true_errmoverr(:,k), 1, lvlcol(k,:), '.');
% end
% scatter(1:numSymbols, Ucorr_save./rms(Ucorr_save), 1, lvlcol(1,:), '.','DisplayName','Ucorr_save');
% scatter(1:numSymbols, Pcorr_save./rms(Pcorr_save), 1, lvlcol(1,:), '.','DisplayName','P_corr');
% scatter(1:numSymbols, P_err_save, 1, lvlcol(1,:), '.','DisplayName','P_err');
% scatter(1:numSymbols, mu_dc_save, 1, lvlcol(2,:), '.','DisplayName','adapted value of $\mu_{DC}$');
% scatter(1:numSymbols, sum(moverr,2,'omitnan'), 1, lvlcol(1,:), '.','DisplayName','Mov Error $\hat{d}$ - x over all levels');
scatter(1:numSymbols, sum(e_dc_save,2,'omitnan'), 1, lvlcol(2,:), '.','DisplayName','Est. Error that is subtracted');
title('Moving Sum Error');
hold off
legend
% 2nd subplot: moverr
subplot(2,2,2); hold on
for k = 1:4
scatter(1:numSymbols, moverr(:,k), 1, lvlcol(k,:), '.');
end
title('Moving Sum Error');
hold off
legend
% 3rd subplot: err
subplot(2,2,3); hold on
for k = 1:4
scatter(1:numSymbols, err(:,k), 1, lvlcol(k,:), '.');
end
title('Error');
hold off
legend
% 4th subplot: err + obj.constellation'
subplot(2,2,4); hold on
for k = 1:4
scatter(1:numSymbols, err(:,k) + obj.constellation(k), 1, lvlcol(k,:), '.');
end
yline(obj.constellation, '--k');
title('Error + Constellation');
hold off
legend
sgtitle('Error Analysis Subplots');
end
end
end
end

View File

@@ -1,245 +0,0 @@
classdef FFE_Kalman < handle
% Implementation of plain and simple FFE.
% 1) Training mode (stable performance when you use NLMS)
% 2) Decision directed mode
% Eq = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-4,"mu_tr",0,"order",25,"sps",2,"decide",0);
properties
sps % usually 2
order
e
error
len_tr
mu_tr
epochs_tr
mu_dd
epochs_dd
constellation
decide
end
methods
function obj = FFE_Kalman(options)
arguments(Input)
options.sps = 2;
options.order = 15;
options.len_tr = 4096;
options.mu_tr = 0;
options.epochs_tr = 5;
options.mu_dd = 1e-5;
options.epochs_dd = 5;
options.decide = false;
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
obj.e = zeros(obj.order,1);
obj.error = 0;
end
function [X,Noi] = process(obj, X, D)
% actual processing of the signal (steps 1. - 3.)
% 1 normalize RMS
X = X.normalize("mode","rms");
obj.constellation = unique(D.signal);
% Training Mode
training = 1;
showviz = 0;
obj.equalize(X.signal, D.signal,obj.mu_tr,obj.epochs_tr,obj.len_tr,training,showviz);
% Decision Directed Mode
n = X.length;
training = 0;
showviz = 0;
[signal,decision]=obj.equalize(X.signal, D.signal,obj.mu_dd,obj.epochs_dd,n,training,showviz);
% Output Signal
if obj.decide
X.signal = decision;
else
X.signal = signal;
end
X.fs = D.fs; %change sampling frequency of outgoing signal from fdac e.g. 2 sps to symbol spaced = fsym
lbdesc = [num2str(obj.order),' tap FFE'];
X = X.logbookentry(lbdesc); % append to logbook
Noi = X;
Noi = X - D;
end
function [y,d_hat] = equalize(obj,x,d,mio,epochs,N,training,showviz)
arguments
obj
x
d
mio
epochs
N
training
showviz
end
x = [zeros(floor(obj.order/2),1); x; zeros(obj.order,1)];
for epoch = 1 : epochs
symbol = 0;
% Initialization of Kalman filter variables
A = 1; % State transition matrix
H = 1; % Observation matrix
Q = 1e-4; % Process noise covariance
R = 1e-1; % Measurement noise covariance
P = 1; % Initial error covariance
mpi_est = 0; % Initial estimate for MPI noise
K = 0; % Kalman gain
subtract_mpi_est = 1;
for sample = 1 : obj.sps : N
symbol = symbol + 1;
% Get the current input sample and the equalizer output
U = x(obj.order + sample - 1 : -1 : sample);
if subtract_mpi_est || ~training
y(symbol,1) = (obj.e.' * U) - mpi_est .* 1 ; % Subtract MPI estimate
else
y(symbol,1) = obj.e.' * U;
end
% Decision and error calculation
if training
d_hat(symbol,1) = d(symbol);
else
[~, symbol_idx] = min(abs(y(symbol) - obj.constellation)); % Closest constellation point
d_hat(symbol,1) = obj.constellation(symbol_idx);
end
err(symbol) = y(symbol) - d_hat(symbol); % Instantaneous residual error
true_err(symbol) = y(symbol) - d(symbol);
% Kalman filter update to track the MPI noise
% Prediction step
P = A * P * A' + Q; % Update error covariance
K = P * H' / (H * P * H' + R); % Kalman gain
% Update step
mpi_est_new = mpi_est + K * (err(symbol) - H * mpi_est); % MPI noise estimation
alpha=0;
mpi_est = alpha * mpi_est + (1 - alpha) * mpi_est_new;
P = (1 - K * H) * P; % Update error covariance
% Subtract MPI noise from the signal
if subtract_mpi_est || ~training
y(symbol) = y(symbol);% - mpi_est;
else
end
% Equalizer weight update (LMS or NLMS)
if mio ~= 0
obj.e = obj.e - (mio * err(symbol) * U); % LMS weight update
else
normalizationfactor = (U.' * U);
obj.e = obj.e - err(symbol) * U / normalizationfactor; % NLMS weight update
end
% Store MPI estimate for visualization
mpi_estimates(symbol) = mpi_est;
Kgain(symbol) = K;
P_(symbol) = P;
H_(symbol) = H;
end %symbols
end %epoch
if ~training
if 1
% figure;
% subplot(2,2,1)
% hold on
% scatter(1:numel(y),y,1,'.');
% plot(1:numel(mpi_estimates), mpi_estimates);
% subplot(2,2,3)
% plot(1:numel(true_err), true_err);
% subplot(2,2,2)
% scatter(1:numel(y),y'-mpi_estimates,1,'.');
% xlabel('Sample Index');
% ylabel('MPI Noise Estimate');
% title('MPI Noise Estimation Over Time (Kalman Filter)');
% grid on;
figure(111);
subplot(3,1,1)
hold on
cla
scatter(1:numel(y),y,1,'.');
plot(1:numel(mpi_estimates), mpi_estimates,'DisplayName','mpi est');
ylim([-2,2]);
subplot(3,1,2)
cla
plot(1:numel(true_err), true_err,'DisplayName','true err');
ylim([-1,1]);
subplot(3,1,3)
plot(1:numel(true_err), true_err-mpi_estimates,'DisplayName',['diff rms: ',num2str(rms(true_err-mpi_estimates))]);
ylim([-1,1]);
legend
figure(333)
hold on
von = 5000;
bis = 15000;
plot(von:bis,true_err(von:bis),'DisplayName','Error')
plot(von:bis,movmean(true_err(von:bis), [30,30]),'DisplayName','Error')
plot(von:bis,mpi_estimates(von:bis),'DisplayName','Est','LineWidth',2);
legend
figure(222)
hold on
crr = xcorr(mpi_estimates,true_err,'normalized');
plot(crr);
end
% y = y-mpi_estimates';
end
end
end
end

View File

@@ -1,197 +0,0 @@
classdef FFE_Kalman_Feedback < handle
% Implementation of plain and simple FFE.
% 1) Training mode (stable performance when you use NLMS)
% 2) Decision directed mode
% Eq = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-4,"mu_tr",0,"order",25,"sps",2,"decide",0);
properties
sps % usually 2
order
e
error
len_tr
mu_tr
epochs_tr
mu_dd
epochs_dd
constellation
decide
end
methods
function obj = FFE_Kalman_Feedback(options)
arguments(Input)
options.sps = 2;
options.order = 15;
options.len_tr = 4096;
options.mu_tr = 0;
options.epochs_tr = 5;
options.mu_dd = 1e-5;
options.epochs_dd = 5;
options.decide = false;
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
obj.e = zeros(obj.order,1);
obj.error = 0;
end
function [X,Noi] = process(obj, X, D)
% actual processing of the signal (steps 1. - 3.)
% 1 normalize RMS
X = X.normalize("mode","rms");
obj.constellation = unique(D.signal);
% Training Mode
training = 1;
showviz = 0;
obj.equalize(X.signal, D.signal,obj.mu_tr,obj.epochs_tr,obj.len_tr,training);
% Decision Directed Mode
n = X.length;
training = 0;
showviz = 0;
[signal,decision]=obj.equalize(X.signal, D.signal,obj.mu_dd,obj.epochs_dd,n,training);
% Output Signal
if obj.decide
X.signal = decision;
else
X.signal = signal;
end
X.fs = D.fs; %change sampling frequency of outgoing signal from fdac e.g. 2 sps to symbol spaced = fsym
lbdesc = [num2str(obj.order),' tap FFE'];
X = X.logbookentry(lbdesc); % append to logbook
Noi = X;
Noi = X - D;
end
function [y, d_hat] = equalize(obj, x, d, mio, epochs, N, training)
arguments
obj
x
d
mio
epochs
N
training
end
% Initialize Kalman filter variables
A = 0.7; % State transition matrix
H = 1; % Observation matrix
Q = 1e-2; % Process noise covariance
R = 1e-3; % Measurement noise covariance
P = 1; % Initial error covariance
mpi_est = 0; % Initial estimate for MPI noise
K = 0; % Kalman gain
% Error buffers for parallelized structure
parallel_depth = 100; % Depending on your parallelization depth
err_buffer = zeros(parallel_depth, 1); % Store collected errors
mpi_est_buffer = zeros(parallel_depth, 1); % Store MPI estimates
x = [zeros(floor(obj.order/2), 1); x; zeros(obj.order, 1)];
for epoch = 1 : epochs
symbol = 0;
for sample = 1 : obj.sps : N
symbol = symbol + 1;
U = x(obj.order + sample - 1 : -1 : sample); % Input window for FFE
% Subtract the estimated MPI noise from the input signal before FFE
x_mpi_reduced = U;
y(symbol, 1) = obj.e.' * x_mpi_reduced; % Output of FFE
y(symbol, 1) = y(symbol, 1) - mpi_est;
% Decision and error calculation
if training
[~, symbol_idx] = min(abs(d(symbol) - obj.constellation)); % Closest constellation point
d_hat(symbol, 1) = d(symbol);
else
[~, symbol_idx] = min(abs(y(symbol) - obj.constellation)); % Closest constellation point
d_hat(symbol, 1) = obj.constellation(symbol_idx);
end
% Calculate the residual error
err(symbol) = y(symbol) - d_hat(symbol); % Instantaneous residual error
% Collect error for feedback after 'parallel_depth' samples
err_buffer(mod(symbol, parallel_depth) + 1) = err(symbol);
% Update Kalman filter based on the accumulated errors every parallel_depth samples
if mod(symbol, parallel_depth) == 0
% Compute the mean error over the last 'parallel_depth' samples
avg_error = mean(err_buffer);
% Prediction step (Kalman filter)
P = A * P * A' + Q; % Update the error covariance
K = P * H' / (H * P * H' + R); % Compute Kalman gain
% Update MPI noise estimate
mpi_est = mpi_est + K * (avg_error - H * mpi_est); % Update based on averaged error
P = (1 - K * H) * P; % Update error covariance
end
% Store MPI estimate for visualization or debugging
k_buffer(symbol) = K;
mpi_est_buffer(symbol) = mpi_est;
% FFE weight update using NLMS
if mio ~= 0
obj.e = obj.e - (mio * err(symbol) * U); % LMS weight update
else
normalizationfactor = (U.' * U);
obj.e = obj.e - err(symbol) * U / normalizationfactor; % NLMS weight update
end
end
end
if ~training
figure(111);
subplot(3,1,1)
hold on
cla
scatter(1:numel(y),y,1,'.');
plot(1:numel(mpi_est_buffer), mpi_est_buffer,'DisplayName','mpi est');
end
end
end
end

View File

@@ -0,0 +1,492 @@
classdef FFE_plain < handle
% Plain feed-forward equalizer with training, DD mode, and BER mu tuning.
%
% This class intentionally contains only the core FFE tap adaptation logic.
% MPI-specific DC tracking, A1/A2 suppression, and delayed update buffers
% stay in FFE and the MPI_reduction classes.
properties
sps
order
e
e_tr
error
len_tr
mu_tr
epochs_tr
adaption_technique
dd_mode
mu_dd
epochs_dd
dd_len_fraction
P
constellation
decide
save_debug = 0
debug_struct
optmize_mus = 0
mu_optimization
mu_optimization_iter = 0
mu_optimization_len
plot_mu_optimization = 0
mu_optimization_fignum = 3010
end
methods
function obj = FFE_plain(options)
arguments(Input)
options.sps = 2
options.order = 15
options.len_tr = 4096
options.mu_tr = 0
options.epochs_tr = 5
options.adaption_technique adaption_method = adaption_method.lms
options.dd_mode = 1
options.mu_dd = 1e-5
options.epochs_dd = 5
options.dd_len_fraction = 1
options.decide = false
options.save_debug = 0
options.optmize_mus = 0
options.mu_optimization_len = 2^15
options.plot_mu_optimization = 0
options.mu_optimization_fignum = 3010
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
obj.sps = floor(obj.sps);
obj.order = floor(obj.order);
obj.dd_len_fraction = min(max(obj.dd_len_fraction,0),1);
obj.resetState();
end
function [X,Noi] = process(obj, X, D)
X = X.normalize("mode","rms");
obj.constellation = unique(D.signal);
obj.resetState();
if obj.optmize_mus
obj.optimizeMus(X.signal,D.signal);
obj.resetState();
end
training = true;
showviz = false;
obj.equalize(X.signal,D.signal,obj.mu_tr,obj.epochs_tr,obj.len_tr,training,showviz);
obj.e_tr = obj.e;
training = false;
n = X.length;
if obj.dd_mode
[signal,decision] = obj.equalize(X.signal,D.signal,obj.mu_dd,obj.epochs_dd,n,training,showviz);
else
[signal,decision] = obj.equalize(X.signal,D.signal,0,1,n,training,showviz);
end
if obj.decide
X.signal = decision;
else
X.signal = signal;
end
X.fs = D.fs;
X = X.logbookentry([num2str(obj.order),' tap FFE']);
Noi = X - D;
end
function [y,d_hat] = equalize(obj,x,d,mu,epochs,N,training,showviz)
arguments
obj
x
d
mu
epochs
N
training
showviz
end
unused_showviz = showviz; %#ok<NASGU>
x = x(:);
d = d(:);
N = obj.validSampleLength(N,x,d);
n_symbols = N / obj.sps;
y = zeros(n_symbols,1);
d_hat = zeros(n_symbols,1);
if n_symbols == 0
return
end
if isempty(obj.constellation)
obj.constellation = unique(d);
end
decision_constellation = obj.constellation(:);
x = [zeros(floor(obj.order/2),1); x; zeros(obj.order,1)];
lambda = mu;
mask = ones(obj.order,1);
maincursor_pos = ceil(length(obj.e)/2);
adaption_code = obj.adaptionCode();
if mu == 0 || (~obj.dd_mode && ~training)
epochs = 1;
end
debug_enabled = obj.save_debug;
if debug_enabled
obj.initializeDebug(n_symbols,training);
end
for epoch = 1:epochs
symbol = 0;
for sample = 1:obj.sps:N
symbol = symbol + 1;
grad = zeros(obj.order,1);
update = zeros(obj.order,1);
weight = 0;
U = x(obj.order+sample-1:-1:sample);
y(symbol,1) = (obj.e.*mask).' * U;
if training
d_hat(symbol,1) = d(symbol);
else
[~,symbol_idx] = min(abs(y(symbol) - decision_constellation));
d_hat(symbol,1) = decision_constellation(symbol_idx);
end
err = d_hat(symbol) - y(symbol);
if mu ~= 0 && (training || obj.dd_mode)
switch adaption_code
case 1
weight = mu / ((U.'*U) + eps);
grad = err * U;
update = grad * weight;
obj.e = obj.e + update;
case 2
weight = mu;
grad = err * U;
update = grad * weight;
obj.e = obj.e + update;
case 3
denom = lambda + U.' * obj.P * U;
k = (obj.P * U) / denom;
update = k * err;
obj.e = obj.e + update;
obj.P = (1/lambda) * (obj.P - k * (U.' * obj.P));
end
end
if debug_enabled && epoch == 1
obj.debug_struct.error_first_epoch(1,symbol) = err * err';
end
if debug_enabled && epoch == epochs
error_power = err * err';
update_power = update.'*update ./ (sqrt((obj.e.'*obj.e) / obj.order) + eps);
obj.debug_struct.error(1,symbol) = error_power;
obj.debug_struct.main_cursor(1,symbol) = abs(obj.e(maincursor_pos));
obj.debug_struct.mu_nlms(1,symbol) = weight;
obj.debug_struct.update_gradient(1,symbol) = grad.'*grad;
if training
obj.debug_struct.error_tr(1,symbol) = error_power;
obj.debug_struct.update_tr(1,symbol) = update_power;
else
obj.debug_struct.error_dd(1,symbol) = error_power;
obj.debug_struct.update(1,symbol) = update_power;
end
end
end
end
end
function optimizeMus(obj,x,d)
[x_opt,d_opt,N_opt] = obj.optimizationSignals(x,d);
switch obj.adaption_technique
case adaption_method.lms
mu_range = [1e-5, 1e-2];
case adaption_method.nlms
mu_range = [1e-3, 5e-1];
case adaption_method.rls
mu_range = [0.98, 0.99999];
otherwise
builtin("error","FFE_plain:InvalidAdaptionTechnique", ...
"Unsupported FFE adaption technique.");
end
vars = optimizableVariable("mu_tr",mu_range,"Transform","log");
if obj.dd_mode
vars = [vars, optimizableVariable("mu_dd",mu_range,"Transform","log")];
end
obj.mu_optimization_iter = 0;
fprintf("FFE_plain mu opt uses %d samples / %d symbols\n",N_opt,numel(d_opt));
obj.mu_optimization = bayesopt(@(p)obj.muObjective(p,x_opt,d_opt),vars, ...
"MaxObjectiveEvaluations",20, ...
"AcquisitionFunctionName","expected-improvement-plus", ...
"IsObjectiveDeterministic",false, ...
"Verbose",0, ...
"PlotFcn",[]);
obj.mu_tr = obj.mu_optimization.XAtMinObjective.mu_tr;
if obj.dd_mode
obj.mu_dd = obj.mu_optimization.XAtMinObjective.mu_dd;
fprintf("\nFFE_plain mu opt done: mu_tr=%9.3e, mu_dd=%9.3e, BER=%9.3e\n", ...
obj.mu_tr,obj.mu_dd,obj.mu_optimization.MinObjective);
else
fprintf("\nFFE_plain mu opt done: mu_tr=%9.3e, BER=%9.3e\n", ...
obj.mu_tr,obj.mu_optimization.MinObjective);
end
if obj.plot_mu_optimization
obj.plotMuOptimization();
end
end
function objective = muObjective(obj,params,x,d)
old_state = obj.captureObjectiveState();
cleanup = onCleanup(@()obj.restoreObjectiveState(old_state));
obj.save_debug = 0;
obj.resetState();
N_tr = min(obj.len_tr,numel(x));
obj.equalize(x,d,params.mu_tr,obj.epochs_tr,N_tr,true,false);
if obj.dd_mode
[signal,~] = obj.equalize(x,d,params.mu_dd,obj.epochs_dd,numel(x),false,false);
else
[signal,~] = obj.equalize(x,d,0,1,numel(x),false,false);
end
[ber,errors] = obj.berObjective(signal,d);
objective = ber;
if ~isfinite(objective)
objective = inf;
end
obj.mu_optimization_iter = obj.mu_optimization_iter + 1;
if obj.dd_mode
fprintf("\rFFE_plain mu opt %02d: mu_tr=%9.3e, mu_dd=%9.3e, BER=%9.3e, errors=%d", ...
obj.mu_optimization_iter,params.mu_tr,params.mu_dd,ber,errors);
else
fprintf("\rFFE_plain mu opt %02d: mu_tr=%9.3e, BER=%9.3e, errors=%d", ...
obj.mu_optimization_iter,params.mu_tr,ber,errors);
end
clear cleanup
end
function [x_opt,d_opt,N_opt] = optimizationSignals(obj,x,d,opt_len)
if nargin < 4
opt_len = obj.mu_optimization_len;
end
N_available = min(numel(x),numel(d) * obj.sps);
if isempty(opt_len) || opt_len <= 0 || isinf(opt_len)
N_opt = N_available;
else
N_opt = min(N_available,max(obj.len_tr,opt_len));
end
N_opt = obj.sps * floor(N_opt / obj.sps);
N_opt = max(obj.sps,N_opt);
n_symbols = N_opt / obj.sps;
x_opt = x(1:N_opt);
d_opt = d(1:n_symbols);
end
function [ber,errors] = berObjective(~,signal,d)
M = numel(unique(d));
mapper = PAMmapper(M,0);
eq_signal_sd = Signal(signal);
eq_signal_hd = mapper.quantize(eq_signal_sd);
tx_symbols = Signal(d);
rx_bits = mapper.demap(eq_signal_hd);
tx_bits = mapper.demap(tx_symbols);
[~,errors,ber,~] = calc_ber(rx_bits.signal,tx_bits.signal, ...
"skip_front",1000, ...
"skip_end",0, ...
"returnErrorLocation",1);
end
function plotMuOptimization(obj)
if isempty(obj.mu_optimization)
return
end
X = obj.mu_optimization.XTrace;
objective = obj.mu_optimization.ObjectiveTrace;
objective = objective(:);
valid = isfinite(objective);
if isempty(X) || ~any(valid)
return
end
var_names = X.Properties.VariableNames;
n_vars = numel(var_names);
eval_idx = (1:numel(objective)).';
objective_plot = obj.positiveObjectiveForLogPlot(objective);
best_plot = obj.positiveObjectiveForLogPlot(cummin(objective));
figure(obj.mu_optimization_fignum);
clf;
t = tiledlayout(2,2,"TileSpacing","compact","Padding","compact");
title(t,"FFE_plain Bayesian mu optimization");
nexttile;
h_candidate = semilogy(eval_idx,objective_plot,"o-","DisplayName","candidate");
obj.addOptimizationDataTips(h_candidate,X,objective,objective_plot,eval_idx,var_names);
hold on;
h_best_trace = semilogy(eval_idx,best_plot,"k-","LineWidth",1.2,"DisplayName","best so far");
h_best_trace.DataTipTemplate.DataTipRows(end+1) = dataTipTextRow("Evaluation",eval_idx);
h_best_trace.DataTipTemplate.DataTipRows(end+1) = dataTipTextRow("Best BER",best_plot);
grid on;
xlabel("Evaluation");
ylabel("BER");
legend("Location","best");
if n_vars < 2
return
end
pairs = nchoosek(1:n_vars,2);
n_pair_plots = min(size(pairs,1),3);
[~,best_idx] = min(objective);
for pair_idx = 1:n_pair_plots
nexttile;
x_name = var_names{pairs(pair_idx,1)};
y_name = var_names{pairs(pair_idx,2)};
x_data = X.(x_name);
y_data = X.(y_name);
c_data = log10(objective_plot);
h_scatter = scatter(log10(x_data),log10(y_data),35,c_data,"filled");
obj.addOptimizationDataTips(h_scatter,X,objective,objective_plot,eval_idx,var_names);
hold on;
h_best = plot(log10(x_data(best_idx)),log10(y_data(best_idx)),"kp", ...
"MarkerSize",12, ...
"MarkerFaceColor","y", ...
"DisplayName","best");
obj.addOptimizationDataTips(h_best,X(best_idx,:),objective(best_idx),objective_plot(best_idx),eval_idx(best_idx),var_names);
grid on;
xlabel("log10(" + string(x_name) + ")");
ylabel("log10(" + string(y_name) + ")");
cb = colorbar;
cb.Label.String = "log10(BER)";
title(string(x_name) + " vs " + string(y_name));
end
end
function addOptimizationDataTips(~,plot_handle,X,objective,objective_plot,eval_idx,var_names)
plot_handle.DataTipTemplate.DataTipRows(end+1) = dataTipTextRow("Evaluation",eval_idx);
plot_handle.DataTipTemplate.DataTipRows(end+1) = dataTipTextRow("BER",objective);
plot_handle.DataTipTemplate.DataTipRows(end+1) = dataTipTextRow("BER shown",objective_plot);
for var_idx = 1:numel(var_names)
var_name = var_names{var_idx};
plot_handle.DataTipTemplate.DataTipRows(end+1) = dataTipTextRow(var_name,X.(var_name));
end
end
function objective_plot = positiveObjectiveForLogPlot(~,objective)
objective_plot = objective;
positive_values = objective(isfinite(objective) & objective > 0);
if isempty(positive_values)
floor_value = 1e-12;
else
floor_value = min(positive_values) / 10;
end
objective_plot(~isfinite(objective_plot) | objective_plot <= 0) = floor_value;
end
function state = captureObjectiveState(obj)
state.e = obj.e;
state.e_tr = obj.e_tr;
state.error = obj.error;
state.P = obj.P;
state.save_debug = obj.save_debug;
state.debug_struct = obj.debug_struct;
end
function restoreObjectiveState(obj,state)
obj.e = state.e;
obj.e_tr = state.e_tr;
obj.error = state.error;
obj.P = state.P;
obj.save_debug = state.save_debug;
obj.debug_struct = state.debug_struct;
end
end
methods (Access = private)
function resetState(obj)
obj.e = zeros(obj.order,1);
obj.e_tr = zeros(obj.order,1);
obj.error = 0;
obj.P = (1/0.05) * eye(obj.order);
obj.debug_struct = struct();
end
function N = validSampleLength(obj,N,x,d)
N_available = min(numel(x),numel(d) * obj.sps);
N = min(N,N_available);
N = obj.sps * floor(N / obj.sps);
N = max(0,N);
end
function adaption_code = adaptionCode(obj)
if obj.adaption_technique == adaption_method.nlms
adaption_code = 1;
elseif obj.adaption_technique == adaption_method.lms
adaption_code = 2;
elseif obj.adaption_technique == adaption_method.rls
adaption_code = 3;
else
builtin("error","FFE_plain:InvalidAdaptionTechnique", ...
"Unsupported FFE adaption technique.");
end
end
function initializeDebug(obj,n_symbols,training)
obj.debug_struct.error = NaN(1,n_symbols);
obj.debug_struct.error_first_epoch = NaN(1,n_symbols);
obj.debug_struct.main_cursor = NaN(1,n_symbols);
obj.debug_struct.mu_nlms = NaN(1,n_symbols);
obj.debug_struct.update_gradient = NaN(1,n_symbols);
if training
obj.debug_struct.error_tr = NaN(1,n_symbols);
obj.debug_struct.update_tr = NaN(1,n_symbols);
else
obj.debug_struct.error_dd = NaN(1,n_symbols);
obj.debug_struct.update = NaN(1,n_symbols);
end
end
end
end

View File

@@ -1,499 +1,3 @@
% classdef ML_MLSE < handle
% % ALGORITHM DESCRIBED IN:
% % W. Lanneer and Y. Lefevre, Machine Learning-Based Pre-Equalizers for
% % Maximum Likelihood Sequence Estimation in High-Speed PONs,
% % in 2023 31st European Signal Processing Conference
%
% % Further ML Refs:
% % https://machinelearningmastery.com/cross-entropy-for-machine-learning/
% % https://docs.pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html
%
% % The central idea is to overcome the (white-) noise assumption within the previously described
% % Viterbi algorithm, more precisely a closed-loop optimization is proposed that finds a suitable
% % filter-set to directly compute the branch metrics c_k (s,s^' ). These can directly be used to
% % carry out the conventional Viterbi algorithm. The system consists of S^L S=F linear FIR filters,
% % combined with one bias coefficient respectively. These filters take the received input samples to
% % compute the branch metrics estimates (c_k ) ̂(s,s^' ) according toThe central idea is to overcome
% % the (white-) noise assumption within the previously described Viterbi algorithm, more precisely
% % a closed-loop optimization is proposed that finds a suitable filter-set to directly compute the
% % branch metrics c_k (s,s^' ). These can directly be used to carry out the conventional Viterbi
% % algorithm. The system consists of S^L S=F linear FIR filters, combined with one bias coefficient
% % respectively. These filters take the received input samples to compute the branch metrics
% % estimates. Finally, the usual Viterbi is carried out...
%
% % Recommended Settings and some findings:
%
% % Requires many training epochs. According to ML people, 100,200 or
% % even up to 1000 epochs are normal for ML-convergence
%
% % The mu parameter _can_ be adaptive - using the cross entropy and when
% % analyzing the isolated training it looks very promisig. However, is
% % later use I found this is not as stable as a fixed learning rate.
% % mu = 0.1 worked good for me
%
% % Longer orders/ filter length are not always better. For me order=11
% % was good.
%
% % Delay factor (delta) is good when the order is also increased. With
% % order = 11, a delta of =4 shows good results
%
% properties
% sps % usually 2
% order
% e
% e_tr
% error
%
% len_tr
% mu_tr
% epochs_tr
%
% dd_mode % 1 or 0 to set DD-mode on or off
% mu_dd %weight update in dd mode
% epochs_dd
%
% adaptive_mu
%
% constellation
%
% L %viterbi memory length
%
% alpha
% DIR
% DIR_flip
% trellis_states
%
% traceback_depth
%
% % --- Added internal class variables used later ---
% S
% Nf
% delta
% nStates
% nFeasible
% combs
% first_sym
% last_sym
% valid
% valid_to_idx
% valid_from_idx
% w
%
% % --- New: fast state lookup ---
% true_to_state_idx
% state_dict % containers.Map: key(sequence)->state index
% key_fmt = '%.8g_'; % key format for sequence strings
% nSym % |constellation|
%
% ber = []
% ce = ones(1,1);
% end
%
% methods
% function obj = ML_MLSE(options)
% arguments(Input)
%
% options.sps = 2;
% options.order = 15;
%
% options.len_tr = 4096;
% options.mu_tr = 0;
% options.epochs_tr = 5;
%
% options.dd_mode = 1;
% options.mu_dd = 1e-5;
% options.epochs_dd = 5;
%
% options.adaptive_mu = 1;
%
% options.delta = 0;
% options.traceback_depth = 1024;
%
% options.L = 1
%
% end
%
% fn = fieldnames(options);
% for n = 1:numel(fn)
% obj.(fn{n}) = options.(fn{n});
% end
%
% obj.e = zeros(obj.order,1);
% obj.error = 0;
% end
%
% function [X,X_viterbi] = process(obj, X, D)
%
% % actual processing of the signal (steps 1. - 3.)
% % 1 normalize RMS
% X = X.normalize("mode","rms");
%
% % Use sorted constellation for deterministic mapping
% obj.constellation = sort(unique(D.signal),'ascend');
% obj.nSym = numel(obj.constellation);
%
% if length(X)/length(D) ~= obj.sps
% warning('Signal length does not fit to reference!');
% end
%
% % ==============================================================
% % INITIALIZATION (only before final epoch and detection mode)
% % ==============================================================
%
% % --- Parameters
% obj.S = numel(obj.constellation); % alphabet size
% obj.Nf = obj.order*obj.sps; % filter length
% % obj.delta = 3;%ceil(obj.Nf/2); % delay parameter
% obj.nStates = obj.S^obj.L;
% obj.nFeasible = obj.nStates*obj.S;
%
% % --- Trellis mapping
% obj.trellis_states = reshape(obj.constellation,1,[]);
% pre_comb_mat = repmat(obj.trellis_states, obj.L, 1);
% pre_comb_cell = mat2cell(pre_comb_mat, ones(1,obj.L), size(pre_comb_mat,2));
% obj.combs = fliplr(combvec(pre_comb_cell{:}).'); % rows: states, columns: [x_k, x_{k-1}, ...]
% obj.first_sym = obj.combs(:,1);
% obj.last_sym = obj.combs(:,end);
% obj.nStates = size(obj.combs,1);
%
% % --- Valid transitions
% obj.valid = false(obj.nStates);
% for from = 1:obj.nStates
% for to = 1:obj.nStates
% if all(obj.combs(to,2:end) == obj.combs(from,1:end-1))
% obj.valid(to,from) = true;
% end
% end
% end
% [obj.valid_to_idx, obj.valid_from_idx] = find(obj.valid);
%
% % --- Allocate vectors and weights
% % !! IF SHAPE FIT, then we already have smth there an we want
% % to start with the existing fitler-set
% if isempty(obj.w) || any(size(obj.w) ~= [obj.Nf+1,obj.nFeasible])
% obj.w = zeros(obj.Nf+1,obj.nFeasible); % filter weights per transition + bias tap
% obj.w = randn(obj.Nf+1,obj.nFeasible);
% end
%
% % --- Precompute dictionary for fast state lookup (sequence -> state)
% keys = cell(obj.nStates,1);
% for i = 1:obj.nStates
% keys{i} = obj.seq_key(obj.combs(i,:)); % combs row is already [x_k, x_{k-1}, ...]
% end
% obj.state_dict = containers.Map(keys, 1:obj.nStates);
%
% % ==============================================================
% % TRAINING
% % ==============================================================
%
% % Training Mode
% n = obj.len_tr;
% training = 1;
% obj.equalize(X.signal, D.signal,obj.mu_tr,obj.epochs_tr,n,training);
% obj.e_tr = obj.e;
%
% % ==============================================================
% % DD-Mode / Fixed Mode
% % ==============================================================
%
% % Decision Directed Mode
% n = X.length;
% training = 0;
% [y,y_vit]=obj.equalize(X.signal, D.signal,obj.mu_dd,obj.epochs_dd,n,training);
%
% X_viterbi = X;
%
% X.signal = y;
% X.fs = D.fs; %change sampling frequency of outgoing signal from fdac e.g. 2 sps to symbol spaced = fsym
% lbdesc = [num2str(obj.order),' tap FFE'];
% X = X.logbookentry(lbdesc); % append to logbook
%
% X_viterbi.signal = y_vit;
% X_viterbi.fs = D.fs; %change sampling frequency of outgoing signal from fdac e.g. 2 sps to symbol spaced = fsym
% lbdesc = [num2str(obj.order),'order FFE + PF + Viterbi'];
% X_viterbi = X_viterbi.logbookentry(lbdesc); % append to logbook
% end
%
% function [y,y_ref] = equalize(obj,x,d,mu,epochs,N,training)
% % ==============================================================
% % FFE + Whitening + ML-Based Branch Metric Estimation + Viterbi
% % ==============================================================
% debug = 1;
% showPlots = 1;
%
% % --- Input padding and preallocation
% y = zeros(N,1);
%
% % number of symbol steps in this block
% nSymbols = ceil(N/obj.sps);
%
% for epoch = 1:epochs
%
% % state metrics (log-domain costs): keep as column [nStates×1]
% pm = zeros(obj.nStates,1); % v_{k-1}(s)
% c_hat = zeros(1,obj.nFeasible);
% v_tilde = zeros(1,obj.nFeasible);
% pred = zeros(nSymbols, obj.nStates, 'uint32');
% pm_sto = nan(obj.nStates, nSymbols,'like',pm);
% CE_accum = 0;
%
%
% %%% START IDX
% if training
% max_start = length(x) - ( (ceil(N/obj.sps)-1)*obj.sps + 1 );
% max_start = max(1, max_start); % safety
% start_sample = randi([1, max_start], 1); %rnd training; not really good
% start_sample = 1;
% end_sample = start_sample + (ceil(N/obj.sps)-1)*obj.sps;
% else
% start_sample = 1;%obj.len_tr;
% end_sample = N;
% end
%
% start_symbol = 1 + floor((start_sample - 1)/obj.sps); % ABSOLUTE symbol index
%
% if numel(d) >= obj.L && start_symbol >= obj.L
% init_seq = d(start_symbol-obj.L+1 : start_symbol); % [d_k-L+1 ... d_k]
% true_to_state_idx = obj.state_dict(obj.seq_key(flip(init_seq))); % [d_k ... d_k-L+1]
% else
% % Not enough history fall back to state 1
% true_to_state_idx = uint32(1);
% end
%
% symbol = 0;
% for sample = start_sample:obj.sps:end_sample
% symbol = symbol + 1;
% k = symbol;
% sym_idx = start_symbol + (symbol - 1);
%
% % --- Build Δ-delayed observation window y_k
% i1 = sample - obj.Nf + 1 + obj.delta;
% i2 = sample + obj.delta;
% buf = x(max(1,i1):min(length(x),i2));
% padL = max(0,1 - i1);
% padR = max(0,i2 - length(x));
% yk = [zeros(padL,1); buf(:); zeros(padR,1)]; % Nf×1
% yk = [yk;1];
%
% % --- Predict branch metrics for all feasible transitions: c_hat
% c_hat = (yk.' * obj.w); % [1×nFeasible]
% c_hat = c_hat.'; % [nFeasible×1]
%
% % --- Extended path metrics: v_tilde = pm(from) + c_hat
% % normalize pm to avoid growth (invariant to additive const)
% pm = pm - min(pm);
% v_tilde = pm(obj.valid_from_idx) + c_hat; % [nFeasible×1]
%
% % ===== Gradient update (Algorithm 1) =====
%
% if 1 %training
% % --- allocate storage once
% if epoch == 1 && symbol == 1
% obj.true_to_state_idx = ones(ceil(N/obj.sps),1,'uint32');
% end
%
% % --- previous "to" becomes current "from"
% if symbol > 1
% true_from_state_idx = obj.true_to_state_idx(symbol-1);
% else
% true_from_state_idx = 1;
% end
%
% % --- compute or reuse "to" state
% if epoch == 1
% % only compute in first epoch
% if sym_idx >= obj.L
% key_to = obj.seq_key(flip(d(sym_idx-obj.L+1 : sym_idx)));
% if isKey(obj.state_dict, key_to)
% obj.true_to_state_idx(symbol) = obj.state_dict(key_to);
% else
% obj.true_to_state_idx(symbol) = true_from_state_idx;
% end
% else
% obj.true_to_state_idx(symbol) = true_from_state_idx;
% end
% end
%
% % --- reuse cached state from second epoch onward
% true_to_state_idx = obj.true_to_state_idx(symbol);
%
% % --- ensure valid (from,to)
% dirac = zeros(obj.nFeasible,1);
% mask = obj.valid_from_idx==true_from_state_idx & ...
% obj.valid_to_idx ==true_to_state_idx;
% if any(mask)
% dirac(mask) = 1;
% else
% idx = find(obj.valid_from_idx==true_from_state_idx,1,'first');
% dirac(idx) = 1;
% obj.true_to_state_idx(symbol) = obj.valid_to_idx(idx);
% end
%
%
%
%
% % softmax over -v_tilde (numerically safe shift)
% v_shift = -(v_tilde - min(v_tilde)); % shift to small positive numbers
% v_shift = min(v_shift, 100); % clamp exponent argument ( exp(50)=3e21)
% expv = exp(v_shift);
% p = expv ./ (sum(expv) + eps);
%
% % for logging only:
% CE_symbol(symbol) = -log(p(dirac==1) + eps);
%
% if sym_idx > obj.L
% CE_smooth(symbol) = 0.01*CE_symbol(symbol) + 0.99*CE_smooth(symbol-1);
% else
% if epoch > 1
% CE_smooth(symbol) = obj.ce(end); %use ce from last epoch or =1 for very first round?!
% else
% CE_smooth(symbol) = CE_symbol(symbol);
% end
% end
%
% CE_accum = CE_symbol(symbol) + CE_accum;
%
%
% % gradient term (t - p)
% dmp = (dirac - p)'; % 1×nFeasible
%
% % Per-feature gradient; implicit expansion gives (Nf+1)×nFeasible
% dL_Dw = (yk) .* dmp;
%
% % Start updates only when the ABSOLUTE symbol index has L history
% if sym_idx >= obj.L
% if obj.adaptive_mu
% mu_eff = CE_smooth(sym_idx);
% mu_eff = max(min(mu_eff, 0.2), 1e-4);
% else
% mu_eff = mu;
% end
%
% obj.w = obj.w - mu_eff .* dL_Dw; % (Nf+1)×nFeasible
% end
%
% % if debug && epoch > 2
% % figure(100);
% % subplot(4,1,1);
% % heatmap(p');
% % title('Probs')
% % subplot(4,1,2);
% % heatmap(dmp);
% % title('Update')
% % subplot(4,1,3);
% % heatmap(dL_Dw);
% % title('Update')
% % subplot(4,1,4);
% % heatmap(bj.w);
% % title('Update')
% %
% % end
%
% end
%
%
%
% % --- Compare-Select (matrix form, min of costs)
% v_tilde_mat = inf(obj.nStates, obj.nStates);
% v_tilde_mat(obj.valid) = v_tilde;
% [pm_next, pred(k,:)] = min(v_tilde_mat, [], 2);
%
% % re-center to keep metrics bounded (decision-invariant)
% pm_next = pm_next - min(pm_next);
%
% pm = pm_next;
% pm_sto(:,symbol) = pm;
% end
%
% % --- Traceback (full; you can window with traceback_depth if desired)
% [~, s_end] = min(pm);
% viterbi_path = zeros(symbol,1,'uint32');
% viterbi_path(symbol) = s_end;
% for n = symbol:-1:2
% viterbi_path(n-1) = pred(n, viterbi_path(n));
% end
%
% y_ref = d(start_symbol:end);
% y = obj.first_sym(viterbi_path);
%
% if debug && training
% sym_start = start_symbol;
% sym_end = start_symbol + symbol - 1;
% ref_slice = d(sym_start : sym_end);
% err = sum(y ~= ref_slice(1:numel(y)));
%
% try
% ref_bits = PAMmapper(obj.S,0).demap(ref_slice);
% eq_bits = PAMmapper(obj.S,0).demap(y);
% [~, ~, ber, ~] = calc_ber(ref_bits, eq_bits, "skip_front", 10, "skip_end", 10, "returnErrorLocation", 1);
% fprintf('Epoch: %d - BER: %.1e \n',epoch, ber);
% obj.ber(epoch) = ber;
% catch
% ser = err./length(y);
% fprintf('Epoch: %d - SER: %.1e \n',epoch, ser);
% end
%
% obj.ce(epoch) = CE_accum./symbol;
%
% if showPlots
% figure(10);clf
% subplot(3,2,1:2);
% heatmap(obj.w);
% title('Filter')
%
% subplot(3,2,3);
% v_tildemat = NaN(obj.nStates, obj.nStates);
% v_tildemat(obj.valid) = v_tilde; % log-domain scores
% heatmap(v_tildemat);
% title('Path Metrics (v_tilde)')
%
% subplot(3,2,4);
% scatter(1:symbol,pm_sto,1,'.')
% title('Path Metric Winners')
%
% subplot(3,2,5);hold on
% scatter(1:symbol,CE_symbol,1,'.');
% scatter(1:symbol,CE_smooth,1,'.')
% title('Cross Entropy')
%
% subplot(3,2,6); hold on
%
% % Left y-axis: Cross Entropy (linear)
% yyaxis left
% scatter(1:length(obj.ce), obj.ce, 10, 's', 'filled')
% ylabel('Cross Entropy')
%
% % Right y-axis: BER (logarithmic)
% yyaxis right
% scatter(1:length(obj.ber), obj.ber, 10, 'd', 'filled')
% set(gca, 'YScale', 'log')
% ylabel('BER (log scale)')
%
% xlim([1, epochs])
% xlabel('Epoch')
% title('Cross Entropy // BER')
% grid on
%
% drawnow
% end
% end
% end
% end
% end
%
% methods (Access=private)
% function k = seq_key(obj, seq)
% % Build a stable key string for a sequence row vector in the *same order as combs rows* ([x_k, x_{k-1}, ...])
% % Use rounding via sprintf to avoid floating-point issues.
% % seq must be a row vector.
% k = sprintf(obj.key_fmt, seq);
% end
% end
% end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
classdef ML_MLSE < handle
% ---------------------------------------------------------------------
% W. Lanneer and Y. Lefevre,
@@ -597,7 +101,7 @@ classdef ML_MLSE < handle
obj.S = obj.nSym;
obj.Nf = obj.order * obj.sps;
obj.nStates = obj.S^obj.L;
obj.nFeasible = obj.nStates * obj.S; %feasible state transitions
obj.nFeasible = obj.nStates * obj.S;
% --- Trellis mapping
obj.trellis_states = reshape(obj.constellation,1,[]);
@@ -621,7 +125,8 @@ classdef ML_MLSE < handle
% --- Initialize weights
if isempty(obj.w) || any(size(obj.w) ~= [obj.Nf+1,obj.nFeasible])
obj.w = randn(obj.Nf+1,obj.nFeasible);
% obj.w = randn(obj.Nf+1,obj.nFeasible);
obj.w = zeros(obj.Nf+1,obj.nFeasible);
end
% --- Fast lookup tables
@@ -660,8 +165,8 @@ classdef ML_MLSE < handle
% EQUALIZE
% ==============================================================
function [y,y_ref] = equalize(obj,x,d,mu,epochs,N,training)
debug = 1;
showPlots = 1;
debug = 0;
showPlots = 0;
y = zeros(N,1);
nSymbols = ceil(N/obj.sps);

View File

@@ -0,0 +1,525 @@
classdef ML_MLSE_GPU < handle
% ---------------------------------------------------------------------
% W. Lanneer and Y. Lefevre,
% Machine Learning-Based Pre-Equalizers for Maximum Likelihood
% Sequence Estimation in High-Speed PONs, EUSIPCO 2023
% ---------------------------------------------------------------------
% This implementation reproduces the closed-loop ML-based
% pre-equalizer training for MLSE, supporting both training and
% detection (decision-directed) modes.
% ---------------------------------------------------------------------
properties
sps
order
e
e_tr
error
len_tr
mu_tr
epochs_tr
dd_mode
mu_dd
epochs_dd
adaptive_mu
constellation
L
alpha
DIR
DIR_flip
trellis_states
traceback_depth
delta
% --- New: GPU Support ---
use_gpu
% Internal variables
S
Nf
nStates
nFeasible
combs
first_sym
last_sym
valid
valid_to_idx
valid_from_idx
w
% Fast lookup
nSym
key_table
trans_index
true_to_state_idx
% Debug metrics
ber = []
ce = ones(1,1)
end
methods
function obj = ML_MLSE_GPU(options)
arguments(Input)
options.sps = 2;
options.order = 15;
options.len_tr = 4096;
options.mu_tr = 0.001;
options.epochs_tr = 5;
options.dd_mode = 1;
options.mu_dd = 1e-5;
options.epochs_dd = 5;
options.adaptive_mu = 1;
options.delta = 0;
options.traceback_depth = 1024;
options.L = 1;
options.use_gpu = 0; % Default: CPU
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
% Check GPU availability
if obj.use_gpu
if gpuDeviceCount() > 0
% GPU is available, keeping use_gpu = 1
else
warning('GPU requested but not available. Falling back to CPU.');
obj.use_gpu = 0;
end
end
obj.e = zeros(obj.order,1);
obj.error = 0;
end
% ==============================================================
% PROCESS
% ==============================================================
function [X,X_viterbi] = process(obj, X, D)
% Normalize input RMS
X = X.normalize("mode","rms");
obj.constellation = sort(unique(D.signal),'ascend');
obj.nSym = numel(obj.constellation);
if length(X)/length(D) ~= obj.sps
warning('Signal length does not fit to reference!');
end
% --- Parameters
obj.S = obj.nSym;
obj.Nf = obj.order * obj.sps;
obj.nStates = obj.S^obj.L;
obj.nFeasible = obj.nStates * obj.S;
% --- Trellis mapping
obj.trellis_states = reshape(obj.constellation,1,[]);
pre_comb_mat = repmat(obj.trellis_states, obj.L, 1);
pre_comb_cell = mat2cell(pre_comb_mat, ones(1,obj.L), size(pre_comb_mat,2));
obj.combs = fliplr(combvec(pre_comb_cell{:}).');
obj.first_sym = obj.combs(:,1);
obj.last_sym = obj.combs(:,end);
obj.nStates = size(obj.combs,1);
% --- Valid transitions
obj.valid = false(obj.nStates);
for from = 1:obj.nStates
for to = 1:obj.nStates
if all(obj.combs(to,2:end) == obj.combs(from,1:end-1))
obj.valid(to,from) = true;
end
end
end
[obj.valid_to_idx,obj.valid_from_idx] = find(obj.valid);
% --- Initialize weights
if isempty(obj.w) || any(size(obj.w) ~= [obj.Nf+1,obj.nFeasible])
obj.w = randn(obj.Nf+1,obj.nFeasible);
obj.w = zeros(obj.Nf+1,obj.nFeasible);
end
% --- Fast lookup tables
[~, sym_idx_mat] = ismember(obj.combs, obj.constellation);
key_vals = 1 + sum((sym_idx_mat - 1) .* (obj.nSym .^ (0:obj.L-1)), 2);
max_key = obj.nSym^obj.L;
obj.key_table = zeros(max_key,1,'uint32');
obj.key_table(key_vals) = 1:obj.nStates;
obj.trans_index = sparse(obj.nStates,obj.nStates);
for i = 1:length(obj.valid_from_idx)
f = obj.valid_from_idx(i);
t = obj.valid_to_idx(i);
obj.trans_index(t,f) = i;
end
% ==============================================================
% TRAINING
% ==============================================================
fprintf('\n--- Training mode ---\n');
% Always run training on CPU to avoid loop latency on GPU
obj.equalize(X.signal, D.signal, obj.mu_tr, obj.epochs_tr, obj.len_tr, true);
obj.e_tr = obj.e;
% ==============================================================
% DECISION-DIRECTED / TESTING
% ==============================================================
fprintf('--- Decision-directed / detection mode ---\n');
x_sig = X.signal;
d_sig = D.signal;
% Move to GPU only for inference if requested
if obj.use_gpu
try
x_sig = gpuArray(single(x_sig));
if isa(obj.w, 'double')
obj.w = gpuArray(single(obj.w));
end
catch
warning('Failed to move data to GPU. Falling back to CPU.');
obj.use_gpu = 0;
end
end
[y, y_vit] = obj.equalize(x_sig, d_sig, obj.mu_dd, obj.epochs_dd, X.length, false);
% Gather results back to CPU
if obj.use_gpu
y = gather(y);
y_vit = gather(y_vit);
obj.w = gather(obj.w);
end
X_viterbi = X;
X.signal = y;
X_viterbi.signal = y_vit;
end
% ==============================================================
% EQUALIZE
% ==============================================================
function [y,y_ref] = equalize(obj,x,d,mu,epochs,N,training)
debug = 1;
showPlots = 1;
% Ensure basic types
if obj.use_gpu && ~isa(x, 'gpuArray') && training % Only force gpu for training if desired, but here we focus on inference
% For training, we keep existing flow for now.
end
y = zeros(N,1);
% On GPU, y should be gpuArray if we build it there, but we return it at the end.
if obj.use_gpu && ~training
y = gpuArray.zeros(N,1);
end
nSymbols = ceil(N/obj.sps);
for epoch = 1:epochs
pm = zeros(obj.nStates,1);
pred = zeros(nSymbols,obj.nStates,'uint32');
% pred is large uint32, GPU support for uint32 exists but sometimes limited.
% We'll keep pred on CPU for Viterbi path storage or gather per chunk if needed.
pm_sto = nan(obj.nStates,nSymbols,'like',pm);
CE_accum = 0;
start_sample = 1;
end_sample = N;
start_symbol = 1 + floor((start_sample - 1)/obj.sps);
% --- initialize true state
if numel(d) >= obj.L && start_symbol >= obj.L
init_seq = d(start_symbol-obj.L+1:start_symbol);
key_init = obj.seq2key(init_seq);
true_to_state_idx = obj.key_table(key_init);
if true_to_state_idx==0, true_to_state_idx=1; end
else
true_to_state_idx = uint32(1);
end
% =========================================================
% INFERENCE OPTIMIZATION (Vectorized Branch Metrics)
% =========================================================
run_vectorized = ~training && obj.use_gpu;
% --- Pre-calculate symbol indices for fast key generation (Training Only)
% This avoids slow ismember() calls inside the loop
d_indices = [];
if training
% Map d to 0..M-1 indices once
[~, d_indices] = ismember(d, obj.constellation);
d_indices = d_indices - 1; % 0-based
end
if run_vectorized
% 1. Construct Sliding Window Input Matrix
% Windows corresponding to symbol centers: start_sample:sps:end_sample
% Each window is [x(sample-Nf+1+delta : sample+delta); 1]
% Create index matrix
num_syms = numel(start_sample:obj.sps:end_sample);
samples_idx = start_sample + (0:num_syms-1)*obj.sps; % [1 x nSymbols]
% Indices for window: relative -Nf+1+delta to +delta
rel_idx = (-obj.Nf + 1 + obj.delta : obj.delta)'; % [Nf x 1]
% Full index matrix (implicit expansion)
idx_mat = rel_idx + samples_idx; % [Nf x nSymbols]
% Handle boundary padding (clamping indices)
% Since x is gpuArray, indexing with clamp is efficient if rewritten
% But MATLAB indexing x(idx_mat) with OOB indices is tricky vectorized.
% Faster approach: Clamp indices to [1, length(x)] and mask zero-pads.
mask_valid = (idx_mat >= 1) & (idx_mat <= length(x));
idx_clamped = max(1, min(length(x), idx_mat));
X_windows = x(idx_clamped); % [Nf x nSymbols]
X_windows = X_windows .* mask_valid; % Zero pad out-of-bounds
% Add bias row
X_windows = [X_windows; ones(1, num_syms, 'like', x)]; % [(Nf+1) x nSymbols]
% 2. Bulk Compute Branch Metrics
% C_all: [nFeasible x nSymbols] = ( [(Nf+1) x nFeasible] )' * [(Nf+1) x nSymbols]
% obj.w usually (Nf+1)xFeasible
C_all = obj.w' * X_windows;
% 3. Bring Metrics to CPU for Viterbi
% Processing Viterbi on CPU is often faster than serial kernel launches on GPU
C_all_cpu = gather(C_all);
% Pre-computation done. Now loop for Viterbi (Add-Compare-Select)
% Prepare CPU variables
pm = gather(pm);
pm_sto = gather(pm_sto);
pred = gather(pred);
v_tilde_mat = inf(obj.nStates, obj.nStates);
% Loop over symbols (pure CPU Viterbi)
for sym_i = 1:num_syms
% Current branch metrics for all edges
c_hat_curr = C_all_cpu(:, sym_i); % [nFeasible x 1]
% ACS Update
pm = pm - min(pm);
v_tilde = pm(obj.valid_from_idx) + c_hat_curr;
v_tilde_mat(obj.valid) = v_tilde;
[pm_next, pred(sym_i,:)] = min(v_tilde_mat, [], 2);
pm_next = pm_next - min(pm_next);
pm = pm_next;
pm_sto(:,sym_i) = pm;
end
symbol = num_syms; % Update for traceback
else
% =========================================================
% STANDARD SEQUENTIAL LOOP (Training / CPU Inference)
% =========================================================
pow_vec = (obj.nSym .^ (0:obj.L-1)).'; % Pre-calc powers for keygen
for sample = start_sample:obj.sps:end_sample
symbol = (sample - start_sample)/obj.sps + 1;
sym_idx = start_symbol + (symbol - 1);
% --- Observation window (with delta)
i1 = sample - obj.Nf + 1 + obj.delta;
i2 = sample + obj.delta;
buf = x(max(1,i1):min(length(x),i2));
padL = max(0,1 - i1);
padR = max(0,i2 - length(x));
yk = [zeros(padL,1); buf(:); zeros(padR,1)];
yk = [yk;1];
% --- Branch metrics
c_hat = (yk.' * obj.w).';
pm = pm - min(pm);
v_tilde = pm(obj.valid_from_idx) + c_hat;
% --- allocate once
if epoch==1 && symbol==1
obj.true_to_state_idx = ones(ceil(N/obj.sps),1,'uint32');
end
% --- previous "to" becomes "from"
if symbol>1
true_from_state_idx = obj.true_to_state_idx(symbol-1);
else
true_from_state_idx = 1;
end
% --- compute or reuse "to" state
if epoch==1
if sym_idx>=obj.L
% OPTIMIZED KEY GENERATION: Use pre-calculated indices
% key_to = obj.seq2key(d(sym_idx-obj.L+1:sym_idx));
% Extract subsequence of indices (flip needed as per seq2key logic?)
% seq2key does: ismember(flip(seq)...).
% d_indices is 0-based index of d.
% We want indices of d(sym_idx-obj.L+1 : sym_idx)
d_sub = d_indices(sym_idx-obj.L+1 : sym_idx);
% seq2key flips the sequence.
% So we need to efficiently calculate scalar key from d_sub.
% key = 1 + sum( flip(d_sub) .* pow );
% Let's do it manually to be fast
key_to = 1 + sum(flip(d_sub) .* pow_vec);
state_idx = obj.key_table(key_to);
if state_idx==0
state_idx = true_from_state_idx;
end
obj.true_to_state_idx(symbol) = state_idx;
else
obj.true_to_state_idx(symbol) = true_from_state_idx;
end
end
true_to_state_idx = obj.true_to_state_idx(symbol);
% --- fast Dirac creation
dirac = zeros(obj.nFeasible,1,'like',x); % inherit type (gpu or cpu)
trans_idx = obj.trans_index(true_to_state_idx,true_from_state_idx);
if trans_idx~=0
dirac(trans_idx)=1;
end
% --- ensure valid (from,to)
if ~any(dirac)
mask = obj.valid_from_idx==true_from_state_idx & ...
obj.valid_to_idx ==true_to_state_idx;
if any(mask)
dirac(mask) = 1;
else
idx = find(obj.valid_from_idx==true_from_state_idx,1,'first');
dirac(idx) = 1;
obj.true_to_state_idx(symbol) = obj.valid_to_idx(idx);
end
end
% ===================================================================
% TRAINING MODE (weight update)
% ===================================================================
if training
% --- Softmax and CE
v_shift = -(v_tilde - min(v_tilde));
v_shift = min(v_shift,100);
expv = exp(v_shift);
p = expv./(sum(expv)+eps);
CE_symbol(symbol) = -log(p(dirac==1)+eps);
% --- CE smoothing and adaptive μ
if sym_idx>obj.L
CE_smooth(symbol)=0.01*CE_symbol(symbol)+0.99*CE_symbol(symbol-1);
else
CE_smooth(symbol)=CE_symbol(symbol);
end
CE_accum=CE_accum+CE_symbol(symbol);
% --- Gradient update
dmp=(dirac-p)';
dL_Dw=(yk).*dmp;
if sym_idx>=obj.L
if obj.adaptive_mu
mu_eff=CE_smooth(symbol);
mu_eff=max(min(mu_eff,0.2),1e-4);
else
mu_eff=mu;
end
obj.w=obj.w - mu_eff.*dL_Dw;
end
end
% ===================================================================
% DECODING MODE (Viterbi only)
% ===================================================================
% Compare-Select (always executed)
vmat=inf(obj.nStates,obj.nStates);
vmat(obj.valid)=v_tilde;
[pm_next,pred(symbol,:)]=min(vmat,[],2);
pm_next=pm_next-min(pm_next);
pm=pm_next;
pm_sto(:,symbol)=pm;
end
end
% --- Traceback
[~,s_end]=min(pm);
vpath=zeros(symbol,1,'uint32');
vpath(symbol)=s_end;
for n=symbol:-1:2
vpath(n-1)=pred(n,vpath(n));
end
y_ref=d(start_symbol:end);
y=obj.first_sym(vpath);
% --- BER/CE reporting and plots
if training
err=sum(y~=y_ref(1:length(y)));
ser=err/length(y);
try
ref_bits=PAMmapper(obj.S,0).demap(y_ref(1:length(y)));
eq_bits=PAMmapper(obj.S,0).demap(y);
[~,~,ber,~]=calc_ber(ref_bits,eq_bits,"skip_front",10,"skip_end",10,"returnErrorLocation",1);
fprintf('Epoch %d - BER: %.2e\n',epoch,ber);
obj.ber(epoch)=ber;
catch
fprintf('Epoch %d - SER: %.2e\n',epoch,ser);
obj.ber(epoch)=ser;
end
obj.ce(epoch)=CE_accum/symbol;
if debug && mod(epoch,10)==1 && showPlots
figure(10);clf
subplot(3,2,1:2);
if obj.use_gpu, wm=gather(obj.w); else, wm=obj.w; end
imagesc(wm);axis xy;colorbar;title('Filter W');
subplot(3,2,3);
vtilde_mat=NaN(obj.nStates,obj.nStates);
vtilde_mat(obj.valid)=gather(v_tilde); % gather just in case
imagesc(vtilde_mat);axis xy;colorbar;title('Path Metrics (v\_tilde)');
subplot(3,2,4);
plot(1:symbol,gather(pm_sto));title('Path Metric Evolution');
subplot(3,2,5);hold on;
scatter(1:symbol,CE_symbol,1,'.');
scatter(1:symbol,CE_smooth,1,'.');
title('Cross Entropy');
subplot(3,2,6);hold on;
yyaxis left
scatter(1:length(obj.ce),obj.ce,10,'s','filled');
ylabel('Cross Entropy');
yyaxis right
scatter(1:length(obj.ber),obj.ber,10,'d','filled');
set(gca,'YScale','log');
ylabel('BER (log)');
xlabel('Epoch');grid on;
title('Convergence');
drawnow;
end
end
end
end
% ==============================================================
% Helper: Sequence key (always scalar)
% ==============================================================
function key = seq2key(obj, seq)
[~, idx] = ismember(flip(seq), obj.constellation);
pow = (obj.nSym .^ (0:obj.L-1)).';
key = 1 + sum((idx(:) - 1) .* pow);
end
end
end

View File

@@ -0,0 +1,311 @@
classdef FFE_A1 < handle
%FFE_A1 Feed-forward equalizer with A1 moving-average input suppression.
%
% This class is intentionally narrow: it contains the normal adaptive FFE
% flow plus the A1 block-rate moving-average input correction. A2,
% DC-tracking, and optimizer logic are kept out of this file.
properties
sps
order
e
e_tr
error
len_tr
mu_tr
epochs_tr
adaption_technique
dd_mode
mu_dd
epochs_dd
dd_len_fraction
% A1 moving-average input suppression
dc_avg_bufferlength_a1
dc_avg_update_blocklength_a1
dc_smoothing_a1
P
constellation
decide
save_debug = 0
debug_struct
end
methods
function obj = FFE_A1(options)
arguments(Input)
options.sps = 2
options.order = 15
options.len_tr = 4096
options.mu_tr = 0
options.epochs_tr = 5
options.adaption_technique adaption_method = adaption_method.lms
options.dd_mode = 1
options.mu_dd = 1e-5
options.epochs_dd = 5
options.dd_len_fraction = 1
options.dc_avg_bufferlength_a1 = 0
options.dc_avg_update_blocklength_a1 = 0
options.dc_smoothing_a1 = 0
options.decide = false
options.save_debug = 0
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
assert(obj.dc_avg_bufferlength_a1 >= 0);
assert(obj.dc_avg_update_blocklength_a1 >= 0);
obj.e = zeros(obj.order,1);
obj.error = 0;
obj.dc_avg_bufferlength_a1 = floor(obj.dc_avg_bufferlength_a1);
obj.dc_avg_update_blocklength_a1 = floor(obj.dc_avg_update_blocklength_a1);
obj.dc_smoothing_a1 = min(max(obj.dc_smoothing_a1,0),1);
end
function [X,Noi] = process(obj,X,D)
X = X.normalize("mode","rms");
obj.constellation = unique(D.signal);
delta = 0.05;
obj.P = (1/delta) * eye(obj.order);
training = true;
showviz = false;
obj.equalize(X.signal,D.signal,obj.mu_tr,obj.epochs_tr,obj.len_tr,training,showviz);
obj.e_tr = obj.e;
n = X.length;
training = false;
if obj.dd_mode
[signal,decision] = obj.equalize(X.signal,D.signal,obj.mu_dd,obj.epochs_dd,n,training,showviz);
else
[signal,decision] = obj.equalize(X.signal,D.signal,0,1,n,training,showviz);
end
if obj.decide
X.signal = decision;
else
X.signal = signal;
end
X.fs = D.fs;
X = X.logbookentry([num2str(obj.order),' tap FFE A1']);
Noi = X - D;
end
function [y,d_hat] = equalize(obj,x,d,mu,epochs,N,training,showviz)
arguments
obj
x
d
mu
epochs
N
training
showviz %#ok<INUSA>
end
x = [zeros(floor(obj.order/2),1); x; zeros(obj.order,1)];
lambda = mu;
n_symbols = ceil(N / obj.sps);
y = zeros(n_symbols,1);
d_hat = zeros(n_symbols,1);
err = zeros(n_symbols,1);
constellation = obj.constellation;
adaption_code = obj.adaptionCode();
adaption_is_rls = adaption_code == 3;
mask = ones(obj.order,1);
maincursor_pos = ceil(length(obj.e)/2);
grad = 0;
weight = 0;
update = 0;
if mu == 0 || (~obj.dd_mode && ~training)
epochs = 1;
end
dc_avg_enabled = obj.dc_avg_bufferlength_a1 > 1;
if dc_avg_enabled
dc_avg_buffer = NaN(obj.dc_avg_bufferlength_a1,1);
dc_avg_buffer_pos = 0;
dc_avg_sum = 0;
dc_avg_valid_count = 0;
dc_avg_est = 0;
dc_avg_update_blocklength = obj.dc_avg_update_blocklength_a1;
if dc_avg_update_blocklength <= 0
dc_avg_update_blocklength = obj.dc_avg_bufferlength_a1;
end
dc_avg_update_blocklength = max(1,floor(dc_avg_update_blocklength));
dc_avg_input_offset = floor(obj.order/2);
% Hardware-like A1 is causal; dc_smoothing_a1 is reserved for offline variants.
end
debug_enabled = obj.save_debug;
if debug_enabled
obj.debug_struct.error = NaN(1,n_symbols);
obj.debug_struct.error_first_epoch = NaN(1,n_symbols);
obj.debug_struct.main_cursor = NaN(1,n_symbols);
obj.debug_struct.mu_nlms = NaN(1,n_symbols);
obj.debug_struct.update_gradient = NaN(1,n_symbols);
obj.debug_struct.dc_avg_offset = NaN(1,n_symbols);
if training
obj.debug_struct.error_tr = NaN(1,n_symbols);
obj.debug_struct.update_tr = NaN(1,n_symbols);
else
obj.debug_struct.error_dd = NaN(1,n_symbols);
obj.debug_struct.update = NaN(1,n_symbols);
end
end
for epoch = 1:epochs
symbol = 0;
for sample = 1:obj.sps:N
symbol = symbol + 1;
dc_avg_offset = 0;
U = x(obj.order+sample-1:-1:sample);
if dc_avg_enabled
dc_avg_offset = dc_avg_est;
U = U - dc_avg_offset;
end
y(symbol,1) = (obj.e.*mask).' * U;
if dc_avg_enabled
raw_sample_idx = dc_avg_input_offset + sample;
[dc_avg_buffer,dc_avg_buffer_pos,dc_avg_sum,dc_avg_valid_count,dc_avg_est] = ...
obj.updateA1Buffer(dc_avg_buffer,dc_avg_buffer_pos,dc_avg_sum, ...
dc_avg_valid_count,dc_avg_est,x(raw_sample_idx),symbol, ...
dc_avg_update_blocklength);
end
if training
d_hat(symbol,1) = d(symbol);
else
[~,symbol_idx] = min(abs(y(symbol) - constellation));
d_hat(symbol,1) = constellation(symbol_idx);
end
err(symbol) = d_hat(symbol) - y(symbol);
if training || obj.dd_mode
switch adaption_code
case 1
normU = (U.'*U) + eps;
weight = mu / normU;
grad = err(symbol) * U;
update = grad * weight;
obj.e = obj.e + update;
case 2
weight = mu;
grad = err(symbol) * U;
update = grad * weight;
obj.e = obj.e + update;
case 3
denom = lambda + U.' * obj.P * U;
k = (obj.P * U) / denom;
update = k * err(symbol);
obj.e = obj.e + update;
obj.P = (1/lambda) * (obj.P - k * (U.' * obj.P));
end
end
if debug_enabled && epoch == 1
obj.debug_struct.error_first_epoch(1,symbol) = err(symbol) * err(symbol)';
end
if debug_enabled && epoch == epochs
error_power = err(symbol) * err(symbol)';
update_power = update.'*update ./ (sqrt((obj.e.'*obj.e) / obj.order) + eps);
obj.debug_struct.error(1,symbol) = error_power;
obj.debug_struct.main_cursor(1,symbol) = abs(obj.e(maincursor_pos));
obj.debug_struct.mu_nlms(1,symbol) = weight;
obj.debug_struct.update_gradient(1,symbol) = grad.'*grad;
obj.debug_struct.dc_avg_offset(1,symbol) = dc_avg_offset;
if training
obj.debug_struct.error_tr(1,symbol) = error_power;
obj.debug_struct.update_tr(1,symbol) = update_power;
else
obj.debug_struct.error_dd(1,symbol) = error_power;
obj.debug_struct.update(1,symbol) = update_power;
end
end
end
end
if ~adaption_is_rls
obj.P = [];
end
end
end
methods (Access = private)
function adaption_code = adaptionCode(obj)
if obj.adaption_technique == adaption_method.nlms
adaption_code = 1;
elseif obj.adaption_technique == adaption_method.lms
adaption_code = 2;
elseif obj.adaption_technique == adaption_method.rls
adaption_code = 3;
else
builtin("error","FFE_A1:InvalidAdaptionTechnique", ...
"Unsupported FFE adaption technique.");
end
end
end
methods (Static, Access = private)
function [buffer,buffer_pos,buffer_sum,valid_count,estimate] = ...
updateA1Buffer(buffer,buffer_pos,buffer_sum,valid_count,estimate, ...
new_value,symbol,update_blocklength)
buffer_pos = buffer_pos + 1;
if buffer_pos > numel(buffer)
buffer_pos = 1;
end
old_value = buffer(buffer_pos);
if isfinite(old_value)
buffer_sum = buffer_sum - old_value;
valid_count = valid_count - 1;
end
if isfinite(new_value)
buffer(buffer_pos) = new_value;
buffer_sum = buffer_sum + new_value;
valid_count = valid_count + 1;
else
buffer(buffer_pos) = NaN;
end
if mod(symbol,update_blocklength) == 0
if valid_count == 0
estimate = 0;
else
estimate = buffer_sum / valid_count;
end
end
end
end
end

View File

@@ -0,0 +1,347 @@
classdef FFE_A2Residual < FFE_plain
%FFE_A2Residual Plain FFE with A2 residual MPI suppression.
%
% This class reuses FFE_plain for process flow and BER-based mu
% optimization. Only equalize is specialized to add the paper-style A2
% residual correction: average y_raw - d_hat per PAM level, subtract the
% level-weighted residual estimate, then redo the DD decision.
properties
dc_level_avg_bufferlength_a2
dc_level_update_blocklength_a2
dc_smoothing_a2
dc_level_weights_a2
end
methods
function obj = FFE_A2Residual(options)
arguments(Input)
options.sps = 2
options.order = 15
options.len_tr = 4096
options.mu_tr = 0
options.epochs_tr = 5
options.adaption_technique adaption_method = adaption_method.lms
options.dd_mode = 1
options.mu_dd = 1e-5
options.epochs_dd = 5
options.dd_len_fraction = 1
options.dc_level_avg_bufferlength_a2 = 0
options.dc_level_update_blocklength_a2 = 0
options.dc_smoothing_a2 = 0
options.dc_level_weights_a2 = 0
options.decide = false
options.save_debug = 0
options.optmize_mus = 0
options.mu_optimization_len = 2^15
options.plot_mu_optimization = 0
options.mu_optimization_fignum = 3010
end
obj@FFE_plain( ...
"sps",options.sps, ...
"order",options.order, ...
"len_tr",options.len_tr, ...
"mu_tr",options.mu_tr, ...
"epochs_tr",options.epochs_tr, ...
"adaption_technique",options.adaption_technique, ...
"dd_mode",options.dd_mode, ...
"mu_dd",options.mu_dd, ...
"epochs_dd",options.epochs_dd, ...
"dd_len_fraction",options.dd_len_fraction, ...
"decide",options.decide, ...
"save_debug",options.save_debug, ...
"optmize_mus",options.optmize_mus, ...
"mu_optimization_len",options.mu_optimization_len, ...
"plot_mu_optimization",options.plot_mu_optimization, ...
"mu_optimization_fignum",options.mu_optimization_fignum);
obj.dc_level_avg_bufferlength_a2 = floor(options.dc_level_avg_bufferlength_a2);
obj.dc_level_update_blocklength_a2 = floor(options.dc_level_update_blocklength_a2);
obj.dc_smoothing_a2 = min(max(options.dc_smoothing_a2,0),1);
obj.dc_level_weights_a2 = options.dc_level_weights_a2;
assert(obj.dc_level_avg_bufferlength_a2 >= 0);
assert(obj.dc_level_update_blocklength_a2 >= 0);
end
function [y,d_hat] = equalize(obj,x,d,mu,epochs,N,training,showviz)
arguments
obj
x
d
mu
epochs
N
training
showviz
end
unused_showviz = showviz; %#ok<NASGU>
x = x(:);
d = d(:);
N = obj.validSampleLength(N,x,d);
n_symbols = N / obj.sps;
y = zeros(n_symbols,1);
d_hat = zeros(n_symbols,1);
if n_symbols == 0
return
end
if isempty(obj.constellation)
obj.constellation = unique(d);
end
decision_constellation = obj.constellation(:);
x = [zeros(floor(obj.order/2),1); x; zeros(obj.order,1)];
lambda = mu;
mask = ones(obj.order,1);
maincursor_pos = ceil(length(obj.e)/2);
adaption_code = obj.adaptionCode();
if mu == 0 || (~obj.dd_mode && ~training)
epochs = 1;
end
dc_level_enabled = obj.dc_level_avg_bufferlength_a2 > 1 && any(obj.dc_level_weights_a2(:) ~= 0);
if dc_level_enabled
[dc_level_weight_by_level,n_levels] = obj.expandedLevelWeights(decision_constellation);
dc_level_buffer_len = obj.dc_level_avg_bufferlength_a2;
dc_level_err_buffer = NaN(n_levels,dc_level_buffer_len);
dc_level_err_buffer_pos_by_level = zeros(n_levels,1);
dc_level_err_sum_by_level = zeros(n_levels,1);
dc_level_buffer_valid_count_by_level = zeros(n_levels,1);
dc_level_mpi_est_by_level = zeros(n_levels,1);
dc_level_valid_count_by_level = zeros(n_levels,1);
dc_level_update_blocklength = obj.dc_level_update_blocklength_a2;
if dc_level_update_blocklength <= 0
dc_level_update_blocklength = dc_level_buffer_len;
end
dc_level_update_blocklength = max(1,floor(dc_level_update_blocklength));
dc_level_window_future_fraction = obj.dc_smoothing_a2; %#ok<NASGU>
end
debug_enabled = obj.save_debug;
if debug_enabled
obj.initializeDebug(n_symbols,training);
end
for epoch = 1:epochs
symbol = 0;
for sample = 1:obj.sps:N
symbol = symbol + 1;
grad = zeros(obj.order,1);
update = zeros(obj.order,1);
weight = 0;
dc_level_mpi_est = 0;
dc_level_weight = 0;
dc_level_valid_count = 0;
U = x(obj.order+sample-1:-1:sample);
y(symbol,1) = (obj.e.*mask).' * U;
y_raw = y(symbol);
if training
d_hat(symbol,1) = d(symbol);
[~,symbol_idx] = min(abs(d_hat(symbol) - decision_constellation));
dc_level_decision_level = decision_constellation(symbol_idx);
else
[~,symbol_idx] = min(abs(y_raw - decision_constellation));
d_hat(symbol,1) = decision_constellation(symbol_idx);
dc_level_decision_level = decision_constellation(symbol_idx);
end
dc_level_symbol_idx = symbol_idx;
if dc_level_enabled
dc_level_mpi_est = dc_level_mpi_est_by_level(dc_level_symbol_idx);
dc_level_valid_count = dc_level_valid_count_by_level(dc_level_symbol_idx);
dc_level_weight = dc_level_weight_by_level(dc_level_symbol_idx) * ...
min(dc_level_valid_count / dc_level_buffer_len,1);
mpi_err = y_raw - d_hat(symbol);
y(symbol,1) = y_raw - dc_level_weight * dc_level_mpi_est;
if training
d_hat(symbol,1) = d(symbol);
else
[~,symbol_idx] = min(abs(y(symbol) - decision_constellation));
d_hat(symbol,1) = decision_constellation(symbol_idx);
dc_level_decision_level = decision_constellation(symbol_idx);
end
[dc_level_err_buffer,dc_level_err_buffer_pos_by_level, ...
dc_level_err_sum_by_level,dc_level_buffer_valid_count_by_level, ...
dc_level_mpi_est_by_level,dc_level_valid_count_by_level] = ...
obj.updateResidualBuffer( ...
dc_level_err_buffer,dc_level_err_buffer_pos_by_level, ...
dc_level_err_sum_by_level,dc_level_buffer_valid_count_by_level, ...
dc_level_mpi_est_by_level,dc_level_valid_count_by_level, ...
mpi_err,dc_level_symbol_idx,symbol,dc_level_update_blocklength);
end
err = d_hat(symbol) - y(symbol);
if mu ~= 0 && (training || obj.dd_mode)
switch adaption_code
case 1
weight = mu / ((U.'*U) + eps);
grad = err * U;
update = grad * weight;
obj.e = obj.e + update;
case 2
weight = mu;
grad = err * U;
update = grad * weight;
obj.e = obj.e + update;
case 3
denom = lambda + U.' * obj.P * U;
k = (obj.P * U) / denom;
update = k * err;
obj.e = obj.e + update;
obj.P = (1/lambda) * (obj.P - k * (U.' * obj.P));
end
end
if debug_enabled && epoch == 1
obj.debug_struct.error_first_epoch(1,symbol) = err * err';
end
if debug_enabled && epoch == epochs
error_power = err * err';
update_power = update.'*update ./ (sqrt((obj.e.'*obj.e) / obj.order) + eps);
obj.debug_struct.error(1,symbol) = error_power;
obj.debug_struct.main_cursor(1,symbol) = abs(obj.e(maincursor_pos));
obj.debug_struct.mu_nlms(1,symbol) = weight;
obj.debug_struct.update_gradient(1,symbol) = grad.'*grad;
obj.debug_struct.dc_level_mpi_est(1,symbol) = dc_level_mpi_est;
obj.debug_struct.dc_level_weight(1,symbol) = dc_level_weight;
obj.debug_struct.dc_level_valid_count(1,symbol) = dc_level_valid_count;
obj.debug_struct.dc_level_symbol_idx(1,symbol) = dc_level_symbol_idx;
obj.debug_struct.dc_level_decision_level(1,symbol) = dc_level_decision_level;
obj.debug_struct.dc_level_y_raw(1,symbol) = y_raw;
if training
obj.debug_struct.error_tr(1,symbol) = error_power;
obj.debug_struct.update_tr(1,symbol) = update_power;
else
obj.debug_struct.error_dd(1,symbol) = error_power;
obj.debug_struct.update(1,symbol) = update_power;
end
end
end
end
end
end
methods (Access = private)
function N = validSampleLength(obj,N,x,d)
N_available = min(numel(x),numel(d) * obj.sps);
N = min(N,N_available);
N = obj.sps * floor(N / obj.sps);
N = max(0,N);
end
function adaption_code = adaptionCode(obj)
if obj.adaption_technique == adaption_method.nlms
adaption_code = 1;
elseif obj.adaption_technique == adaption_method.lms
adaption_code = 2;
elseif obj.adaption_technique == adaption_method.rls
adaption_code = 3;
else
builtin("error","FFE_A2Residual:InvalidAdaptionTechnique", ...
"Unsupported FFE adaption technique.");
end
end
function [weights,n_levels] = expandedLevelWeights(obj,decision_constellation)
n_levels = numel(decision_constellation);
if isscalar(obj.dc_level_weights_a2)
weights = repmat(obj.dc_level_weights_a2,n_levels,1);
elseif numel(obj.dc_level_weights_a2) == n_levels
weights = obj.dc_level_weights_a2(:);
else
builtin("error","FFE_A2Residual:InvalidDCLevelWeights", ...
"dc_level_weights_a2 must be scalar or have one entry per constellation level.");
end
end
function initializeDebug(obj,n_symbols,training)
obj.debug_struct.error = NaN(1,n_symbols);
obj.debug_struct.error_first_epoch = NaN(1,n_symbols);
obj.debug_struct.main_cursor = NaN(1,n_symbols);
obj.debug_struct.mu_nlms = NaN(1,n_symbols);
obj.debug_struct.update_gradient = NaN(1,n_symbols);
obj.debug_struct.dc_level_mpi_est = NaN(1,n_symbols);
obj.debug_struct.dc_level_weight = NaN(1,n_symbols);
obj.debug_struct.dc_level_valid_count = NaN(1,n_symbols);
obj.debug_struct.dc_level_symbol_idx = NaN(1,n_symbols);
obj.debug_struct.dc_level_decision_level = NaN(1,n_symbols);
obj.debug_struct.dc_level_y_raw = NaN(1,n_symbols);
if training
obj.debug_struct.error_tr = NaN(1,n_symbols);
obj.debug_struct.update_tr = NaN(1,n_symbols);
else
obj.debug_struct.error_dd = NaN(1,n_symbols);
obj.debug_struct.update = NaN(1,n_symbols);
end
end
end
methods (Static, Access = private)
function [buffer,buffer_pos_by_level,err_sum_by_level,buffer_valid_count_by_level, ...
mpi_est_by_level,valid_count_by_level] = updateResidualBuffer( ...
buffer,buffer_pos_by_level,err_sum_by_level,buffer_valid_count_by_level, ...
mpi_est_by_level,valid_count_by_level,new_value,symbol_idx,symbol, ...
update_blocklength)
buffer_len = size(buffer,2);
buffer_pos = buffer_pos_by_level(symbol_idx) + 1;
if buffer_pos > buffer_len
buffer_pos = 1;
end
buffer_pos_by_level(symbol_idx) = buffer_pos;
old_value = buffer(symbol_idx,buffer_pos);
if isfinite(old_value)
err_sum_by_level(symbol_idx) = err_sum_by_level(symbol_idx) - old_value;
buffer_valid_count_by_level(symbol_idx) = buffer_valid_count_by_level(symbol_idx) - 1;
end
if isfinite(new_value)
buffer(symbol_idx,buffer_pos) = new_value;
err_sum_by_level(symbol_idx) = err_sum_by_level(symbol_idx) + new_value;
buffer_valid_count_by_level(symbol_idx) = buffer_valid_count_by_level(symbol_idx) + 1;
else
buffer(symbol_idx,buffer_pos) = NaN;
end
if mod(symbol,update_blocklength) == 0
if update_blocklength == 1
valid_count_by_level(symbol_idx) = buffer_valid_count_by_level(symbol_idx);
if valid_count_by_level(symbol_idx) == 0
mpi_est_by_level(symbol_idx) = 0;
else
mpi_est_by_level(symbol_idx) = ...
err_sum_by_level(symbol_idx) / valid_count_by_level(symbol_idx);
end
else
valid_count_by_level = buffer_valid_count_by_level;
has_valid = valid_count_by_level > 0;
mpi_est_by_level(:) = 0;
mpi_est_by_level(has_valid) = ...
err_sum_by_level(has_valid) ./ valid_count_by_level(has_valid);
end
end
end
end
end

View File

@@ -0,0 +1,361 @@
classdef FFE_A2TrackedLevels < handle
%FFE_A2TrackedLevels FFE with A2 tracked-level decision thresholds.
%
% This class is intentionally narrow: it contains the normal adaptive FFE
% flow plus the A2 tracked-level decision logic. The output sample y stays
% unchanged; only the decision constellation is moved by the tracked
% level-wise offsets.
properties
sps
order
e
e_tr
error
len_tr
mu_tr
epochs_tr
adaption_technique
dd_mode
mu_dd
epochs_dd
dd_len_fraction
% A2 tracked-level decision
dc_level_avg_bufferlength_a2
dc_level_update_blocklength_a2
dc_smoothing_a2
dc_level_weights_a2
P
constellation
decide
save_debug = 0
debug_struct
end
methods
function obj = FFE_A2TrackedLevels(options)
arguments(Input)
options.sps = 2
options.order = 15
options.len_tr = 4096
options.mu_tr = 0
options.epochs_tr = 5
options.adaption_technique adaption_method = adaption_method.lms
options.dd_mode = 1
options.mu_dd = 1e-5
options.epochs_dd = 5
options.dd_len_fraction = 1
options.dc_level_avg_bufferlength_a2 = 0
options.dc_level_update_blocklength_a2 = 0
options.dc_smoothing_a2 = 0
options.dc_level_weights_a2 = 0
options.decide = false
options.save_debug = 0
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
assert(obj.dc_level_avg_bufferlength_a2 >= 0);
assert(obj.dc_level_update_blocklength_a2 >= 0);
obj.e = zeros(obj.order,1);
obj.error = 0;
obj.dc_level_avg_bufferlength_a2 = floor(obj.dc_level_avg_bufferlength_a2);
obj.dc_level_update_blocklength_a2 = floor(obj.dc_level_update_blocklength_a2);
obj.dc_smoothing_a2 = min(max(obj.dc_smoothing_a2,0),1);
end
function [X,Noi] = process(obj,X,D)
X = X.normalize("mode","rms");
obj.constellation = unique(D.signal);
delta = 0.05;
obj.P = (1/delta) * eye(obj.order);
training = true;
showviz = false;
obj.equalize(X.signal,D.signal,obj.mu_tr,obj.epochs_tr,obj.len_tr,training,showviz);
obj.e_tr = obj.e;
n = X.length;
training = false;
if obj.dd_mode
[signal,decision] = obj.equalize(X.signal,D.signal,obj.mu_dd,obj.epochs_dd,n,training,showviz);
else
[signal,decision] = obj.equalize(X.signal,D.signal,0,1,n,training,showviz);
end
if obj.decide
X.signal = decision;
else
X.signal = signal;
end
X.fs = D.fs;
X = X.logbookentry([num2str(obj.order),' tap FFE A2 tracked levels']);
Noi = X - D;
end
function [y,d_hat] = equalize(obj,x,d,mu,epochs,N,training,showviz)
arguments
obj
x
d
mu
epochs
N
training
showviz %#ok<INUSA>
end
x = [zeros(floor(obj.order/2),1); x; zeros(obj.order,1)];
lambda = mu;
n_symbols = ceil(N / obj.sps);
y = zeros(n_symbols,1);
d_hat = zeros(n_symbols,1);
err = zeros(n_symbols,1);
constellation = obj.constellation;
adaption_code = obj.adaptionCode();
adaption_is_rls = adaption_code == 3;
mask = ones(obj.order,1);
maincursor_pos = ceil(length(obj.e)/2);
grad = 0;
weight = 0;
update = 0;
if mu == 0 || (~obj.dd_mode && ~training)
epochs = 1;
end
dc_level_enabled = obj.dc_level_avg_bufferlength_a2 > 1 && any(obj.dc_level_weights_a2(:) ~= 0);
if dc_level_enabled
if isempty(constellation)
builtin("error","FFE_A2TrackedLevels:MissingConstellation", ...
"A2 tracked-level decision requires obj.constellation to be set.");
end
n_levels = numel(constellation);
if isscalar(obj.dc_level_weights_a2)
dc_level_weight_by_level = repmat(obj.dc_level_weights_a2,n_levels,1);
elseif numel(obj.dc_level_weights_a2) == n_levels
dc_level_weight_by_level = obj.dc_level_weights_a2(:);
else
builtin("error","FFE_A2TrackedLevels:InvalidDCLevelWeights", ...
"dc_level_weights_a2 must be scalar or have one entry per constellation level.");
end
dc_level_buffer_len = obj.dc_level_avg_bufferlength_a2;
dc_level_buffer = NaN(n_levels,dc_level_buffer_len);
dc_level_buffer_pos_by_level = zeros(n_levels,1);
dc_level_sum_by_level = zeros(n_levels,1);
dc_level_buffer_valid_count_by_level = zeros(n_levels,1);
dc_level_offset_by_level = zeros(n_levels,1);
dc_level_valid_count_by_level = zeros(n_levels,1);
dc_level_update_blocklength = obj.dc_level_update_blocklength_a2;
if dc_level_update_blocklength <= 0
dc_level_update_blocklength = dc_level_buffer_len;
end
dc_level_update_blocklength = max(1,floor(dc_level_update_blocklength));
dc_level_window_future_fraction = obj.dc_smoothing_a2; %#ok<NASGU>
end
debug_enabled = obj.save_debug;
if debug_enabled
obj.debug_struct.error = NaN(1,n_symbols);
obj.debug_struct.error_first_epoch = NaN(1,n_symbols);
obj.debug_struct.main_cursor = NaN(1,n_symbols);
obj.debug_struct.mu_nlms = NaN(1,n_symbols);
obj.debug_struct.update_gradient = NaN(1,n_symbols);
obj.debug_struct.dc_level_mpi_est = NaN(1,n_symbols);
obj.debug_struct.dc_level_weight = NaN(1,n_symbols);
obj.debug_struct.dc_level_valid_count = NaN(1,n_symbols);
obj.debug_struct.dc_level_symbol_idx = NaN(1,n_symbols);
obj.debug_struct.dc_level_decision_level = NaN(1,n_symbols);
obj.debug_struct.dc_level_y_raw = NaN(1,n_symbols);
if training
obj.debug_struct.error_tr = NaN(1,n_symbols);
obj.debug_struct.update_tr = NaN(1,n_symbols);
else
obj.debug_struct.error_dd = NaN(1,n_symbols);
obj.debug_struct.update = NaN(1,n_symbols);
end
end
for epoch = 1:epochs
symbol = 0;
for sample = 1:obj.sps:N
symbol = symbol + 1;
dc_level_offset = 0;
dc_level_weight = 0;
dc_level_valid_count = 0;
U = x(obj.order+sample-1:-1:sample);
y(symbol,1) = (obj.e.*mask).' * U;
y_raw = y(symbol);
if training
d_hat(symbol,1) = d(symbol);
[~,symbol_idx] = min(abs(d_hat(symbol) - constellation));
dc_level_decision_level = constellation(symbol_idx);
else
if dc_level_enabled
dc_level_ramp_by_level = min(dc_level_valid_count_by_level / dc_level_buffer_len,1);
dc_level_decision_constellation = constellation + ...
dc_level_weight_by_level .* dc_level_ramp_by_level .* dc_level_offset_by_level;
[~,symbol_idx] = min(abs(y_raw - dc_level_decision_constellation));
dc_level_decision_level = dc_level_decision_constellation(symbol_idx);
else
[~,symbol_idx] = min(abs(y_raw - constellation));
dc_level_decision_level = constellation(symbol_idx);
end
d_hat(symbol,1) = constellation(symbol_idx);
end
dc_level_symbol_idx = symbol_idx;
if dc_level_enabled
dc_level_offset = dc_level_offset_by_level(symbol_idx);
dc_level_valid_count = dc_level_valid_count_by_level(symbol_idx);
dc_level_weight = dc_level_weight_by_level(symbol_idx) * ...
min(dc_level_valid_count / dc_level_buffer_len,1);
[dc_level_buffer,dc_level_buffer_pos_by_level,dc_level_sum_by_level, ...
dc_level_buffer_valid_count_by_level,dc_level_offset_by_level, ...
dc_level_valid_count_by_level] = obj.updateTrackedLevelBuffer( ...
dc_level_buffer,dc_level_buffer_pos_by_level,dc_level_sum_by_level, ...
dc_level_buffer_valid_count_by_level,dc_level_offset_by_level, ...
dc_level_valid_count_by_level,y_raw,symbol_idx,symbol, ...
dc_level_update_blocklength,constellation);
end
err(symbol) = d_hat(symbol) - y(symbol);
if training || obj.dd_mode
switch adaption_code
case 1
normU = (U.'*U) + eps;
weight = mu / normU;
grad = err(symbol) * U;
update = grad * weight;
obj.e = obj.e + update;
case 2
weight = mu;
grad = err(symbol) * U;
update = grad * weight;
obj.e = obj.e + update;
case 3
denom = lambda + U.' * obj.P * U;
k = (obj.P * U) / denom;
update = k * err(symbol);
obj.e = obj.e + update;
obj.P = (1/lambda) * (obj.P - k * (U.' * obj.P));
end
end
if debug_enabled && epoch == 1
obj.debug_struct.error_first_epoch(1,symbol) = err(symbol) * err(symbol)';
end
if debug_enabled && epoch == epochs
error_power = err(symbol) * err(symbol)';
update_power = update.'*update ./ (sqrt((obj.e.'*obj.e) / obj.order) + eps);
obj.debug_struct.error(1,symbol) = error_power;
obj.debug_struct.main_cursor(1,symbol) = abs(obj.e(maincursor_pos));
obj.debug_struct.mu_nlms(1,symbol) = weight;
obj.debug_struct.update_gradient(1,symbol) = grad.'*grad;
obj.debug_struct.dc_level_mpi_est(1,symbol) = dc_level_offset;
obj.debug_struct.dc_level_weight(1,symbol) = dc_level_weight;
obj.debug_struct.dc_level_valid_count(1,symbol) = dc_level_valid_count;
obj.debug_struct.dc_level_symbol_idx(1,symbol) = dc_level_symbol_idx;
obj.debug_struct.dc_level_decision_level(1,symbol) = dc_level_decision_level;
obj.debug_struct.dc_level_y_raw(1,symbol) = y_raw;
if training
obj.debug_struct.error_tr(1,symbol) = error_power;
obj.debug_struct.update_tr(1,symbol) = update_power;
else
obj.debug_struct.error_dd(1,symbol) = error_power;
obj.debug_struct.update(1,symbol) = update_power;
end
end
end
end
if ~adaption_is_rls
obj.P = [];
end
end
end
methods (Access = private)
function adaption_code = adaptionCode(obj)
if obj.adaption_technique == adaption_method.nlms
adaption_code = 1;
elseif obj.adaption_technique == adaption_method.lms
adaption_code = 2;
elseif obj.adaption_technique == adaption_method.rls
adaption_code = 3;
else
builtin("error","FFE_A2TrackedLevels:InvalidAdaptionTechnique", ...
"Unsupported FFE adaption technique.");
end
end
end
methods (Static, Access = private)
function [buffer,buffer_pos_by_level,level_sum_by_level,buffer_valid_count_by_level, ...
offset_by_level,valid_count_by_level] = updateTrackedLevelBuffer( ...
buffer,buffer_pos_by_level,level_sum_by_level,buffer_valid_count_by_level, ...
offset_by_level,valid_count_by_level,new_value,symbol_idx,symbol, ...
update_blocklength,constellation)
buffer_pos = buffer_pos_by_level(symbol_idx) + 1;
if buffer_pos > size(buffer,2)
buffer_pos = 1;
end
buffer_pos_by_level(symbol_idx) = buffer_pos;
old_value = buffer(symbol_idx,buffer_pos);
if isfinite(old_value)
level_sum_by_level(symbol_idx) = level_sum_by_level(symbol_idx) - old_value;
buffer_valid_count_by_level(symbol_idx) = buffer_valid_count_by_level(symbol_idx) - 1;
end
if isfinite(new_value)
buffer(symbol_idx,buffer_pos) = new_value;
level_sum_by_level(symbol_idx) = level_sum_by_level(symbol_idx) + new_value;
buffer_valid_count_by_level(symbol_idx) = buffer_valid_count_by_level(symbol_idx) + 1;
else
buffer(symbol_idx,buffer_pos) = NaN;
end
if mod(symbol,update_blocklength) == 0
valid_count_by_level = buffer_valid_count_by_level;
has_valid = valid_count_by_level > 0;
offset_by_level(:) = 0;
offset_by_level(has_valid) = ...
level_sum_by_level(has_valid) ./ valid_count_by_level(has_valid) - ...
constellation(has_valid);
end
end
end
end

View File

@@ -0,0 +1,752 @@
classdef FFE_DCTracking < FFE_plain
%FFE_DCTracking Plain FFE with the DC-tracking offset loop.
%
% This class reuses the plain FFE process idea and BER mu optimization,
% but keeps only the DC-tracking path from the larger FFE class. A1, A2,
% and delayed FFE tap-update buffers are intentionally excluded.
properties
dc_tracking_mu
dc_tracking_adaptive_enabled
dc_tracking_persistence_gain
dc_tracking_power_exponent
dc_tracking_buffer_len
dc_tracking_mu_eff_max
e_dc
optimize_dc_tracking_params = 0
dc_tracking_optimization
dc_tracking_optimization_iter = 0
dc_tracking_optimization_len
dc_tracking_optimization_max_evals
dc_tracking_optimization_delay_weight
dc_tracking_optimization_smoothing_len
end
properties (Access = private)
dc_tracking_mu_min = 1e-6
dc_tracking_mu_max = 3e-1
dc_tracking_mu_eff_min = 0
end
methods
function obj = FFE_DCTracking(options)
arguments(Input)
options.sps = 2
options.order = 15
options.len_tr = 4096
options.mu_tr = 0
options.epochs_tr = 5
options.adaption_technique adaption_method = adaption_method.lms
options.dd_mode = 1
options.mu_dd = 1e-5
options.epochs_dd = 5
options.dd_len_fraction = 1
options.dc_tracking_mu = 0
options.dc_tracking_adaptive_enabled = false
options.dc_tracking_persistence_gain = 0
options.dc_tracking_power_exponent = 2
options.dc_tracking_buffer_len = 1
options.dc_tracking_mu_eff_max = inf
options.decide = false
options.save_debug = 0
options.optmize_mus = 0
options.mu_optimization_len = 2^15
options.plot_mu_optimization = 0
options.mu_optimization_fignum = 3010
options.optimize_dc_tracking_params = 0
options.dc_tracking_optimization_len = 2^15
options.dc_tracking_optimization_max_evals = 20
options.dc_tracking_optimization_delay_weight = 1e-3
options.dc_tracking_optimization_smoothing_len = 501
end
obj@FFE_plain( ...
"sps",options.sps, ...
"order",options.order, ...
"len_tr",options.len_tr, ...
"mu_tr",options.mu_tr, ...
"epochs_tr",options.epochs_tr, ...
"adaption_technique",options.adaption_technique, ...
"dd_mode",options.dd_mode, ...
"mu_dd",options.mu_dd, ...
"epochs_dd",options.epochs_dd, ...
"dd_len_fraction",options.dd_len_fraction, ...
"decide",options.decide, ...
"save_debug",options.save_debug, ...
"optmize_mus",options.optmize_mus, ...
"mu_optimization_len",options.mu_optimization_len, ...
"plot_mu_optimization",options.plot_mu_optimization, ...
"mu_optimization_fignum",options.mu_optimization_fignum);
obj.dc_tracking_mu = options.dc_tracking_mu;
obj.dc_tracking_adaptive_enabled = options.dc_tracking_adaptive_enabled;
obj.dc_tracking_persistence_gain = options.dc_tracking_persistence_gain;
obj.dc_tracking_power_exponent = options.dc_tracking_power_exponent;
obj.dc_tracking_buffer_len = floor(options.dc_tracking_buffer_len);
obj.dc_tracking_mu_eff_max = options.dc_tracking_mu_eff_max;
obj.e_dc = 0;
obj.optimize_dc_tracking_params = options.optimize_dc_tracking_params;
obj.dc_tracking_optimization_len = options.dc_tracking_optimization_len;
obj.dc_tracking_optimization_max_evals = max(1, ...
floor(options.dc_tracking_optimization_max_evals));
obj.dc_tracking_optimization_delay_weight = ...
max(0,options.dc_tracking_optimization_delay_weight);
obj.dc_tracking_optimization_smoothing_len = max(1, ...
floor(options.dc_tracking_optimization_smoothing_len));
assert(obj.dc_tracking_buffer_len >= 0);
end
function [X,Noi] = process(obj,X,D)
X = X.normalize("mode","rms");
obj.constellation = unique(D.signal);
obj.resetTrackingState();
if obj.optmize_mus
obj.optimizeMus(X.signal,D.signal);
obj.resetTrackingState();
end
if obj.optimize_dc_tracking_params
obj.optimizeDcTrackingParams(X.signal,D.signal);
obj.resetTrackingState();
end
training = true;
showviz = false;
obj.equalize(X.signal,D.signal,obj.mu_tr,obj.epochs_tr,obj.len_tr,training,showviz);
obj.e_tr = obj.e;
n = X.length;
training = false;
if obj.dd_mode
[signal,decision] = obj.equalize(X.signal,D.signal,obj.mu_dd,obj.epochs_dd,n,training,showviz);
else
[signal,decision] = obj.equalize(X.signal,D.signal,0,1,n,training,showviz);
end
if obj.decide
X.signal = decision;
else
X.signal = signal;
end
X.fs = D.fs;
X = X.logbookentry([num2str(obj.order),' tap FFE DC tracking']);
Noi = X - D;
end
function optimizeMus(obj,x,d)
[x_opt,d_opt,N_opt] = obj.optimizationSignals(x,d);
switch obj.adaption_technique
case adaption_method.lms
mu_range = [1e-5, 1e-2];
case adaption_method.nlms
mu_range = [1e-3, 5e-1];
case adaption_method.rls
mu_range = [0.98, 0.99999];
otherwise
builtin("error","FFE_DCTracking:InvalidAdaptionTechnique", ...
"Unsupported FFE adaption technique.");
end
vars = optimizableVariable("mu_tr",mu_range,"Transform","log");
if obj.dd_mode
vars = [vars, optimizableVariable("mu_dd",mu_range,"Transform","log")];
end
optimize_dc_tracking_mu = obj.dc_tracking_mu ~= 0 && ...
~obj.optimize_dc_tracking_params;
if optimize_dc_tracking_mu
vars = [vars, optimizableVariable("dc_tracking_mu",[1e-5,1e-1],"Transform","log")];
end
obj.mu_optimization_iter = 0;
fprintf("FFE_DCTracking mu opt uses %d samples / %d symbols\n",N_opt,numel(d_opt));
obj.mu_optimization = bayesopt(@(p)obj.muObjective(p,x_opt,d_opt),vars, ...
"MaxObjectiveEvaluations",20, ...
"AcquisitionFunctionName","expected-improvement-plus", ...
"IsObjectiveDeterministic",false, ...
"Verbose",0, ...
"PlotFcn",[]);
obj.mu_tr = obj.mu_optimization.XAtMinObjective.mu_tr;
if obj.dd_mode
obj.mu_dd = obj.mu_optimization.XAtMinObjective.mu_dd;
end
if optimize_dc_tracking_mu
obj.dc_tracking_mu = obj.mu_optimization.XAtMinObjective.dc_tracking_mu;
end
if obj.dd_mode && optimize_dc_tracking_mu
fprintf("\nFFE_DCTracking mu opt done: mu_tr=%9.3e, mu_dd=%9.3e, dc_tracking_mu=%9.3e, BER=%9.3e\n", ...
obj.mu_tr,obj.mu_dd,obj.dc_tracking_mu,obj.mu_optimization.MinObjective);
elseif obj.dd_mode
fprintf("\nFFE_DCTracking mu opt done: mu_tr=%9.3e, mu_dd=%9.3e, BER=%9.3e\n", ...
obj.mu_tr,obj.mu_dd,obj.mu_optimization.MinObjective);
elseif optimize_dc_tracking_mu
fprintf("\nFFE_DCTracking mu opt done: mu_tr=%9.3e, dc_tracking_mu=%9.3e, BER=%9.3e\n", ...
obj.mu_tr,obj.dc_tracking_mu,obj.mu_optimization.MinObjective);
else
fprintf("\nFFE_DCTracking mu opt done: mu_tr=%9.3e, BER=%9.3e\n", ...
obj.mu_tr,obj.mu_optimization.MinObjective);
end
if obj.plot_mu_optimization
obj.plotMuOptimization();
end
end
function optimizeDcTrackingParams(obj,x,d)
[x_opt,d_opt,N_opt] = obj.optimizationSignals( ...
x,d,obj.dc_tracking_optimization_len);
vars = optimizableVariable( ...
"dc_tracking_mu",[1e-5,1e-1],"Transform","log");
initial_values = min(max(obj.dc_tracking_mu,1e-5),1e-1);
initial_names = "dc_tracking_mu";
if obj.dc_tracking_adaptive_enabled
vars = [vars, ...
optimizableVariable("dc_tracking_persistence_gain",[0,2]), ...
optimizableVariable("dc_tracking_mu_eff_max",[1e-3,3e-1], ...
"Transform","log")];
initial_values = [initial_values, ...
min(max(obj.dc_tracking_persistence_gain,0),2), ...
min(max(obj.dc_tracking_mu_eff_max,1e-3),3e-1)];
initial_names = [initial_names, ...
"dc_tracking_persistence_gain", "dc_tracking_mu_eff_max"];
end
initial_x = array2table(initial_values, ...
"VariableNames",cellstr(initial_names));
obj.dc_tracking_optimization_iter = 0;
fprintf("FFE_DCTracking DC opt uses fixed mu_tr=%9.3e, mu_dd=%9.3e on %d samples / %d symbols\n", ...
obj.mu_tr,obj.mu_dd,N_opt,numel(d_opt));
old_rng = rng;
cleanup_rng = onCleanup(@()rng(old_rng));
rng(42,"twister");
obj.dc_tracking_optimization = bayesopt( ...
@(p)obj.dcTrackingObjective(p,x_opt,d_opt),vars, ...
"MaxObjectiveEvaluations",obj.dc_tracking_optimization_max_evals, ...
"InitialX",initial_x, ...
"AcquisitionFunctionName","expected-improvement-plus", ...
"IsObjectiveDeterministic",true, ...
"Verbose",0, ...
"PlotFcn",[]);
clear cleanup_rng
best = obj.dc_tracking_optimization.XAtMinObjective;
obj.dc_tracking_mu = best.dc_tracking_mu;
if obj.dc_tracking_adaptive_enabled
obj.dc_tracking_persistence_gain = ...
best.dc_tracking_persistence_gain;
obj.dc_tracking_mu_eff_max = best.dc_tracking_mu_eff_max;
fprintf("\nFFE_DCTracking DC opt done: dc_tracking_mu=%9.3e, persistence_gain=%6.3f, mu_eff_max=%9.3e, objective=%9.3e\n", ...
obj.dc_tracking_mu,obj.dc_tracking_persistence_gain, ...
obj.dc_tracking_mu_eff_max, ...
obj.dc_tracking_optimization.MinObjective);
else
fprintf("\nFFE_DCTracking DC opt done: dc_tracking_mu=%9.3e, objective=%9.3e\n", ...
obj.dc_tracking_mu,obj.dc_tracking_optimization.MinObjective);
end
end
function objective = muObjective(obj,params,x,d)
old_state = obj.captureTrackingObjectiveState();
cleanup = onCleanup(@()obj.restoreTrackingObjectiveState(old_state));
obj.save_debug = 0;
if any(strcmp(params.Properties.VariableNames,"dc_tracking_mu"))
obj.dc_tracking_mu = params.dc_tracking_mu;
end
obj.resetTrackingState();
N_tr = min(obj.len_tr,numel(x));
obj.equalize(x,d,params.mu_tr,obj.epochs_tr,N_tr,true,false);
if obj.dd_mode
[signal,~] = obj.equalize(x,d,params.mu_dd,obj.epochs_dd,numel(x),false,false);
else
[signal,~] = obj.equalize(x,d,0,1,numel(x),false,false);
end
[ber,errors] = obj.berObjective(signal,d);
objective = ber;
if ~isfinite(objective)
objective = inf;
end
obj.mu_optimization_iter = obj.mu_optimization_iter + 1;
has_dc_tracking_mu = any(strcmp(params.Properties.VariableNames,"dc_tracking_mu"));
if obj.dd_mode && has_dc_tracking_mu
fprintf("\rFFE_DCTracking mu opt %02d: mu_tr=%9.3e, mu_dd=%9.3e, dc_tracking_mu=%9.3e, BER=%9.3e, errors=%d", ...
obj.mu_optimization_iter,params.mu_tr,params.mu_dd,params.dc_tracking_mu,ber,errors);
elseif obj.dd_mode
fprintf("\rFFE_DCTracking mu opt %02d: mu_tr=%9.3e, mu_dd=%9.3e, BER=%9.3e, errors=%d", ...
obj.mu_optimization_iter,params.mu_tr,params.mu_dd,ber,errors);
elseif has_dc_tracking_mu
fprintf("\rFFE_DCTracking mu opt %02d: mu_tr=%9.3e, dc_tracking_mu=%9.3e, BER=%9.3e, errors=%d", ...
obj.mu_optimization_iter,params.mu_tr,params.dc_tracking_mu,ber,errors);
else
fprintf("\rFFE_DCTracking mu opt %02d: mu_tr=%9.3e, BER=%9.3e, errors=%d", ...
obj.mu_optimization_iter,params.mu_tr,ber,errors);
end
clear cleanup
end
function objective = dcTrackingObjective(obj,params,x,d)
old_state = obj.captureTrackingObjectiveState();
cleanup = onCleanup(@()obj.restoreTrackingObjectiveState(old_state));
obj.applyDcTrackingObjectiveParams(params);
obj.save_debug = 1;
obj.resetTrackingState();
N_tr = min(obj.len_tr,numel(x));
obj.equalize(x,d,obj.mu_tr,obj.epochs_tr,N_tr,true,false);
if obj.dd_mode
[signal,~] = obj.equalize( ...
x,d,obj.mu_dd,obj.epochs_dd,numel(x),false,false);
else
[signal,~] = obj.equalize(x,d,0,1,numel(x),false,false);
end
[ber,errors] = obj.berObjective(signal,d);
[delay_symbols,delay_corr] = obj.dcTrackingDelayObjective(signal,d);
delay_penalty = obj.dc_tracking_optimization_delay_weight * ...
abs(delay_symbols) / max(numel(d),1);
objective = ber + delay_penalty;
if ~isfinite(objective)
objective = inf;
end
obj.dc_tracking_optimization_iter = ...
obj.dc_tracking_optimization_iter + 1;
if obj.dc_tracking_adaptive_enabled
fprintf("\rFFE_DCTracking DC opt %02d: dc_tracking_mu=%9.3e, persistence_gain=%6.3f, mu_eff_max=%9.3e, BER=%9.3e, delay=%7.0f, corr=%6.3f, obj=%9.3e, errors=%d", ...
obj.dc_tracking_optimization_iter,params.dc_tracking_mu, ...
params.dc_tracking_persistence_gain, ...
params.dc_tracking_mu_eff_max,ber,delay_symbols,delay_corr, ...
objective,errors);
else
fprintf("\rFFE_DCTracking DC opt %02d: dc_tracking_mu=%9.3e, BER=%9.3e, delay=%7.0f, corr=%6.3f, obj=%9.3e, errors=%d", ...
obj.dc_tracking_optimization_iter,params.dc_tracking_mu, ...
ber,delay_symbols,delay_corr,objective,errors);
end
clear cleanup
end
function [y,d_hat] = equalize(obj,x,d,mu,epochs,N,training,showviz)
arguments
obj
x
d
mu
epochs
N
training
showviz
end
unused_showviz = showviz; %#ok<NASGU>
x = x(:);
d = d(:);
N = obj.validSampleLength(N,x,d);
n_symbols = N / obj.sps;
y = zeros(n_symbols,1);
d_hat = zeros(n_symbols,1);
if n_symbols == 0
return
end
if isempty(obj.constellation)
obj.constellation = unique(d);
end
decision_constellation = obj.constellation(:);
x = [zeros(floor(obj.order/2),1); x; zeros(obj.order,1)];
lambda = mu;
mask = ones(obj.order,1);
maincursor_pos = ceil(length(obj.e)/2);
adaption_code = obj.adaptionCode();
adaption_is_rls = adaption_code == 3;
if mu == 0 || (~obj.dd_mode && ~training)
epochs = 1;
end
dc_tracking_mu_eff_min = obj.dc_tracking_mu_eff_min;
dc_tracking_mu_eff_max = obj.dc_tracking_mu_eff_max;
dc_tracking_persistence_gain = 0;
if obj.dc_tracking_adaptive_enabled
dc_tracking_persistence_gain = obj.dc_tracking_persistence_gain;
end
dc_tracking_enabled = obj.dc_tracking_mu ~= 0;
if dc_tracking_enabled
obj.dc_tracking_mu = min(max(obj.dc_tracking_mu, ...
obj.dc_tracking_mu_min),obj.dc_tracking_mu_max);
end
dc_tracking_mu = obj.dc_tracking_mu;
dc_tracking_use_persistence = dc_tracking_persistence_gain > 0;
dc_tracking_base_mu_eff = min(max(dc_tracking_mu, ...
dc_tracking_mu_eff_min),dc_tracking_mu_eff_max);
dc_buffer_enabled = dc_tracking_enabled && obj.dc_tracking_buffer_len > 1;
if dc_buffer_enabled
dc_tracking_err_buffer = NaN(obj.dc_tracking_buffer_len,1);
dc_tracking_err_buffer_pos = 0;
dc_tracking_err_sum = 0;
dc_tracking_abs_err_sum = 0;
dc_tracking_valid_count = 0;
else
dc_tracking_err_buffer = [];
dc_tracking_err_buffer_pos = 0;
dc_tracking_err_sum = 0;
dc_tracking_abs_err_sum = 0;
dc_tracking_valid_count = 0;
end
debug_enabled = obj.save_debug;
if debug_enabled
obj.initializeTrackingDebug(n_symbols,training);
end
for epoch = 1:epochs
symbol = 0;
for sample = 1:obj.sps:N
symbol = symbol + 1;
grad = zeros(obj.order,1);
update = zeros(obj.order,1);
weight = 0;
dc_tracking_mu_eff = 0;
U = x(obj.order+sample-1:-1:sample);
y(symbol,1) = obj.e_dc + (obj.e.*mask).' * U;
if training
d_hat(symbol,1) = d(symbol);
else
[~,symbol_idx] = min(abs(y(symbol) - decision_constellation));
d_hat(symbol,1) = decision_constellation(symbol_idx);
end
err = d_hat(symbol) - y(symbol);
switch adaption_code
case 1
weight = mu / ((U.'*U) + eps);
grad = err * U;
update = grad * weight;
case 2
weight = mu;
grad = err * U;
update = grad * weight;
case 3
denom = lambda + U.' * obj.P * U;
k = (obj.P * U) / denom;
update = k * err;
end
obj.e = obj.e + update;
if adaption_is_rls
obj.P = (1/lambda) * (obj.P - k * (U.' * obj.P));
end
if dc_tracking_enabled
[obj.e_dc,dc_tracking_mu_eff,dc_tracking_err_buffer, ...
dc_tracking_err_buffer_pos,dc_tracking_err_sum, ...
dc_tracking_abs_err_sum,dc_tracking_valid_count] = ...
obj.updateDcTracking( ...
err,dc_tracking_mu,dc_tracking_base_mu_eff, ...
dc_tracking_persistence_gain,dc_tracking_use_persistence, ...
dc_buffer_enabled,dc_tracking_mu_eff_min,dc_tracking_mu_eff_max, ...
symbol,dc_tracking_err_buffer,dc_tracking_err_buffer_pos, ...
dc_tracking_err_sum,dc_tracking_abs_err_sum, ...
dc_tracking_valid_count);
end
if debug_enabled && epoch == epochs
error_power = err * err';
update_power = update.'*update ./ (sqrt((obj.e.'*obj.e) / obj.order) + eps);
obj.debug_struct.error(1,symbol) = error_power;
obj.debug_struct.main_cursor(1,symbol) = abs(obj.e(maincursor_pos));
obj.debug_struct.mu_nlms(1,symbol) = weight;
obj.debug_struct.update_gradient(1,symbol) = grad.'*grad;
obj.debug_struct.dc_tracking_mu_eff(1,symbol) = dc_tracking_mu_eff;
obj.debug_struct.dc_tracking_est(1,symbol) = obj.e_dc;
if training
obj.debug_struct.error_tr(1,symbol) = error_power;
obj.debug_struct.update_tr(1,symbol) = update_power;
else
obj.debug_struct.error_dd(1,symbol) = error_power;
obj.debug_struct.update(1,symbol) = update_power;
end
end
end
end
end
end
methods (Access = private)
function resetTrackingState(obj)
obj.e = zeros(obj.order,1);
obj.e_tr = zeros(obj.order,1);
obj.error = 0;
obj.e_dc = 0;
obj.P = (1/0.05) * eye(obj.order);
obj.debug_struct = struct();
end
function state = captureTrackingObjectiveState(obj)
state.e = obj.e;
state.e_tr = obj.e_tr;
state.error = obj.error;
state.e_dc = obj.e_dc;
state.P = obj.P;
state.save_debug = obj.save_debug;
state.debug_struct = obj.debug_struct;
state.dc_tracking_mu = obj.dc_tracking_mu;
state.dc_tracking_persistence_gain = ...
obj.dc_tracking_persistence_gain;
state.dc_tracking_mu_eff_max = obj.dc_tracking_mu_eff_max;
end
function restoreTrackingObjectiveState(obj,state)
obj.e = state.e;
obj.e_tr = state.e_tr;
obj.error = state.error;
obj.e_dc = state.e_dc;
obj.P = state.P;
obj.save_debug = state.save_debug;
obj.debug_struct = state.debug_struct;
obj.dc_tracking_mu = state.dc_tracking_mu;
obj.dc_tracking_persistence_gain = ...
state.dc_tracking_persistence_gain;
obj.dc_tracking_mu_eff_max = state.dc_tracking_mu_eff_max;
end
function applyDcTrackingObjectiveParams(obj,params)
var_names = string(params.Properties.VariableNames);
if any(var_names == "dc_tracking_mu")
obj.dc_tracking_mu = params.dc_tracking_mu;
end
if any(var_names == "dc_tracking_persistence_gain")
obj.dc_tracking_persistence_gain = ...
params.dc_tracking_persistence_gain;
end
if any(var_names == "dc_tracking_mu_eff_max")
obj.dc_tracking_mu_eff_max = params.dc_tracking_mu_eff_max;
end
end
function [delay_symbols,delay_corr] = ...
dcTrackingDelayObjective(obj,signal,d)
delay_symbols = 0;
delay_corr = 0;
if ~isfield(obj.debug_struct,"dc_tracking_est") || ...
isempty(obj.debug_struct.dc_tracking_est)
return
end
smooth_len = obj.dc_tracking_optimization_smoothing_len;
dc_tracking_est_s = movmean( ...
obj.debug_struct.dc_tracking_est(:),smooth_len,"omitnan");
avg_lvl_dc = obj.averageLevelTrace(signal,d,smooth_len);
xcorr_len = min(numel(dc_tracking_est_s),numel(avg_lvl_dc));
if xcorr_len < 2
return
end
inv_dc_xcorr = -dc_tracking_est_s(1:xcorr_len);
avg_lvl_xcorr = avg_lvl_dc(1:xcorr_len);
inv_dc_xcorr = fillmissing( ...
inv_dc_xcorr,"linear","EndValues","nearest");
avg_lvl_xcorr = fillmissing( ...
avg_lvl_xcorr,"linear","EndValues","nearest");
inv_dc_xcorr = inv_dc_xcorr - mean(inv_dc_xcorr,"omitnan");
avg_lvl_xcorr = avg_lvl_xcorr - mean(avg_lvl_xcorr,"omitnan");
if rms(inv_dc_xcorr) <= eps || rms(avg_lvl_xcorr) <= eps
return
end
[dc_level_xcorr,dc_level_lags] = xcorr( ...
inv_dc_xcorr,avg_lvl_xcorr,"coeff");
[delay_corr,delay_idx] = max(dc_level_xcorr);
delay_symbols = dc_level_lags(delay_idx);
if ~isfinite(delay_corr)
delay_corr = 0;
delay_symbols = 0;
end
end
function avg_lvl_dc = averageLevelTrace(obj,signal,d,smooth_len)
signal = signal(:);
d = d(:);
n_symbols = min(numel(signal),numel(d));
signal = signal(1:n_symbols);
d = d(1:n_symbols);
levels = unique(d);
avg_for_lvl = NaN(numel(levels),n_symbols);
for level_idx = 1:numel(levels)
level_mask = d == levels(level_idx);
level_samples = signal(level_mask);
if isempty(level_samples)
continue
end
smooth_window = min(smooth_len,numel(level_samples));
avg_for_lvl(level_idx,level_mask) = movmean( ...
level_samples,smooth_window,"omitnan","Endpoints","shrink");
avg_for_lvl(level_idx,:) = obj.interpolateMissingAverage( ...
avg_for_lvl(level_idx,:));
end
avg_lvl_dc = mean(avg_for_lvl,1,"omitnan").';
end
function level_average = interpolateMissingAverage(~,level_average)
valid_samples = isfinite(level_average);
if nnz(valid_samples) == 0
return
elseif nnz(valid_samples) == 1
level_average(:) = level_average(valid_samples);
return
end
t = 1:numel(level_average);
level_average(~valid_samples) = interp1( ...
t(valid_samples),level_average(valid_samples), ...
t(~valid_samples),"linear","extrap");
end
function N = validSampleLength(obj,N,x,d)
N_available = min(numel(x),numel(d) * obj.sps);
N = min(N,N_available);
N = obj.sps * floor(N / obj.sps);
N = max(0,N);
end
function adaption_code = adaptionCode(obj)
if obj.adaption_technique == adaption_method.nlms
adaption_code = 1;
elseif obj.adaption_technique == adaption_method.lms
adaption_code = 2;
elseif obj.adaption_technique == adaption_method.rls
adaption_code = 3;
else
builtin("error","FFE_DCTracking:InvalidAdaptionTechnique", ...
"Unsupported FFE adaption technique.");
end
end
function initializeTrackingDebug(obj,n_symbols,training)
obj.debug_struct.error = NaN(1,n_symbols);
obj.debug_struct.error_first_epoch = NaN(1,n_symbols);
obj.debug_struct.main_cursor = NaN(1,n_symbols);
obj.debug_struct.mu_nlms = NaN(1,n_symbols);
obj.debug_struct.update_gradient = NaN(1,n_symbols);
obj.debug_struct.dc_tracking_mu_eff = NaN(1,n_symbols);
obj.debug_struct.dc_tracking_est = NaN(1,n_symbols);
if training
obj.debug_struct.error_tr = NaN(1,n_symbols);
obj.debug_struct.update_tr = NaN(1,n_symbols);
else
obj.debug_struct.error_dd = NaN(1,n_symbols);
obj.debug_struct.update = NaN(1,n_symbols);
end
end
function [e_dc,mu_eff,err_buffer,err_buffer_pos,err_sum,abs_err_sum,valid_count] = ...
updateDcTracking(obj,err_current,mu_dc,base_mu_eff,persistence_gain, ...
use_persistence,buffer_enabled,mu_eff_min,mu_eff_max,symbol, ...
err_buffer,err_buffer_pos,err_sum,abs_err_sum,valid_count)
mu_eff = 0;
e_dc = obj.e_dc;
if buffer_enabled
err_buffer_pos = err_buffer_pos + 1;
if err_buffer_pos > obj.dc_tracking_buffer_len
err_buffer_pos = 1;
end
old_err = err_buffer(err_buffer_pos);
if isfinite(old_err)
err_sum = err_sum - old_err;
valid_count = valid_count - 1;
if use_persistence
abs_err_sum = abs_err_sum - abs(old_err);
end
end
if isfinite(err_current)
err_buffer(err_buffer_pos) = err_current;
err_sum = err_sum + err_current;
valid_count = valid_count + 1;
if use_persistence
abs_err_sum = abs_err_sum + abs(err_current);
end
else
err_buffer(err_buffer_pos) = NaN;
end
if mod(symbol,obj.dc_tracking_buffer_len) == 0
if valid_count == 0
err_mean = 0;
err_abs_mean = 0;
else
err_mean = err_sum / valid_count;
err_abs_mean = abs_err_sum / valid_count;
end
mu_eff = obj.effectiveDcMu(mu_dc,base_mu_eff,persistence_gain, ...
use_persistence,err_mean,err_abs_mean,mu_eff_min,mu_eff_max);
e_dc = e_dc + mu_eff * err_mean;
end
else
if isfinite(err_current)
err_mean = err_current;
err_abs_mean = abs(err_current);
else
err_mean = 0;
err_abs_mean = 0;
end
mu_eff = obj.effectiveDcMu(mu_dc,base_mu_eff,persistence_gain, ...
use_persistence,err_mean,err_abs_mean,mu_eff_min,mu_eff_max);
e_dc = e_dc + mu_eff * err_mean;
end
end
function mu_eff = effectiveDcMu(~,mu_dc,base_mu_eff,persistence_gain, ...
use_persistence,err_mean,err_abs_mean,mu_eff_min,mu_eff_max)
if use_persistence
persistence_scale = abs(err_mean) / (err_abs_mean + eps);
persistence_scale = min(max(persistence_scale,0),1);
mu_eff = mu_dc * (1 + persistence_gain * persistence_scale);
mu_eff = min(max(mu_eff,mu_eff_min),mu_eff_max);
else
mu_eff = base_mu_eff;
end
end
end
end

View File

@@ -0,0 +1,411 @@
classdef FFE_DCremoval_adaptive_mu < handle
% FFE variant for MPI/DC-removal experiments.
% With dc_buffer_len <= 1, ffe_buffer_len <= 1 and no smoothing, this
% follows FFE.m semantics so MPI-reduction changes can be isolated.
properties
sps
order
e
e_tr
error
len_tr
mu_tr
epochs_tr
adaption_technique
dd_mode
mu_dd
epochs_dd
dd_len_fraction
mu_dc
e_dc
P
dc_buffer_len
adaptive_mu_mode
ffe_buffer_len
smoothing_buffer_length
smoothing_buffer_update
constellation
decide
save_debug = 0;
debug_struct
optmize_mus = 0;
mu_optimization
mu_optimization_iter = 0;
end
methods
function obj = FFE_DCremoval_adaptive_mu(options)
arguments(Input)
options.sps = 2;
options.order = 15;
options.len_tr = 4096;
options.mu_tr = 0;
options.epochs_tr = 5;
options.adaption_technique adaption_method = adaption_method.lms;
options.dd_mode = 1;
options.mu_dd = 1e-5;
options.epochs_dd = 5;
options.dd_len_fraction = 0.25;
options.mu_dc = 0.05;
options.dc_buffer_len = 1;
options.adaptive_mu_mode = 1;
options.ffe_buffer_len = 1;
options.smoothing_buffer_length = 0;
options.smoothing_buffer_update = 0;
options.decide = false;
options.save_debug = 0;
options.optmize_mus = 0;
end
assert(options.dc_buffer_len >= 0);
assert(options.ffe_buffer_len >= 0);
assert(options.smoothing_buffer_length >= 0);
if options.smoothing_buffer_length > 0
assert(options.smoothing_buffer_update > 0);
end
fn = fieldnames(options);
for n = 1:numel(fn)
obj.(fn{n}) = options.(fn{n});
end
obj.e = zeros(obj.order,1);
obj.e_dc = 0;
obj.error = 0;
obj.dc_buffer_len = floor(obj.dc_buffer_len);
obj.ffe_buffer_len = floor(obj.ffe_buffer_len);
obj.smoothing_buffer_length = floor(obj.smoothing_buffer_length);
obj.smoothing_buffer_update = floor(obj.smoothing_buffer_update);
end
function [X,Noi] = process(obj, X, D)
X = X.normalize("mode","rms");
obj.constellation = unique(D.signal);
obj.e_dc = 0;
delta = 0.05;
obj.P = (1/delta) * eye(obj.order);
if obj.optmize_mus
obj.optimizeMus(X.signal,D.signal);
obj.e = zeros(obj.order,1);
obj.e_dc = 0;
obj.P = (1/delta) * eye(obj.order);
end
training = 1;
showviz = 0;
obj.equalize(X.signal,D.signal,obj.mu_tr,obj.epochs_tr,obj.len_tr,training,showviz);
obj.e_tr = obj.e;
n = X.length;
training = 0;
if obj.dd_mode
n_dd = obj.ddLength(n);
obj.equalize(X.signal,D.signal,obj.mu_dd,obj.epochs_dd,n_dd,training,showviz);
end
[signal,decision] = obj.applyCurrentTaps(X.signal,n);
if obj.decide
X.signal = decision;
else
X.signal = signal;
end
X.fs = D.fs;
lbdesc = [num2str(obj.order),' tap FFE'];
X = X.logbookentry(lbdesc);
Noi = X;
Noi = X - D;
end
function [y,d_hat] = equalize(obj,x,d,mu,epochs,N,training,showviz)
arguments
obj
x
d
mu
epochs
N
training
showviz = 0 %#ok<INUSD>
end
x = [zeros(floor(obj.order/2),1); x; zeros(obj.order,1)];
lambda = mu;
if training
mask = ones(obj.order,1);
else
mask = zeros(obj.order,1);
mask(900:end) = 1;
mask(ceil(length(obj.e)/2)) = 1;
end
mask = ones(obj.order,1);
always_ideal_decision = 0;
grad = 0;
weight = 0;
update = 0;
if mu == 0 || (~obj.dd_mode && ~training)
epochs = 1;
end
dc_buffer_enabled = obj.mu_dc ~= 0 && obj.dc_buffer_len > 1;
adaptive_dc_enabled = dc_buffer_enabled && obj.adaptive_mu_mode;
if dc_buffer_enabled
e_dc_buffer = NaN(obj.dc_buffer_len,1);
end
ffe_buffer_enabled = ~training && obj.ffe_buffer_len > 1 && ...
obj.adaption_technique ~= adaption_method.rls;
if ffe_buffer_enabled
grad_buffer = NaN(obj.order,obj.ffe_buffer_len);
end
if obj.smoothing_buffer_length > 0
smoothing_buffer = ones(1,obj.smoothing_buffer_length) .* mean(obj.constellation);
smoothing_mean = mean(obj.constellation);
end
P_err = 0;
alpha = 0.98;
err_prev = 0;
gamma_dc = 1e-6;
mu_min = 1e-6;
mu_max = 3e-1;
for epoch = 1 : epochs
symbol = 0;
for sample = 1 : obj.sps : N
symbol = symbol + 1;
if obj.smoothing_buffer_length > 0
smoothing_buffer = circshift(smoothing_buffer,1,2);
smoothing_buffer(1) = x(sample);
if mod(symbol,obj.smoothing_buffer_update) == 0
smoothing_mean = mean(smoothing_buffer);
end
x(sample:sample+obj.sps-1) = x(sample:sample+obj.sps-1) - smoothing_mean;
end
U = x(obj.order+sample-1:-1:sample);
y(symbol,1) = obj.e_dc + (obj.e.*mask).' * U;
if training
d_hat(symbol,1) = d(symbol);
else
if ~always_ideal_decision
[~,symbol_idx] = min(abs(y(symbol) - obj.constellation));
d_hat(symbol,1) = obj.constellation(symbol_idx);
else
d_hat(symbol,1) = d(symbol);
end
end
err(symbol) = d_hat(symbol) - y(symbol); %#ok<AGROW>
true_err(symbol) = y(symbol) - d(symbol); %#ok<AGROW,NASGU>
if training || obj.dd_mode
switch obj.adaption_technique
case adaption_method.lms
weight = mu;
grad = err(symbol) * U;
update = grad * weight;
case adaption_method.nlms
normU = (U.'*U) + eps;
weight = mu / normU;
grad = err(symbol) * U;
update = grad * weight;
case adaption_method.rls
denom = lambda + U.' * obj.P * U;
k = (obj.P * U) / denom;
update = k * err(symbol);
end
if ffe_buffer_enabled
grad_buffer = circshift(grad_buffer,1,2);
grad_buffer(:,1) = update;
if mod(symbol,obj.ffe_buffer_len) == 0
obj.e = obj.e + mean(grad_buffer,2,"omitnan");
end
else
obj.e = obj.e + update;
end
if obj.adaption_technique == adaption_method.rls
obj.P = (1/lambda) * (obj.P - k * (U.' * obj.P));
end
if obj.mu_dc ~= 0
if adaptive_dc_enabled
delta_mu = gamma_dc * err(symbol) * err_prev * (U.'*U);
obj.mu_dc = min(max(obj.mu_dc + delta_mu,mu_min),mu_max);
err_prev = err(symbol);
P_err = alpha*P_err + (1-alpha)*err(symbol)^2;
mu_dc_eff = obj.mu_dc / (P_err + eps);
else
mu_dc_eff = obj.mu_dc;
end
if dc_buffer_enabled
e_dc_buffer = circshift(e_dc_buffer,1);
e_dc_buffer(1) = obj.e_dc + mu_dc_eff * err(symbol);
if mod(symbol,obj.dc_buffer_len) == 0
obj.e_dc = median(e_dc_buffer,"omitnan");
end
else
obj.e_dc = obj.e_dc + mu_dc_eff * err(symbol);
end
end
end
if obj.save_debug
obj.debug_struct.error(epoch,symbol) = err(symbol) * err(symbol)';
if training
obj.debug_struct.error_tr(epoch,symbol) = err(symbol) * err(symbol)';
obj.debug_struct.update_tr(epoch,symbol) = update.'*update ./ rms(obj.e);
end
end
% obj.error(epoch,symbol) = err(symbol) * err(symbol)';
end
end
end
function [y,d_hat] = applyCurrentTaps(obj,x,N)
x = [zeros(floor(obj.order/2),1); x; zeros(obj.order,1)];
for sample = 1 : obj.sps : N
symbol = (sample - 1) / obj.sps + 1;
U = x(obj.order+sample-1:-1:sample);
y(symbol,1) = obj.e_dc + obj.e.' * U;
[~,symbol_idx] = min(abs(y(symbol) - obj.constellation));
d_hat(symbol,1) = obj.constellation(symbol_idx);
end
end
function N_dd = ddLength(obj,N)
if isempty(obj.dd_len_fraction) || obj.dd_len_fraction <= 0 || obj.dd_len_fraction >= 1
N_dd = N;
return
end
N_dd = floor(N * obj.dd_len_fraction);
N_dd = max(obj.sps,N_dd);
N_dd = min(N,N_dd);
end
function optimizeMus(obj,x,d)
switch obj.adaption_technique
case adaption_method.lms
mu_range = [1e-5, 1e-2];
case adaption_method.nlms
mu_range = [1e-3, 5e-1];
case adaption_method.rls
mu_range = [0.98, 0.99999];
end
mu_dc_range = [1e-5, 1e-1];
mu_tr_var = optimizableVariable("mu_tr",mu_range,"Transform","log");
vars = mu_tr_var;
if obj.dd_mode
vars = [vars, optimizableVariable("mu_dd",mu_range,"Transform","log")];
end
optimize_mu_dc = obj.mu_dc ~= 0;
if optimize_mu_dc
vars = [vars, optimizableVariable("mu_dc",mu_dc_range,"Transform","log")];
end
obj.mu_optimization_iter = 0;
obj.mu_optimization = bayesopt(@(p)obj.muObjective(p,x,d),vars, ...
"MaxObjectiveEvaluations",10, ...
"AcquisitionFunctionName","expected-improvement-plus", ...
"IsObjectiveDeterministic",false, ...
"Verbose",0, ...
"PlotFcn",[]);
obj.mu_tr = obj.mu_optimization.XAtMinObjective.mu_tr;
if obj.dd_mode
obj.mu_dd = obj.mu_optimization.XAtMinObjective.mu_dd;
end
if optimize_mu_dc
obj.mu_dc = obj.mu_optimization.XAtMinObjective.mu_dc;
end
objective_db = 10*log10(obj.mu_optimization.MinObjective);
if obj.dd_mode && optimize_mu_dc
fprintf("\nFFE_DCremoval_adaptive_mu opt done: mu_tr=%9.3e, mu_dd=%9.3e, mu_dc=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB\n", ...
obj.mu_tr,obj.mu_dd,obj.mu_dc,obj.mu_optimization.MinObjective,objective_db);
elseif obj.dd_mode
fprintf("\nFFE_DCremoval_adaptive_mu opt done: mu_tr=%9.3e, mu_dd=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB\n", ...
obj.mu_tr,obj.mu_dd,obj.mu_optimization.MinObjective,objective_db);
elseif optimize_mu_dc
fprintf("\nFFE_DCremoval_adaptive_mu opt done: mu_tr=%9.3e, mu_dc=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB\n", ...
obj.mu_tr,obj.mu_dc,obj.mu_optimization.MinObjective,objective_db);
else
fprintf("\nFFE_DCremoval_adaptive_mu opt done: mu_tr=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB\n", ...
obj.mu_tr,obj.mu_optimization.MinObjective,objective_db);
end
end
function objective = muObjective(obj,params,x,d)
old_debug = obj.save_debug;
old_mu_dc = obj.mu_dc;
obj.save_debug = 1;
if isprop(params,"mu_dc")
obj.mu_dc = params.mu_dc;
end
obj.e = zeros(obj.order,1);
obj.e_dc = 0;
obj.P = (1/0.05) * eye(obj.order);
obj.debug_struct = struct();
obj.equalize(x,d,params.mu_tr,obj.epochs_tr,obj.len_tr,1,0);
if obj.dd_mode
obj.equalize(x,d,params.mu_dd,obj.epochs_dd,obj.ddLength(numel(x)),0,0);
objective = mean(obj.debug_struct.error(end,:),"omitnan");
else
objective = mean(obj.debug_struct.error_tr(end,:),"omitnan");
end
if ~isfinite(objective)
objective = inf;
end
objective_db = 10*log10(objective);
obj.mu_optimization_iter = obj.mu_optimization_iter + 1;
optimize_mu_dc = isprop(params,"mu_dc");
if obj.dd_mode && optimize_mu_dc
fprintf("\rFFE_DCremoval_adaptive_mu opt %02d: mu_tr=%9.3e, mu_dd=%9.3e, mu_dc=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB", ...
obj.mu_optimization_iter,params.mu_tr,params.mu_dd,params.mu_dc,objective,objective_db);
elseif obj.dd_mode
fprintf("\rFFE_DCremoval_adaptive_mu opt %02d: mu_tr=%9.3e, mu_dd=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB", ...
obj.mu_optimization_iter,params.mu_tr,params.mu_dd,objective,objective_db);
elseif optimize_mu_dc
fprintf("\rFFE_DCremoval_adaptive_mu opt %02d: mu_tr=%9.3e, mu_dc=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB", ...
obj.mu_optimization_iter,params.mu_tr,params.mu_dc,objective,objective_db);
else
fprintf("\rFFE_DCremoval_adaptive_mu opt %02d: mu_tr=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB", ...
obj.mu_optimization_iter,params.mu_tr,objective,objective_db);
end
obj.save_debug = old_debug;
obj.mu_dc = old_mu_dc;
end
end
end

View File

@@ -10,6 +10,7 @@ classdef VNLE < handle
sps % usually 2
order
e
e_dc
error
len_tr
@@ -18,10 +19,17 @@ classdef VNLE < handle
mu_dd
epochs_dd
mu_dc
constellation
decide
save_debug = 0;
debug_struct
optmize_mus = 0;
mu_optimization
mu_optimization_iter = 0;
x_norm
ce
@@ -42,8 +50,11 @@ classdef VNLE < handle
options.mu_dd = 1e-5;
options.epochs_dd = 5;
options.mu_dc = 0;
options.decide = false;
options.save_debug = 0;
options.optmize_mus = 0;
end
@@ -54,6 +65,7 @@ classdef VNLE < handle
obj.error = 0;
obj.e_dc = 0;
end
@@ -69,6 +81,13 @@ classdef VNLE < handle
[obj.ie2,obj.ie3] = obj.calcIndiceVectors(obj.order);
obj.e = zeros( sum(obj.ce) ,1);
obj.e_dc = 0;
if obj.optmize_mus
obj.optimizeMus(X.signal,D.signal);
obj.e = zeros(sum(obj.ce),1);
obj.e_dc = 0;
end
% Training Mode
training = 1;
@@ -148,7 +167,7 @@ classdef VNLE < handle
x_in = x(obj.order(1)+sample-1:-1:sample);
x_in = obj.calcVNLENonlinVecs(x_in,obj.ie2,obj.ie3,obj.order,obj.x_norm);
y(symbol,1) = obj.e.' * x_in; % Calculating output of LMS __ * |
y(symbol,1) = obj.e_dc + obj.e.' * x_in; % Calculating output of LMS __ * |
if training
err = y(symbol) - d(symbol); % Instantaneous error
@@ -164,6 +183,9 @@ classdef VNLE < handle
normalizationfactor = (x_in.' * x_in);
obj.e = obj.e - err * x_in / normalizationfactor; % Weight update rule of NLMS
end
if obj.mu_dc ~= 0
obj.e_dc = obj.e_dc - obj.mu_dc * err;
end
if mod(sample,100) == 1 && showviz
a2.XData = 1:2*numel(y);
@@ -177,12 +199,85 @@ classdef VNLE < handle
end
obj.error(epoch,symbol) = err * err'; % Instantaneous square error
if obj.save_debug
obj.debug_struct.error(epoch,symbol) = err * err';
if training
obj.debug_struct.error_tr(epoch,symbol) = err * err';
end
end
end
end
end
function optimizeMus(obj,x,d)
mu_range = [1e-5, 1e-2];
mu_dc_range = [1e-5, 1e-1];
vars = [optimizableVariable("mu_tr",mu_range,"Transform","log"), ...
optimizableVariable("mu_dd",mu_range,"Transform","log")];
optimize_mu_dc = obj.mu_dc ~= 0;
if optimize_mu_dc
vars = [vars, optimizableVariable("mu_dc",mu_dc_range,"Transform","log")];
end
obj.mu_optimization_iter = 0;
obj.mu_optimization = bayesopt(@(p)obj.muObjective(p,x,d),vars, ...
"MaxObjectiveEvaluations",10, ...
"AcquisitionFunctionName","expected-improvement-plus", ...
"IsObjectiveDeterministic",false, ...
"Verbose",0, ...
"PlotFcn",[]);
obj.mu_tr = obj.mu_optimization.XAtMinObjective.mu_tr;
obj.mu_dd = obj.mu_optimization.XAtMinObjective.mu_dd;
if optimize_mu_dc
obj.mu_dc = obj.mu_optimization.XAtMinObjective.mu_dc;
end
objective_db = 10*log10(obj.mu_optimization.MinObjective);
if optimize_mu_dc
fprintf("\nVNLE mu opt done: mu_tr=%9.3e, mu_dd=%9.3e, mu_dc=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB\n", ...
obj.mu_tr,obj.mu_dd,obj.mu_dc,obj.mu_optimization.MinObjective,objective_db);
else
fprintf("\nVNLE mu opt done: mu_tr=%9.3e, mu_dd=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB\n", ...
obj.mu_tr,obj.mu_dd,obj.mu_optimization.MinObjective,objective_db);
end
end
function objective = muObjective(obj,params,x,d)
old_debug = obj.save_debug;
old_mu_dc = obj.mu_dc;
obj.save_debug = 1;
optimize_mu_dc = ismember("mu_dc",string(params.Properties.VariableNames));
if optimize_mu_dc
obj.mu_dc = params.mu_dc;
end
obj.e = zeros(sum(obj.ce),1);
obj.e_dc = 0;
obj.debug_struct = struct();
obj.equalize(x,d,params.mu_tr,obj.epochs_tr,obj.len_tr,1,0);
obj.equalize(x,d,params.mu_dd,obj.epochs_dd,numel(x),0,0);
objective = mean(obj.debug_struct.error(end,:),"omitnan");
if ~isfinite(objective)
objective = inf;
end
objective_db = 10*log10(objective);
obj.mu_optimization_iter = obj.mu_optimization_iter + 1;
if optimize_mu_dc
fprintf("\rVNLE mu opt %02d: mu_tr=%9.3e, mu_dd=%9.3e, mu_dc=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB", ...
obj.mu_optimization_iter,params.mu_tr,params.mu_dd,params.mu_dc,objective,objective_db);
else
fprintf("\rVNLE mu opt %02d: mu_tr=%9.3e, mu_dd=%9.3e, MSE=%9.3e, MSE_dB=%7.2f dB", ...
obj.mu_optimization_iter,params.mu_tr,params.mu_dd,objective,objective_db);
end
obj.save_debug = old_debug;
obj.mu_dc = old_mu_dc;
end
%% Functions needed During Adaption
function x_in_vnle_format = calcVNLENonlinVecs(~,x_in_block,I_2,I_3,N_,norm_)
% These are the second and third order input signal products of the VNLE EQ

View File

@@ -103,6 +103,7 @@ classdef MaxVar_Timing_Recovery < handle
% y = [y 0];
% end
data_out.signal = y.';
data_out.fs = obj.fsym;
end

View File

@@ -1,6 +1,7 @@
classdef Timing_Recovery < handle
properties(Access=public)
modulation
timing_error_detector
sps
damping_factor
@@ -12,6 +13,7 @@ classdef Timing_Recovery < handle
function obj = Timing_Recovery(options)
arguments(Input)
options.modulation = 'PAM/PSK/QAM'
options.timing_error_detector = 'Gardner';
options.sps = 2;
options.damping_factor = 1.0;
@@ -32,6 +34,7 @@ classdef Timing_Recovery < handle
function [data_out,timing_error] = process(obj, data_in)
timing_synchronization = comm.SymbolSynchronizer( ...
"Modulation", obj.modulation, ...
"TimingErrorDetector", obj.timing_error_detector, ...
"SamplesPerSymbol", obj.sps, ...
"DampingFactor", obj.damping_factor, ...

View File

@@ -21,14 +21,12 @@ classdef DBHandler < handle
% obj = DBHandler('pathToDB', 'path/to/database.db');
arguments
options.dataBase = ""; % Default value for pathToDB if not provided
options.dataBase = "labor_highspeed"; % Default value for pathToDB if not provided
options.type = "mysql";
options.server = "";
options.server = "192.168.178.192"; % university coffee PC: "134.245.243.254";
options.port = 3306;
options.user = "";
options.password = "";
options.user = "silas";
options.password = "silas";
end
% Assign values to class properties based on input arguments
@@ -49,15 +47,20 @@ classdef DBHandler < handle
elseif options.type == "mysql"
% datasource = "jdbc:mysql://134.245.243.254:3306/labor";
obj.conn = database( ...
if ismac
driverlocation = "/Users/silas/Documents/mysql-connector-j-9.6.0/mysql-connector-j-9.6.0.jar";
else
driverlocation = "C:\Users\Silas\Documents\mysql-connector-j-9.3.0\mysql-connector-j-9.3.0.jar";
end
obj.conn = database( ...
string(obj.dataBase), ... % Database name
options.user, ... % Username
options.password, ... % Password (or getSecret)
"Vendor", "MySQL", ...
"Server", options.server, ...
"PortNumber", 3306, ...
"JDBCDriverLocation", "C:\Users\Silas\Documents\mysql-connector-j-9.3.0\mysql-connector-j-9.3.0.jar");
"PortNumber", options.port, ...
"JDBCDriverLocation", driverlocation);
end

View File

@@ -0,0 +1,400 @@
SELECT COUNT(*)
FROM `Results`
SELECT COUNT(DISTINCT run_id) AS unique_run_count
FROM `Results`;
SELECT COUNT(*) AS entries_for_run
FROM `Results`
WHERE run_id = -- your desired run_id here
2937;
ALTER TABLE `Runs`
ADD COLUMN grossrate DOUBLE GENERATED ALWAYS AS (
(
CASE
WHEN pam_level = 4 THEN 2.0
WHEN pam_level = 6 THEN 2.5
WHEN pam_level = 8 THEN 3.0
ELSE FLOOR(LOG2(pam_level) * 10) / 10
END
) * symbolrate
) STORED;
SELECT
JSON_EXTRACT(voa_class, '$.atten_state[1]') AS atten_state_second
FROM Runs
WHERE run_id = 1000;
SELECT DISTINCT
run_id
FROM Results r
WHERE date_of_processing > '2025-11-14 12:00:00';
SELECT DISTINCT
symbolrate
FROM Runs
WHERE pam_level = 4
AND symbolrate IS NOT NULL
ORDER BY symbolrate;
SELECT DISTINCT
wavelength
FROM Runs
SELECT
run_id,
COUNT(DISTINCT eq_id) AS unique_eq_count,
COUNT(*) AS entries_for_run
FROM `Results`
WHERE run_id IS NOT NULL
GROUP BY run_id
ORDER BY run_id;
SELECT
COUNT(DISTINCT eq_id) AS unique_eq_count,
COUNT(*) AS entries_for_run
FROM `Results`
WHERE run_id = -- your run_id here
2936;
SELECT
run_id,
COUNT(DISTINCT eq_id) AS unique_eq_count
FROM `Results`
WHERE run_id IS NOT NULL
GROUP BY run_id
ORDER BY run_id;
SELECT DISTINCT
r.run_id,
r.eq_id,
e.equalizer_structure,
e.eq,
e.mlse
FROM Results AS r
JOIN Equalizer AS e USING (eq_id)
WHERE r.run_id = -- your run_id here
2731
ORDER BY r.run_id, r.eq_id
SELECT DISTINCT
r.run_id
FROM Results AS r
JOIN Runs AS u
ON r.run_id = u.run_id
WHERE u.pam_level = 6
ORDER BY r.run_id;
-- older than aug 2025
CREATE OR REPLACE VIEW dashboard_old AS
SELECT
r.run_id,
r.eq_id,
e.equalizer_structure,
-- extract the DIR field from the JSON in e.MLSE
MAX(JSON_UNQUOTE(JSON_EXTRACT(e.MLSE, '$.DIR'))) AS DIR,
u.symbolrate,
u.pam_level,
u.wavelength,
u.fiber_length,
u.db_mode,
-- accumulated dispersion in ps
0.09 * u.fiber_length * (u.wavelength - 1310) AS accumulated_dispersion,
-- BER & new averages
AVG(r.BER) AS avg_BER,
MAX(r.SNR) AS max_SNR,
MAX(r.GMI) AS max_GMI,
AVG(r.Alpha) AS avg_Alpha,
MIN(r.BER) AS min_BER,
AVG(r.BER_precoded) AS avg_BER_precoded,
MIN(r.BER_precoded) AS min_BER_precoded,
COUNT(*) AS num_occurrences
FROM Results AS r
JOIN Runs AS u ON r.run_id = u.run_id
JOIN Equalizer AS e ON r.eq_id = e.eq_id
WHERE r.eq_id IS NOT NULL
AND r.date_of_processing < '2025-08-01 00:00:00'
GROUP BY
r.run_id,
r.eq_id,
e.equalizer_structure,
u.symbolrate,
u.pam_level,
u.wavelength,
u.fiber_length,
u.db_mode
ORDER BY
r.run_id,
r.eq_id;
CREATE OR REPLACE VIEW dashboard_new AS
SELECT
r.run_id,
r.eq_id,
e.equalizer_structure,
-- extract the DIR field from the JSON in e.MLSE
MAX(JSON_UNQUOTE(JSON_EXTRACT(e.MLSE, '$.DIR'))) AS DIR,
u.symbolrate,
u.pam_level,
u.wavelength,
u.fiber_length,
u.db_mode,
-- accumulated dispersion in ps
0.09 * u.fiber_length * (u.wavelength - 1310) AS accumulated_dispersion,
-- BER & new averages
AVG(r.BER) AS avg_BER,
MAX(r.SNR) AS max_SNR,
MAX(r.GMI) AS max_GMI,
AVG(r.Alpha) AS avg_Alpha,
MIN(r.BER) AS min_BER,
AVG(r.BER_precoded) AS avg_BER_precoded,
MIN(r.BER_precoded) AS min_BER_precoded,
COUNT(*) AS num_occurrences
FROM Results AS r
JOIN Runs AS u ON r.run_id = u.run_id
JOIN Equalizer AS e ON r.eq_id = e.eq_id
WHERE r.eq_id IS NOT NULL
AND r.date_of_processing >= '2025-08-01 00:00:00'
GROUP BY
r.run_id,
r.eq_id,
e.equalizer_structure,
u.symbolrate,
u.pam_level,
u.wavelength,
u.fiber_length,
u.db_mode
ORDER BY
r.run_id,
r.eq_id;
CREATE OR REPLACE VIEW dashboard_ungrouped_alltime AS
SELECT
r.result_id,
r.run_id,
r.eq_id,
e.equalizer_structure,
u.bitrate,
u.grossrate,
u.symbolrate,
u.pam_level,
u.wavelength,
u.fiber_length,
u.db_mode,
u.rop_attenuation,
-- accumulated dispersion in ps
0.07 * u.fiber_length * (u.wavelength - 1310) AS accumulated_dispersion,
r.date_of_processing,
r.numBits,
r.numBitErr,
r.BER,
r.numBitErr_precoded,
r.BER_precoded,
r.STD,
r.STDrx,
r.GMI,
r.AIR,
r.EVM,
r.Alpha
FROM Results AS r
JOIN Runs AS u ON r.run_id = u.run_id
JOIN Equalizer AS e ON r.eq_id = e.eq_id
WHERE r.eq_id IS NOT NULL;
CREATE OR REPLACE VIEW dashboard_ungrouped_aug_nov_2025 AS
SELECT
r.result_id,
r.run_id,
r.eq_id,
e.equalizer_structure,
u.bitrate,
u.grossrate,
u.symbolrate,
u.pam_level,
u.wavelength,
u.fiber_length,
u.db_mode,
u.rop_attenuation,
-- accumulated dispersion in ps
0.07 * u.fiber_length * (u.wavelength - 1310) AS accumulated_dispersion,
r.date_of_processing,
r.numBits,
r.numBitErr,
r.BER,
r.numBitErr_precoded,
r.BER_precoded,
r.STD,
r.STDrx,
r.GMI,
r.AIR,
r.EVM,
r.Alpha
FROM Results AS r
JOIN Runs AS u ON r.run_id = u.run_id
JOIN Equalizer AS e ON r.eq_id = e.eq_id
WHERE r.eq_id IS NOT NULL
AND r.date_of_processing >= '2025-08-01 00:00:00'
AND r.date_of_processing < '2025-11-14 00:00:00';
-- Das waren meine ehemals besten BERs, im Nov habe ich ML-based hinzugefügt und
-- das Rx Filter nochmal sehr eng gestellt, das war inbesondere bei geringeren Raten sehr hilfreich um
-- out.of-band noise zu filtern -> BER ging ja teilweise runter und wieder hoch...
CREATE OR REPLACE VIEW dashboard_ungrouped_aug_nov_2025 AS
SELECT
r.result_id,
r.run_id,
r.eq_id,
e.equalizer_structure,
u.bitrate,
u.grossrate,
u.symbolrate,
u.pam_level,
u.wavelength,
u.fiber_length,
u.db_mode,
u.rop_attenuation,
-- accumulated dispersion in ps
0.07 * u.fiber_length * (u.wavelength - 1310) AS accumulated_dispersion,
r.date_of_processing,
r.numBits,
r.numBitErr,
r.BER,
r.numBitErr_precoded,
r.BER_precoded,
r.STD,
r.STDrx,
r.GMI,
r.AIR,
r.EVM
FROM Results AS r
JOIN Runs AS u ON r.run_id = u.run_id
JOIN Equalizer AS e ON r.eq_id = e.eq_id
WHERE r.eq_id IS NOT NULL
AND r.date_of_processing >= '2025-08-01 00:00:00'
AND r.date_of_processing < '2025-11-14 00:00:00';
CREATE OR REPLACE VIEW dashboard_ungrouped_old AS
SELECT
r.result_id,
r.run_id,
r.eq_id,
e.equalizer_structure,
u.symbolrate,
u.pam_level,
u.wavelength,
u.fiber_length,
u.db_mode,
-- accumulated dispersion in ps
0.07 * u.fiber_length * (u.wavelength - 1310) AS accumulated_dispersion,
r.date_of_processing,
r.numBits,
r.numBitErr,
r.BER,
r.numBitErr_precoded,
r.BER_precoded,
r.STD,
r.STDrx,
r.GMI,
r.AIR,
r.EVM
FROM Results AS r
JOIN Runs AS u ON r.run_id = u.run_id
JOIN Equalizer AS e ON r.eq_id = e.eq_id
WHERE r.eq_id IS NOT NULL
AND r.date_of_processing < '2025-08-01 00:00:00';
CREATE OR REPLACE VIEW dashboard_ungrouped_during_ecoc AS
SELECT
r.result_id,
r.run_id,
r.eq_id,
e.equalizer_structure,
u.symbolrate,
u.pam_level,
u.wavelength,
u.fiber_length,
u.db_mode,
-- accumulated dispersion in ps
0.07 * u.fiber_length * (u.wavelength - 1310) AS accumulated_dispersion,
r.date_of_processing,
r.numBits,
r.numBitErr,
r.BER,
r.numBitErr_precoded,
r.BER_precoded,
r.STD,
r.STDrx,
r.GMI,
r.AIR,
r.EVM
FROM Results AS r
JOIN Runs AS u ON r.run_id = u.run_id
JOIN Equalizer AS e ON r.eq_id = e.eq_id
WHERE r.eq_id IS NOT NULL
AND r.date_of_processing > '2025-09-20 00:00:00';
SELECT COUNT(*)
FROM `dashboard_ungrouped`
CREATE OR REPLACE VIEW power_state_info AS
SELECT
run_id,
power_laser AS power_laser,
power_rop AS power_mzm,
-- new power_rop first
power_pd_in
+ CAST(voa_class->>'$.atten_state[1]' AS DECIMAL(12,6))
AS power_rop,
-- then voa_atten
CAST(voa_class->>'$.atten_state[1]' AS DECIMAL(12,6))
AS voa_atten,
-- then the photodiode input power
power_pd_in AS power_pd_in,
-- appended columns
wavelength,
pam_level,
db_mode,
is_mpi,
fiber_length
FROM Runs;

View File

@@ -95,14 +95,14 @@ classdef DataStorage < handle
end
function addStorage(obj,varName)
% add a storage
storage = cell(obj.dim);
obj.sto.(string(varName)) = storage;
end
function addStorage(obj,varName)
% add a storage
storage = cell(obj.getStorageSize());
obj.sto.(string(varName)) = storage;
end
function addValueToStorage(obj, valueToStore ,storageVarName, varargin)
@@ -273,14 +273,18 @@ classdef DataStorage < handle
end
%append to index list :-)
fn_=fieldnames(obj.sto);
n_ = fn_{1};
lin_idx(c,:) = sub2ind(size(obj.sto.(n_)),indices{:});
% lin_idx(c,:) = eval(['sub2ind(size(obj.sto.',n_,')',str,');']);
end
if isscalar(indices)
lin_idx(c,:) = indices{1};
else
fn_=fieldnames(obj.sto);
n_ = fn_{1};
lin_idx(c,:) = sub2ind(size(obj.sto.(n_)),indices{:});
% lin_idx(c,:) = eval(['sub2ind(size(obj.sto.',n_,')',str,');']);
end
end
end
% Mapping for single Index
@@ -291,24 +295,33 @@ classdef DataStorage < handle
function [phys_indices,param_name] = getPhysIndicesByLinIndex(obj, lin_idx)
% Converts a linear index into the corresponding physical parameter values
% Inputs:
% - lin_idx: The linear index within the storage array
function [phys_indices,param_name] = getPhysIndicesByLinIndex(obj, lin_idx)
% Converts a linear index into the corresponding physical parameter values
% Inputs:
% - lin_idx: The linear index within the storage array
% Output:
% - phys_indices: A cell array containing the physical parameter values for each dimension
% Initialize output cell array
phys_indices = cell(1, numel(obj.fn));
% Convert linear index to subscript indices
[subscripts{1:numel(obj.dim)}] = ind2sub(obj.dim, lin_idx);
% Map subscripts to physical values for each parameter
for i = 1:numel(obj.fn)
param_name{i} = obj.fn(i);
phys_indices{i} = obj.parameter.(param_name{i}).getPhysForIndex(subscripts{i});
end
% Initialize output cell array
phys_indices = cell(1, numel(obj.fn));
param_name = cell(1, numel(obj.fn));
if isempty(obj.fn)
return
end
% Convert linear index to subscript indices
if isscalar(obj.dim)
subscripts = {lin_idx};
else
[subscripts{1:numel(obj.dim)}] = ind2sub(obj.dim, lin_idx);
end
% Map subscripts to physical values for each parameter
for i = 1:numel(obj.fn)
param_name{i} = obj.fn(i);
phys_indices{i} = obj.parameter.(param_name{i}).getPhysForIndex(subscripts{i});
end
end
function [physStruct, stored_value] = getPhysAndValueByLinIndex(obj, storageVarName, lin_idx)
@@ -327,8 +340,14 @@ classdef DataStorage < handle
% Initialize an empty structure
physStruct = struct();
% Convert linear index to subscript indices
[subscripts{1:numel(obj.dim)}] = ind2sub(obj.dim, lin_idx);
% Convert linear index to subscript indices
if isempty(obj.fn)
subscripts = {};
elseif isscalar(obj.dim)
subscripts = {lin_idx};
else
[subscripts{1:numel(obj.dim)}] = ind2sub(obj.dim, lin_idx);
end
% Map subscripts to physical values and parameter names for each dimension
for i = 1:numel(obj.fn)
@@ -343,17 +362,31 @@ classdef DataStorage < handle
stored_value = obj.sto.(storageVarName){lin_idx};
end
function num_elements = getLastLinIndice(obj)
% Returns all possible linear indices for the data structure
% Output:
% - lin_indices: A column vector containing all linear indices for the storage array
% Calculate the total number of elements in the storage array
num_elements = prod(obj.dim);
end
end
function num_elements = getLastLinIndice(obj)
% Returns all possible linear indices for the data structure
% Output:
% - lin_indices: A column vector containing all linear indices for the storage array
% Calculate the total number of elements in the storage array
if isempty(obj.dim)
num_elements = 1;
else
num_elements = prod(obj.dim);
end
end
function storageSize = getStorageSize(obj)
if isempty(obj.dim)
storageSize = [1, 1];
elseif isscalar(obj.dim)
storageSize = [obj.dim, 1];
else
storageSize = obj.dim;
end
end
end
end

View File

@@ -0,0 +1,9 @@
classdef channel_model < int32
enumeration
awgn (1)
awgn_alphad (2)
physical (3)
end
end

8
Datatypes/db_decoder.m Normal file
View File

@@ -0,0 +1,8 @@
classdef db_decoder < int32
enumeration
sequencedetection (0) % use MLSE for decoding
memoryless (1) % use modulo
end
end

View File

@@ -5,6 +5,8 @@ classdef signalform < int32
sawtooth (2)
square (3)
noise (4)
random (5)
prms (6)
end
end
end

Binary file not shown.

View File

@@ -0,0 +1,32 @@
function signal_out = awgn_alpha_d_channel(signal_in, options)
%AWGN_ALPHA_D_CHANNEL Apply a 1 + alpha*D FIR channel followed by AWGN.
arguments
signal_in
options.alpha (1,1) double = 0
options.delay_samples (1,1) double {mustBeInteger, mustBePositive} = 1
options.snr_dB (1,1) double = 20
options.randkey = []
end
signal_out = signal_in;
taps = zeros(1, options.delay_samples + 1);
taps(1) = 1;
taps(end) = options.alpha;
signal_out.signal = filter(taps, 1, signal_in.signal, [], 1);
if isa(signal_out, 'Signal')
desc = sprintf('1 + alpha*D channel with alpha %.4f and delay %d samples', ...
options.alpha, options.delay_samples);
meta = struct( ...
'alpha', options.alpha, ...
'delay_samples', options.delay_samples);
signal_out = signal_out.logbookentry(desc, meta);
end
signal_out = awgn_channel(signal_out, ...
"snr_dB", options.snr_dB, ...
"randkey", options.randkey);
end

View File

@@ -0,0 +1,43 @@
function signal_out = awgn_channel(signal_in, options)
%AWGN_CHANNEL Apply additive white Gaussian noise to a signal object.
arguments
signal_in
options.snr_dB (1,1) double = 20
options.randkey = []
end
signal_out = signal_in;
x = signal_in.signal;
signalPower = mean(abs(x).^2, 'all'); %or use signal_in.power
if signalPower == 0
return
end
snrLinear = 10^(options.snr_dB/10);
noisePower = signalPower / snrLinear;
if isempty(options.randkey)
if isreal(x)
noise = sqrt(noisePower) * randn(size(x));
else
noise = sqrt(noisePower / 2) * (randn(size(x)) + 1i * randn(size(x)));
end
else
rs = RandStream('mt19937ar', 'Seed', options.randkey);
if isreal(x)
noise = sqrt(noisePower) * randn(rs, size(x));
else
noise = sqrt(noisePower / 2) * (randn(rs, size(x)) + 1i * randn(rs, size(x)));
end
end
signal_out.signal = x + noise;
if isa(signal_out, 'Signal')
desc = sprintf('AWGN channel with SNR %.2f dB', options.snr_dB);
meta = struct('snr_dB', options.snr_dB, 'randkey', options.randkey);
signal_out = signal_out.logbookentry(desc, meta);
end
end

View File

@@ -0,0 +1,189 @@
function [db_results] = duobinary_target(eq_, mlse_,M, rx_signal, tx_symbols, tx_bits, options)
arguments
eq_
mlse_
M
rx_signal
tx_symbols
tx_bits
options.precode_mode db_mode
options.showAnalysis = 0;
options.eth_style_symbol_mapping = 0;
options.postFFE = [];
options.decoding_mode db_decoder = db_decoder.sequencedetection;
end
%Duobinary Targeting
db_ref_sequence = Duobinary().encode(tx_symbols);
db_ref_constellation = unique(db_ref_sequence.signal);
[eq_signal, eq_noise] = eq_.process(rx_signal,db_ref_sequence);
if ~isempty(options.postFFE)
[eq_signal,eq_noise] = options.postFFE.process(eq_signal,db_ref_sequence);
end
%
switch options.decoding_mode
case db_decoder.sequencedetection %MLSE
mlse_.DIR = [1,1];
if isa(mlse_,'MLSE_viterbi')
mlse_sig_sd = mlse_.process(eq_signal);
else
[mlse_sig_sd,LLR,GMI_MLSE] = mlse_.process(eq_signal,tx_symbols);
end
pam_sig_hd = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).quantize(mlse_sig_sd);
case db_decoder.memoryless %DB Target FFE
% Hard decision on FFE output
eq_signal_hd = PAMmapper(M, 0).quantize(eq_signal,'custom_const',db_ref_constellation.');
eq_signal_hd = Duobinary().decode(eq_signal_hd);
pam_sig_hd = eq_signal_hd;
% tx_symbols = Duobinary().encode(tx_symbols);
% tx_symbols = Duobinary().decode(tx_symbols.*db_const_scale_factor);
end
% precoding to mitigate error propagation, most prominently used in
% combination with duobinary signaling to avoid catastrophic error
% behavior (see J.W.M. Bergmans, Digital Baseband Transmission and Recording -> partial response signaling)
switch options.precode_mode
case db_mode.no_db
% TX Data is not precoded:
% A) Emulate diff precoding
switch options.decoding_mode
case db_decoder.sequencedetection %MLSE
mlse_sig_hd_precoded = Duobinary().encode(pam_sig_hd);
mlse_sig_hd_precoded = Duobinary().decode(mlse_sig_hd_precoded);
case db_decoder.memoryless %DB Target FFE
% mlse_sig_hd_precoded = Duobinary().encode(pam_sig_hd);
% mlse_sig_hd_precoded = Duobinary().decode(mlse_sig_hd_precoded);
mlse_sig_hd_precoded = pam_sig_hd;
end
tx_symbols_precoded = Duobinary().encode(tx_symbols);
tx_symbols_precoded = Duobinary().decode(tx_symbols_precoded);
tx_bits_precoded = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(tx_symbols_precoded);
rx_bits_mlse = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(mlse_sig_hd_precoded);
[~,errors_db_diff_precoded,ber_db_diff_precoded,~] = calc_ber(rx_bits_mlse.signal,tx_bits_precoded.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1);
%B) Just determine BER
rx_bits_mlse = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(pam_sig_hd);
tx_bits = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(tx_symbols);
[bits_mlse,errors_mlse,ber_db,~] = calc_ber(rx_bits_mlse.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1);
case db_mode.db_precoded
% Daten SIND TATSÄCHLICH precoded auf TX Seite:
% A) Decode at Rx if no DB targeting was applied (we are in VNLE or MLSE EQ structure here!
switch options.decoding_mode
case db_decoder.sequencedetection %MLSE
mlse_sig_hd_decoded = Duobinary().encode(pam_sig_hd,"M",M);
mlse_sig_hd_decoded = Duobinary().decode(mlse_sig_hd_decoded,"M",M);
case db_decoder.memoryless %DB Target FFE
mlse_sig_hd_decoded = pam_sig_hd;
end
tx_symbols_precoded = Duobinary().encode(tx_symbols,"M",M);
tx_symbols_precoded = Duobinary().decode(tx_symbols_precoded,"M",M);
tx_bits_precoded = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(tx_symbols_precoded);
rx_bits_mlse_decoded = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(mlse_sig_hd_decoded);
[~,errors_db_diff_precoded,ber_db_diff_precoded,a] = calc_ber(rx_bits_mlse_decoded.signal,tx_bits_precoded.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1);
burst_db_precoded = count_error_bursts(a, 40);
% B) Omit the Coding by comparing with demapped TX symbol sequence
tx_bits = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(tx_symbols);
rx_bits_mlse = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(pam_sig_hd);
[bits_db,errors_db,ber_db,a] = calc_ber(rx_bits_mlse.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1);
burst_db = count_error_bursts(a, 40);
cols = linspecer(8);
figure();hold on;
stem(1:40,burst_db,'LineWidth',1,'Color',cols(4,:),'Marker','_','DisplayName','w/o diff. precoder');
stem(1:40,burst_db_precoded,'LineWidth',1,'Color',cols(3,:),'Marker','.','LineStyle','-','DisplayName','w diff. precoder');
xlabel('Bit Error Burst Length')
ylabel('Occurence')
set(gca, 'yscale', 'log');
end
% M = numel(unique(tx_symbols.signal));
rx_bits = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(pam_sig_hd);
[bits_db,errors_db,ber_db,errorIndice_db] = calc_ber(rx_bits.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1);
alpha = arburg(eq_noise.signal,1);%pf_.coefficients(2);
alpha = alpha(2);
switch options.decoding_mode
case db_decoder.sequencedetection %MLSE
if isa(mlse_,'MLSE_viterbi')
gmi_mlse = NaN;
air_mlse = NaN;
else
gmi_mlse = GMI_MLSE;
air_mlse = tx_symbols.fs .* floor(log2(double(M))*10)/10 .* gmi_mlse ./ log2(double(M));
end
case db_decoder.memoryless %DB Target FFE
% [gmi] = calc_air(eq_signal_sd, tx_symbols, "skip_front", 10000, "skip_end", 10000);
[gmi] = calc_ngmi(eq_signal,tx_symbols);
gmi_mlse = max(gmi,0);
air_mlse = tx_symbols.fs .* floor(log2(double(M))*10)/10 .* gmi ./ log2(double(M));
end
db_results = struct();
db_results.metrics = Metricstruct;
db_results.metrics.result_id = NaN;
db_results.metrics.run_id = NaN;
db_results.metrics.eqParam_id = NaN;
db_results.metrics.date_of_processing = datetime('now');
db_results.metrics.BER = ber_db;
db_results.metrics.numBits = bits_db;
db_results.metrics.numBitErr = errors_db;
db_results.metrics.BER_precoded = ber_db_diff_precoded;
db_results.metrics.numBitErr_precoded = errors_db_diff_precoded;
db_results.metrics.GMI = gmi_mlse;
db_results.metrics.AIR = air_mlse;
db_results.metrics.MLSE_dir = mlse_.DIR;
db_results.metrics.Alpha = alpha;
% Create DB results structure
db_results.config = Equalizerstruct();
eq_.e = [];
eq_.e2 = [];
eq_.e3 = [];
db_results.config.eq = jsonencode(eq_);
% mlse_.DIR = [];
db_results.config.mlse = jsonencode(mlse_);
db_results.config.equalizer_structure = int32(equalizer_structure.vnle_db_mlse);
db_results.config.comment = 'function: Duobinary tgt. (VNLE -> MLSE)';
if options.showAnalysis
eq_signal.eye(eq_signal.fs,M,"fignum",249);
eq_noise = eq_noise - mean(eq_noise.signal);
rx_signal.spectrum("normalizeTo0dB",1,"fignum",250,"displayname","Rx Spectrum");
Duobinary().encode(tx_symbols).spectrum("normalizeTo0dB",1,"fignum",250,"displayname","DB encoded reference");
showEQNoisePSD(eq_noise,"fignum",250,"displayname",'Duobinary Target Noise after Equalization');
fprintf('DB tgt BER: %.2e \n',ber_db);
figure(341); clf;
tx_symbols_uncoded = Duobinary().decode(db_ref_sequence);
showLevelHistogram(eq_signal, db_ref_sequence, "fignum", 341,"ref_symbol_uncoded",tx_symbols_uncoded);
end
end

View File

@@ -1,4 +1,4 @@
function [ffe_results] = ffe(eq_, M, rx_signal, tx_symbols, tx_bits, options)
function [ffe_results,eq_signal_sd] = ffe(eq_, M, rx_signal, tx_symbols, tx_bits, options)
% FFE Processes signals through FFE equalizer
%
% Inputs:
@@ -74,13 +74,22 @@ end
% Create FFE results structure
ffe_results = struct();
try
eq_.e = [];
eq_.e2 = [];
eq_.e3 = [];
eq_.b = [];
eq_.b2 = [];
eq_.b3 = [];
config_clear_props = { ...
"e", ...
"e2", ...
"e3", ...
"b", ...
"b2", ...
"b3", ...
"mu_optimization", ...
"dc_optimization", ...
"dc_tracking_optimization", ...
"a2_level_weight_optimization"};
for config_clear_idx = 1:numel(config_clear_props)
prop_name = config_clear_props{config_clear_idx};
if isprop(eq_,prop_name)
eq_.(prop_name) = [];
end
end
ffe_results.config = Equalizerstruct();
@@ -168,6 +177,8 @@ function displayAnalysis(eq_noise, eq_signal_sd, rx_signal, eq_, tx_symbols, M,
showEQNoisePSD(eq_noise, "fignum", 103, "displayname", 'Residual Noise after FFE');
% showEQcoefficients('n1', eq_.e, "displayname", 'Coefficients', 'fignum', 104);
% Figure 2: Post-FFE coefficients (if available)
if ~isempty(postFFE)
showEQcoefficients('n1', postFFE.e, "displayname", 'Coefficients', 'fignum', 104);
@@ -176,10 +187,11 @@ function displayAnalysis(eq_noise, eq_signal_sd, rx_signal, eq_, tx_symbols, M,
try
figure(339);hold on
showEQfilter(eq_.e, eq_signal_sd.fs.*2,"displayname",'training','fignum',339);
% showEQfilter(eq_.e, eq_signal_sd.fs.*2,"displayname",'dec. directed','fignum',339);
legend on
end
show2Dconstellation(eq_signal_sd, tx_symbols,"displayname",'Visualization of symbol correlation','fignum',340);
% try
% figure(240); hold on; plot(pow2db(movmean(eq_.debug_struct.error_tr',100)));ylim([-30,3]);title('error training');
%
@@ -188,6 +200,6 @@ function displayAnalysis(eq_noise, eq_signal_sd, rx_signal, eq_, tx_symbols, M,
% figure(242); hold on; plot(pow2db(movmean(eq_.debug_struct.update',1000)));title('update step dd');
% end
eq_signal_sd.eye(eq_signal_sd.fs,M,"displayname",'Eye','fignum',105);
% eq_signal_sd.eye(eq_signal_sd.fs,M,"displayname",'Eye','fignum',105);
end
end

View File

@@ -0,0 +1,211 @@
function [ffe_results] = ffe_db(eq_, M, rx_signal, tx_symbols, tx_bits, options)
% FFE Processes signals through FFE equalizer
%
% Inputs:
% eq_ - Equalizer object
% M - Modulation order
% rx_signal - Received signal
% tx_symbols - Transmitted symbols
% tx_bits - Transmitted bits
% options - Optional parameters
%
% Outputs:
% ffe_results - Results from FFE processing
arguments
eq_
M
rx_signal
tx_symbols
tx_bits
options.db_target = 0;
options.precode_mode db_mode
options.showAnalysis = 0;
options.eth_style_symbol_mapping = 0;
options.postFFE = [];
options.database = [];
end
%% Process signals through equalizer
% FFE or VNLE
if options.db_target
tx_symbols_ref = Duobinary().encode(tx_symbols);
db_ref_constellation = unique(tx_symbols_ref.signal);
[eq_signal_sd, eq_noise] = eq_.process(rx_signal, tx_symbols_ref);
else
[eq_signal_sd, eq_noise] = eq_.process(rx_signal, tx_symbols);
end
% Apply post-FFE if provided
if ~isempty(options.postFFE)
tic
[eq_signal_sd, eq_noise] = options.postFFE.process(eq_signal_sd, tx_symbols);
toc
end
try
ch_coefficients = arburg(eq_noise.signal,1);
channel_alpha = ch_coefficients(2);
end
% Hard decision on FFE output
if options.db_target
eq_signal_hd = PAMmapper(M, 0).quantize(eq_signal_sd,'custom_const',db_ref_constellation.');
else
eq_signal_hd = PAMmapper(M, 0).quantize(eq_signal_sd);
end
if options.db_target
eq_signal_hd = Duobinary().decode(eq_signal_hd);
tx_symbols = Duobinary().encode(tx_symbols);
tx_symbols = Duobinary().decode(tx_symbols);
end
%% Calculate BER based on precoding mode
[bits, errors, ber, error_pos, errors_precoded, ber_precoded] = calculateBER(eq_signal_hd, tx_symbols, tx_bits, options.precode_mode, M, options.eth_style_symbol_mapping);
%% Calculate performance metrics
[snr, snr_lvl] = calc_snr(tx_symbols.signal, eq_noise.signal);
% [gmi] = calc_air(eq_signal_sd, tx_symbols, "skip_front", 10000, "skip_end", 10000);
[gmi] = calc_ngmi(eq_signal_sd,tx_symbols);
gmi = max(gmi,0);
air = tx_symbols.fs .* floor(log2(double(M))*10)/10 .* gmi ./ log2(double(M));
[evm_total, evm_lvl] = calc_evm(eq_signal_sd, tx_symbols);
[std_total, std_lvl] = calc_std(eq_signal_sd, tx_symbols);
[std_rxraw_total, std_rxraw_lvl] = calc_std(rx_signal.resample("fs_out", tx_symbols.fs), tx_symbols);
%% Display analysis if requested
if options.showAnalysis
displayAnalysis(eq_noise, eq_signal_sd, rx_signal, eq_, tx_symbols, M, options.postFFE);
end
%% Prepare output structure
% Determine postFFE order
if ~isempty(options.postFFE)
npostFFE = options.postFFE.order;
else
npostFFE = 0;
end
% Create FFE results structure
ffe_results = struct();
try
eq_.e = [];
eq_.e2 = [];
eq_.e3 = [];
eq_.b = [];
eq_.b2 = [];
eq_.b3 = [];
end
ffe_results.config = Equalizerstruct();
ffe_results.config.eq = jsonencode(eq_);
ffe_results.config.equalizer_structure = int32(equalizer_structure.ffe);
ffe_results.config.comment = 'function: ffe';
ffe_results.metrics = Metricstruct;
ffe_results.metrics.result_id = NaN;
ffe_results.metrics.run_id = NaN;
ffe_results.metrics.eqParam_id = NaN;
ffe_results.metrics.date_of_processing = datetime('now');
ffe_results.metrics.BER = ber;
ffe_results.metrics.numBits = bits;
ffe_results.metrics.numBitErr = errors;
ffe_results.metrics.BER_precoded = ber_precoded;
ffe_results.metrics.numBitErr_precoded = errors_precoded;
ffe_results.metrics.SNR = snr;
ffe_results.metrics.SNR_level = snr_lvl;
ffe_results.metrics.STD = std_total;
ffe_results.metrics.STD_level = std_lvl;
ffe_results.metrics.STDrx = std_rxraw_total;
ffe_results.metrics.STDrx_level = std_rxraw_lvl;
ffe_results.metrics.GMI = gmi;
ffe_results.metrics.AIR = air;
ffe_results.metrics.EVM = evm_total;
ffe_results.metrics.EVM_level = evm_lvl;
ffe_results.metrics.Alpha = channel_alpha;
end
%% Helper Functions
function [bits, errors, ber, error_pos, errors_precoded, ber_precoded] = calculateBER(eq_signal_hd, tx_symbols, tx_bits, precode_mode, M, eth_style)
% Calculate BER based on precoding mode
mapper = PAMmapper(M, 0, "eth_style", eth_style);
switch precode_mode
case db_mode.no_db
% TX Data is not precoded
% A) Emulate diff precoding
eq_signal_hd_precoded = Duobinary().encode(eq_signal_hd, "M", M);
eq_signal_hd_precoded = Duobinary().decode(eq_signal_hd_precoded, "M", M);
tx_symbols_precoded = Duobinary().encode(tx_symbols);
tx_symbols_precoded = Duobinary().decode(tx_symbols_precoded);
tx_bits_precoded = mapper.demap(tx_symbols_precoded);
rx_bits = mapper.demap(eq_signal_hd_precoded);
[~, errors_precoded, ber_precoded, ~] = calc_ber(rx_bits.signal, tx_bits_precoded.signal, "skip_front", 10, "skip_end", 10, "returnErrorLocation", 1);
% B) Just determine BER
rx_bits = mapper.demap(eq_signal_hd);
tx_bits = mapper.demap(tx_symbols);
[bits, errors, ber, error_pos] = calc_ber(rx_bits.signal, tx_bits.signal, "skip_front", 10, "skip_end", 10, "returnErrorLocation", 1);
case db_mode.db_precoded
% Data is precoded on TX side
% A) Decode at Rx if no DB targeting was applied
eq_signal_hd_decoded = Duobinary().encode(eq_signal_hd, "M", M);
eq_signal_hd_decoded = Duobinary().decode(eq_signal_hd_decoded, "M", M);
rx_bits_decoded = mapper.demap(eq_signal_hd_decoded);
[~, errors_precoded, ber_precoded, ~] = calc_ber(rx_bits_decoded.signal, tx_bits.signal, "skip_front", 10, "skip_end", 10, "returnErrorLocation", 1);
% B) Omit the Coding by comparing with demapped TX symbol sequence
tx_bits_demapped = mapper.demap(tx_symbols);
rx_bits = mapper.demap(eq_signal_hd);
[bits, errors, ber, error_pos] = calc_ber(rx_bits.signal, tx_bits_demapped.signal, "skip_front", 10, "skip_end", 10, "returnErrorLocation", 1);
end
end
function displayAnalysis(eq_noise, eq_signal_sd, rx_signal, eq_, tx_symbols, M, postFFE)
% Display analysis plots and metrics
% Initialize figure handles
% Corrected line - added tx_symbols as second positional argument
% showLevelScatter(rx_signal.resample("fs_out", tx_symbols.fs), tx_symbols, "fignum", 100);
warning off
showLevelScatter(eq_signal_sd, tx_symbols, "fignum", 101);
figure(gcf);hold on; plot(((1:length(eq_noise.signal)) / eq_noise.fs) * 1e6,movmean(eq_noise.signal,2000,1), 'LineWidth',3,'Color','black')
warning on
showLevelHistogram(eq_signal_sd, tx_symbols, "fignum", 102);
showEQNoisePSD(eq_noise, "fignum", 103, "displayname", 'Residual Noise after FFE');
% Figure 2: Post-FFE coefficients (if available)
if ~isempty(postFFE)
showEQcoefficients('n1', postFFE.e, "displayname", 'Coefficients', 'fignum', 104);
end
try
figure(339);hold on
showEQfilter(eq_.e, eq_signal_sd.fs.*2,"displayname",'training','fignum',339);
% showEQfilter(eq_.e, eq_signal_sd.fs.*2,"displayname",'dec. directed','fignum',339);
legend on
end
% try
% figure(240); hold on; plot(pow2db(movmean(eq_.debug_struct.error_tr',100)));ylim([-30,3]);title('error training');
%
% figure(241); hold on; plot(pow2db(movmean(eq_.debug_struct.update_tr',100)));title('update step training');
%
% figure(242); hold on; plot(pow2db(movmean(eq_.debug_struct.update',1000)));title('update step dd');
% end
eq_signal_sd.eye(eq_signal_sd.fs,M,"displayname",'Eye','fignum',105);
end

View File

@@ -101,11 +101,11 @@ switch precode_mode
tx_bits_precoded = mapper.demap(tx_symbols_precoded);
rx_bits = mapper.demap(eq_signal_hd_precoded);
[~, errors_precoded, ber_precoded, ~] = calc_ber(rx_bits.signal, tx_bits_precoded.signal, "skip_front", 30000, "skip_end", 150, "returnErrorLocation", 1);
[~, errors_precoded, ber_precoded, ~] = calc_ber(rx_bits.signal, tx_bits_precoded.signal, "skip_front", 150, "skip_end", 150, "returnErrorLocation", 1);
% B) Just determine BER
rx_bits = mapper.demap(eq_signal_hd);
[bits, errors, ber, error_pos] = calc_ber(rx_bits.signal, tx_bits.signal, "skip_front", 30000, "skip_end", 150, "returnErrorLocation", 1);
[bits, errors, ber, error_pos] = calc_ber(rx_bits.signal, tx_bits.signal, "skip_front", 150, "skip_end", 150, "returnErrorLocation", 1);
case db_mode.db_precoded
% Data is precoded on TX side

View File

@@ -0,0 +1,146 @@
function Scpe_sig = preprocessSignal(Scpe_sig, Symbols, fsym, options)
% PREPROCESSSIGNAL Performs standard preprocessing on a signal
%
% Inputs:
% Scpe_sig - Input signal
% Symbols - Reference symbols for synchronization
% fsym - Symbol frequency
%
% Outputs:
% Scpe_sig - Preprocessed signal
arguments
Scpe_sig
Symbols
fsym
options.mode string = "auto"
options.tx_pulseformer = []
options.debug_plots (1,1) logical = false
options.apply_gaussian_filter (1,1) logical = true
options.gaussian_cutoff_factor (1,1) double = 0.52
end
preprocessMode = resolvePreprocessMode(options.mode, options.tx_pulseformer, Symbols);
targetFs = 2 * fsym;
switch preprocessMode
case "matched_filter"
matchedPulseformer = buildMatchedPulseformer(options.tx_pulseformer, Symbols, fsym, targetFs);
Scpe_sig = matchedPulseformer.process(Scpe_sig);
case "resample"
Scpe_sig = Scpe_sig.resample("fs_out", targetFs);
otherwise
error('preprocessSignal:InvalidMode', 'Unsupported preprocessing mode "%s".', preprocessMode);
end
[Scpe_sig, Scpe_cell, inverted] = Scpe_sig.tsynch("reference", Symbols, "fs_ref", fsym, "debug_plots", options.debug_plots);
% Scpe_sig = Scpe_cell{1};
% Apply Gaussian filter
if options.apply_gaussian_filter
Scpe_sig = Filter('filtdegree', 8, "f_cutoff", Symbols.fs.*options.gaussian_cutoff_factor, ...
"fs", Scpe_sig.fs, "filterType", filtertypes.gaussian, ...
"active", true).process(Scpe_sig);
end
%Remove DC offset
Scpe_sig = Scpe_sig - mean(Scpe_sig.signal);
end
function preprocessMode = resolvePreprocessMode(requestedMode, txPulseformer, Symbols)
requestedMode = string(requestedMode);
if requestedMode ~= "auto"
preprocessMode = requestedMode;
return
end
pulseformer = txPulseformer;
if isempty(pulseformer)
pulseformer = findSignalPulseformer(Symbols);
end
if isRootRaisedCosinePulseformer(pulseformer)
preprocessMode = "matched_filter";
else
preprocessMode = "resample";
end
end
function tf = isRootRaisedCosinePulseformer(pulseformer)
tf = false;
if isempty(pulseformer)
return
end
pulseValue = readPulseformerField(pulseformer, 'pulse', []);
if isempty(pulseValue)
return
end
pulseValue = normalizePulseValue(pulseValue);
tf = pulseValue == pulseform.rrc;
end
function matchedPulseformer = buildMatchedPulseformer(txPulseformer, Symbols, fsym, targetFs)
pulseformerMeta = txPulseformer;
if isempty(pulseformerMeta)
pulseformerMeta = findSignalPulseformer(Symbols);
end
pulseValue = normalizePulseValue(readPulseformerField(pulseformerMeta, 'pulse', pulseform.rrc));
alphaValue = readPulseformerField(pulseformerMeta, 'alpha', 0.05);
pulseLengthValue = readPulseformerField(pulseformerMeta, 'pulselength', 16);
if isempty(alphaValue)
alphaValue = 0.05;
end
matchedPulseformer = Pulseformer( ...
"fsym", fsym, ...
"fdac", targetFs, ...
"pulse", pulseValue, ...
"pulselength", pulseLengthValue, ...
"alpha", alphaValue, ...
"matched", 1);
end
function value = readPulseformerField(pulseformerMeta, fieldName, defaultValue)
value = defaultValue;
if isempty(pulseformerMeta)
return
end
if isstruct(pulseformerMeta) && isfield(pulseformerMeta, fieldName)
candidate = pulseformerMeta.(fieldName);
if ~isempty(candidate)
value = candidate;
end
return
end
if isobject(pulseformerMeta) && isprop(pulseformerMeta, fieldName)
candidate = pulseformerMeta.(fieldName);
if ~isempty(candidate)
value = candidate;
end
end
end
function pulseValue = normalizePulseValue(rawValue)
if isa(rawValue, 'pulseform')
pulseValue = rawValue;
return
end
if iscell(rawValue)
rawValue = rawValue{1};
end
if isstring(rawValue) || ischar(rawValue)
pulseValue = pulseform.(char(string(rawValue)));
return
end
pulseValue = pulseform(rawValue);
end

View File

@@ -48,6 +48,8 @@ eq_signal_hd = PAMmapper(M, 0).quantize(eq_signal_sd);
[mlse_sig_sd,whitened_noise] = pf_.process(eq_signal_sd, eq_noise);
[tx_symbols_pr,~] = pf_.process(tx_symbols, eq_noise);
if 0 %tx_symbols.fs > 190e9
if pf_.ncoeff == 1
if pf_.coefficients(2) < 0
@@ -272,34 +274,35 @@ eq_noise.spectrum("displayname",'Equalized Signal','fignum',336,'normalizeTo0dB'
showEQNoisePSD(eq_noise, "fignum", 338, "displayname", 'Residual Noise after VNLE', 'postfilter_taps', pf_.coefficients);
for t = 1:4
pf_.ncoeff = t;
[~,~] = pf_.process(eq_signal_sd, eq_noise);
showEQNoisePSD(eq_noise, "fignum", 3388, "displayname", 'Residual Noise after VNLE', 'postfilter_taps', pf_.coefficients);
showEQNoisePSD(eq_noise, "fignum", 339, "displayname", 'Residual Noise after VNLE', 'postfilter_taps', pf_.coefficients);
end
tx_symbols.spectrum("displayname",'Equalized Signal','fignum',1234,'normalizeTo0dB',1);
tx_symbols.spectrum("displayname",'Equalized Signal','fignum',340,'normalizeTo0dB',1);
if ~isempty(postFFE)
showEQcoefficients('n1', postFFE.e, "displayname", 'Coefficients', 'fignum', 338);
showEQcoefficients('n1', postFFE.e, "displayname", 'Coefficients', 'fignum', 341);
end
% showEQcoefficients('n1', eq_.e1,'n2', eq_.e2,'n3', eq_.e3, "displayname", 'Coefficients', 'fignum', 339);
showEQcoefficients('n1', eq_.e,'n2', eq_.e2,'n3', eq_.e3, "displayname", 'Coefficients', 'fignum', 339);
showEQfilter(eq_.e, eq_signal_sd.fs.*2);
figure(340); clf;
eq_signal_sd.eye(eq_signal_sd.fs, M, "fignum", 340);
eq_signal_sd.eye(eq_signal_sd.fs, M, "fignum", 342);
figure(341); clf;
showLevelHistogram(eq_signal_sd, tx_symbols, "fignum", 341);
showLevelHistogram(eq_signal_sd, tx_symbols, "fignum", 343);
warning off
showLevelScatter(eq_signal_sd, tx_symbols, "fignum", 400);
showLevelScatter(eq_signal_sd, tx_symbols, "fignum", 344);
% showLevelScatter(rx_signal.resample("fs_out", tx_symbols.fs), tx_symbols, "fignum", 401);
drawnow;
warning on
whitened_noise.spectrum("displayname",'after postfilter','fignum',342);
eq_noise.spectrum("displayname",'before postfilter','fignum',342);
whitened_noise.spectrum("displayname",'after postfilter','fignum',345);
eq_noise.spectrum("displayname",'before postfilter','fignum',345);
end

View File

@@ -0,0 +1,36 @@
function output = dsp_recipe_minimal(Scpe_sig_raw, Symbols, Tx_bits, options)
%DSP_RECIPE_MINIMAL Minimal example recipe for the DSP job framework.
% This recipe intentionally performs only light preprocessing and records
% summary values. It demonstrates the recipe interface without running a
% full equalizer chain.
arguments
Scpe_sig_raw
Symbols
Tx_bits
options.fsym
options.M
options.duob_mode
options.dataTable table
options.userParameters struct = struct()
options.debug_plots (1,1) logical = false
end
Scpe_sig = preprocessSignal(Scpe_sig_raw, Symbols, options.fsym, ...
"mode", "auto", ...
"debug_plots", options.debug_plots);
eq_ffe = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-1,"mu_tr",0.4,"order",25,...
"sps",2,"decide",0,"optmize_mus",0,"dd_mode",options.userParameters.dd_mode,"adaption_technique","nlms","dc_tracking_mu",1.021e-05);
ffe_results = ffe(eq_ffe, options.M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", options.duob_mode, ...
'showAnalysis', options.debug_plots, ...
"postFFE", [], ...
"eth_style_symbol_mapping", 0);
ffe_results.config.equalizer_structure = "ffe";
ffe_results.metrics.print("description",'FFE');
output.ffe_package = ffe_results;
end

View File

@@ -0,0 +1,206 @@
function output = dsp_scope_signal(Scpe_sig_raw, Symbols, Tx_bits, options)
%DSP_SCOPE_SIGNAL Run the common DSP chain starting from a scope signal.
arguments
Scpe_sig_raw
Symbols
Tx_bits
options.fsym
options.M
options.duob_mode
options.userParameters struct = struct()
options.preprocess_mode string = "auto"
options.tx_pulseformer = []
options.debug_plots (1,1) logical = false
end
output.ffe_package = [];
output.dfe_package = [];
output.mlse_package = [];
output.vnle_package = [];
output.dbtgt_package = [];
output.dbenc_package = [];
output.mlmlse_package = [];
fsym = options.fsym;
M = options.M;
duob_mode = options.duob_mode;
len_tr = 4096*2;
ffe_order_ffe = [50, 0, 0];
ffe_order_dfe = [50, 5, 5];
ffe_order_vnle = [50, 5, 5];
ffe_order_dbtgt = [50, 5, 5];
dfe_order_vnle = [0, 0, 0];
dfe_order_dbtgt = [0, 0, 0];
dfe_feedback_order = [2, 0, 0];
pf_ncoeffs = 1;
mu_ffe = [0.0001, 0.0008, 0.001];
mu_dfe = 0.0004;
mu_dc = 0.005;
dc_buffer_len = 1; %#ok<NASGU>
mu_tr = 0;
mu_dd = 0.05;
adaption = 1;
use_dd_mode = 1;
use_ffe = 1;
use_dfe = 1;
use_vnle_mlse = 1;
use_dbtgt = 1;
use_dbenc = 0;
use_ml_mlse = 0;
showAnalysis = 0;
decoding_mode = [];
addProcessingResultToDatabase = 0; %#ok<NASGU>
paramStruct = options.userParameters;
if ~isempty(paramStruct)
paramNames = fieldnames(paramStruct);
for i = 1:numel(paramNames)
thisName = paramNames{i};
thisValue = paramStruct.(thisName);
eval([thisName ' = thisValue;']);
end
end
pf_ = Postfilter("ncoeff", pf_ncoeffs, "useBurg", 1); %#ok<NASGU>
mlse_ = MLSE("duobinary_output", 0, 'M', M, 'trellis_states', PAMmapper(M,0).levels); %#ok<NASGU>
eq_post = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-4,"mu_tr",0,"order",2001,"sps",1,"decide",0,"adaption_technique","lms","dc_tracking_mu",mu_dc); %#ok<NASGU>
eq_post = FFE("epochs_tr",5,"epochs_dd",2,"len_tr",2^13,"mu_dd",mu_dd,"mu_tr",mu_tr,"order",25,"sps",2,"decide",0, "adaption_technique",adaption_method(adaption),"dd_mode",use_dd_mode,"dc_tracking_mu",mu_dc); %#ok<NASGU>
mlse_db_enc = MLSE("DIR", [1,1], "duobinary_output", 0, "M", M, "trellis_states", PAMmapper(M,0).levels); %#ok<NASGU>
eq_db_enc = EQ("Ne", ffe_order_dbtgt, "Nb", dfe_order_dbtgt, "training_length", len_tr, ...
"training_loops", 5, "dd_loops", 5, "K", 2, "DCmu", mu_dc, ...
"DDmu", [mu_ffe mu_dfe], "DFEmu", 0.005, "FFEmu", 0, "plotfinal", 0, "ideal_dfe", 1);
Scpe_sig = preprocessSignal(Scpe_sig_raw, Symbols, fsym, ...
"mode", options.preprocess_mode, ...
"tx_pulseformer", options.tx_pulseformer, ...
"debug_plots", 0);
% Scpe_sig.spectrum("fignum",200,"normalizeTo0dB",1,"displayname",'Rx','addDCoffset',-6);
% ylim([-30,3]);
% xlim([-5,100]);
Scpe_sig.signal = Scpe_sig.signal(1:2*Symbols.length);
Scpe_sig.signal = real(Scpe_sig.signal);
if duob_mode ~= db_mode.db_encoded
if use_ffe
eq_ffe = EQ("Ne",ffe_order_ffe,"Nb",[0,0,0],"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",0);
eq_ffe = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-1,"mu_tr",0.4,"order",ffe_order_ffe(1),...
"sps",2,"decide",0,"optmize_mus",0,"dd_mode",1,"adaption_technique","nlms","dc_tracking_mu",1.021e-05);
ffe_results = ffe(eq_ffe, M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", duob_mode, ...
'showAnalysis', options.debug_plots, ...
"postFFE", [], ...
"eth_style_symbol_mapping", 0);
ffe_results.config.equalizer_structure = "ffe";
ffe_results.metrics.print("description",'FFE');
output.ffe_package = ffe_results;
end
if use_dfe
eq_dfe = EQ("Ne",ffe_order_dfe,"Nb",dfe_feedback_order,"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",0);
dfe_results = ffe(eq_dfe, M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", duob_mode, ...
'showAnalysis', options.debug_plots, ...
"postFFE", [], ...
"eth_style_symbol_mapping", 0);
dfe_results.config.equalizer_structure = "dfe";
dfe_results.metrics.print("description",'DFE');
output.dfe_package = dfe_results;
end
if use_vnle_mlse
eq_ = EQ("Ne",ffe_order_vnle,"Nb",dfe_order_vnle,"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",0);
pf_ = Postfilter("ncoeff",pf_ncoeffs,"useBurg",1);
useviterbi = 0;
if useviterbi
mlse_ = MLSE_viterbi("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels);
else
if duob_mode == db_mode.no_db && M == 6
trellexlusion = 1;
else
trellexlusion = 0;
end
mlse_ = MLSE("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels,'scale_mode',2,'trellis_exclusion',trellexlusion,'trellis_state_mode',2);
end
[ffe_results, mlse_results] = vnle_postfilter_mlse(eq_, pf_, mlse_, M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", duob_mode, ...
'showAnalysis', showAnalysis, ...
"postFFE", [], ...
"eth_style_symbol_mapping", 0);
ffe_results.config.equalizer_structure = "vnle";
ffe_results.metrics.print("description",'VNLE');
mlse_results.metrics.print("description",'MLSE');
output.mlse_package = mlse_results;
output.vnle_package = ffe_results;
end
if use_ml_mlse
mu_ml = 0.01;
training_epochs = 100;
ml_mlse_equalizer = ML_MLSE("epochs_tr",training_epochs,"epochs_dd",1, ...
"len_tr",length(Scpe_sig)/4,"mu_dd",mu_ml,"mu_tr",mu_ml,"order",11,"sps",2, ...
"traceback_depth",128,"L",1,"delta",4,"adaptive_mu",0);
ml_mlse_results = ml_mlse(ml_mlse_equalizer, M, Scpe_sig, Symbols, Tx_bits, "precode_mode", duob_mode);
output.mlmlse_package = ml_mlse_results;
end
if use_dbtgt
useviterbi = 0;
if useviterbi
mlse_db_ = MLSE_viterbi("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels);
else
if duob_mode == db_mode.no_db && M == 6
trellexlusion = 1;
else
trellexlusion = 0;
end
mlse_db_ = MLSE("DIR",[1,1],"duobinary_output",0,"M",M,"trellis_states",PAMmapper(M,0).levels,'scale_mode',2,'trellis_exclusion',trellexlusion,'trellis_state_mode',3);
end
eq_ = EQ("Ne",ffe_order_dbtgt,"Nb",dfe_order_dbtgt,"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",1);
if isempty(decoding_mode)
dbt_results = duobinary_target(eq_, mlse_db_, M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", duob_mode, ...
'showAnalysis', showAnalysis, ...
"postFFE", []);
else
dbt_results = duobinary_target(eq_, mlse_db_, M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", duob_mode, ...
'showAnalysis', showAnalysis, ...
"postFFE", [], ...
"decoding_mode", decoding_mode);
end
dbt_results.metrics.print("description",'Duob. Target');
output.dbtgt_package = dbt_results;
end
end
if duob_mode == db_mode.db_encoded
mlse_db_enc = MLSE_viterbi("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels); %#ok<NASGU>
mlse_db_enc = MLSE("DIR", [1,1], "duobinary_output", 0, "M", M, "trellis_states", PAMmapper(M,0).levels);
db_results = duobinary_signaling(eq_db_enc, mlse_db_enc, M, Scpe_sig, Symbols, Tx_bits, "precode_mode",duob_mode, "showAnalysis",showAnalysis,"postFFE",[]);
output.dbenc_package = db_results;
end
end

View File

@@ -0,0 +1,404 @@
function output = mpi_recipe_dev(Scpe_sig_raw, Symbols, Tx_bits, options)
%mpi_recipe_dev Minimal example recipe for the DSP job framework.
% This recipe intentionally performs only light preprocessing and records
% summary values. It demonstrates the recipe interface without running a
% full equalizer chain.
arguments
Scpe_sig_raw
Symbols
Tx_bits
options.fsym
options.M
options.duob_mode
options.dataTable table
options.userParameters struct = struct()
options.debug_plots (1,1) logical = false
end
Scpe_sig = preprocessSignal(Scpe_sig_raw, Symbols, options.fsym, ...
"mode", "auto", ...
"debug_plots", options.debug_plots);
output = struct();
%% conventional FFE
eq_core_settings = { ...
"sps", 2, ...
"order", 25, ...
"decide", 0, ...
"adaption_technique", "nlms"};
eq_training_settings = { ...
"len_tr", 4096, ...
"epochs_tr", 5, ...
"mu_tr", 0.04};
eq_dd_settings = { ...
"dd_mode", 1, ...
"epochs_dd", 3, ...
"mu_dd", 0.012};
eq_a1_settings = { ...
"dc_smoothing_a1", 0, ...
"dc_avg_bufferlength_a1", 0, ...
"dc_avg_update_blocklength_a1", 1};
eq_a2_settings = { ...
"dc_smoothing_a2", 0, ...
"dc_level_avg_bufferlength_a2", 0, ...
"dc_level_update_blocklength_a2", 0, ...
"dc_level_weights_a2", [0.6 0.6 0.6 0.6]};
eq_dc_tracking_settings = { ...
"dc_tracking_mu", 0, ...
"dc_tracking_adaptive_enabled", 0, ...
"dc_tracking_persistence_gain", 0, ...
"dc_tracking_buffer_len", 0};
eq_ffe_update_settings = { ...
"ffe_update_buffer_len", 1};
eq_optimizer_settings = { ...
"optmize_mus", 0, ...
"plot_mu_optimization", options.debug_plots, ...
"optimize_dc_tracking_params", 0, ...
"optimize_a2_level_weights", 0, ...
"a2_level_weight_optimization_len", 2^15, ...
"a2_level_weight_optimization_max_evals", 30, ...
"a2_level_weight_max", 1};
eq_debug_settings = { ...
"save_debug", false};
eq_settings = [ ...
eq_core_settings, ...
eq_training_settings, ...
eq_dd_settings, ...
eq_a1_settings, ...
eq_a2_settings, ...
eq_dc_tracking_settings, ...
eq_ffe_update_settings, ...
eq_optimizer_settings, ...
eq_debug_settings];
eq_ffe = FFE(eq_settings{:});
if 1
[ffe_results,equalized_signal] = ffe(eq_ffe, options.M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", options.duob_mode, ...
'showAnalysis', options.debug_plots, ...
"postFFE", [], ...
"eth_style_symbol_mapping", 0);
ffe_results.metrics.print("description",sprintf('Immediate A2; SIR %d dB',options.dataTable.sir));
output.conventional_ffe = ffe_results;
end
if options.debug_plots
showLevelScatter(Scpe_sig_raw, Symbols, ...
"fsym", options.fsym, ...
"fignum", 400, ...
"normalize", true);
[~, avg_for_lvl] = showLevelScatter(equalized_signal, Symbols, ...
"fsym", options.fsym, ...
"fignum", 401, ...
"normalize", true);
end
%% A2
eq_core_settings = { ...
"sps", 2, ...
"order", 25, ...
"decide", 0, ...
"adaption_technique", "nlms"};
eq_training_settings = { ...
"len_tr", 4096, ...
"epochs_tr", 5, ...
"mu_tr", 0.04};
eq_dd_settings = { ...
"dd_mode", 1, ...
"epochs_dd", 3, ...
"mu_dd", 0.012};
eq_a1_settings = { ...
"dc_smoothing_a1", 0, ...
"dc_avg_bufferlength_a1", 0, ...
"dc_avg_update_blocklength_a1", 1};
eq_a2_settings = { ...
"dc_smoothing_a2", 1, ...
"dc_level_avg_bufferlength_a2", 256, ...
"dc_level_update_blocklength_a2", options.userParameters.block_update, ...
"dc_level_weights_a2", [0.6 0.6 0.6 0.6]};
eq_dc_tracking_settings = { ...
"dc_tracking_mu", 0, ...
"dc_tracking_adaptive_enabled", 0, ...
"dc_tracking_persistence_gain", 0, ...
"dc_tracking_buffer_len", 0};
eq_ffe_update_settings = { ...
"ffe_update_buffer_len", 1};
eq_optimizer_settings = { ...
"optmize_mus", 0, ...
"plot_mu_optimization", options.debug_plots, ...
"optimize_dc_tracking_params", 0, ...
"optimize_a2_level_weights", 1, ...
"a2_level_weight_optimization_len", 2^15, ...
"a2_level_weight_optimization_max_evals", 30, ...
"a2_level_weight_max", 1};
eq_debug_settings = { ...
"save_debug", false};
eq_settings = [ ...
eq_core_settings, ...
eq_training_settings, ...
eq_dd_settings, ...
eq_a1_settings, ...
eq_a2_settings, ...
eq_dc_tracking_settings, ...
eq_ffe_update_settings, ...
eq_optimizer_settings, ...
eq_debug_settings];
eq_ffe = FFE(eq_settings{:});
if 1
[ffe_results,equalized_signal] = ffe(eq_ffe, options.M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", options.duob_mode, ...
'showAnalysis', options.debug_plots, ...
"postFFE", [], ...
"eth_style_symbol_mapping", 0);
ffe_results.metrics.print("description",sprintf('Immediate A2; SIR %d dB',options.dataTable.sir));
output.a2_immediate_updates = ffe_results;
end
if options.debug_plots
showLevelScatter(Scpe_sig_raw, Symbols, ...
"fsym", options.fsym, ...
"fignum", 400, ...
"normalize", true);
[~, avg_for_lvl] = showLevelScatter(equalized_signal, Symbols, ...
"fsym", options.fsym, ...
"fignum", 401, ...
"normalize", true);
end
%% A1
eq_a1_settings = { ...
"dc_smoothing_a1", 1 ...
"dc_avg_bufferlength_a1", 2048, ...
"dc_avg_update_blocklength_a1", options.userParameters.block_update};
eq_a2_settings = { ...
"dc_smoothing_a2", 0, ...
"dc_level_avg_bufferlength_a2", 0, ...
"dc_level_update_blocklength_a2", 0, ...
"dc_level_weights_a2", [0.6 0.6 0.6 0.6]};
eq_optimizer_settings = { ...
"optmize_mus", 0, ...
"plot_mu_optimization", options.debug_plots, ...
"optimize_dc_tracking_params", 0, ...
"optimize_a2_level_weights", 0, ...
"a2_level_weight_optimization_len", 2^15, ...
"a2_level_weight_optimization_max_evals", 30, ...
"a2_level_weight_max", 1};
eq_debug_settings = { ...
"save_debug", false};
eq_settings = [ ...
eq_core_settings, ...
eq_training_settings, ...
eq_dd_settings, ...
eq_a1_settings, ...
eq_a2_settings, ...
eq_dc_tracking_settings, ...
eq_ffe_update_settings, ...
eq_optimizer_settings, ...
eq_debug_settings];
eq_ffe = FFE(eq_settings{:});
if 1
[ffe_results,equalized_signal] = ffe(eq_ffe, options.M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", options.duob_mode, ...
'showAnalysis', options.debug_plots, ...
"postFFE", [], ...
"eth_style_symbol_mapping", 0);
ffe_results.metrics.print("description",sprintf('Immediate A1; SIR %d dB',options.dataTable.sir));
output.a1_immediate_updates = ffe_results;
end
%% Tracking
eq_core_settings = { ...
"sps", 2, ...
"order", 25, ...
"decide", 0, ...
"adaption_technique", "nlms"};
eq_training_settings = { ...
"len_tr", 4096, ...
"epochs_tr", 5, ...
"mu_tr", 0.04};
eq_dd_settings = { ...
"dd_mode", 1, ...
"epochs_dd", 3, ...
"mu_dd", 0.012};
eq_a1_settings = { ...
"dc_smoothing_a1", 0, ...
"dc_avg_bufferlength_a1", 0, ...
"dc_avg_update_blocklength_a1", 1};
eq_a2_settings = { ...
"dc_smoothing_a2", 0, ...
"dc_level_avg_bufferlength_a2", 0, ...
"dc_level_update_blocklength_a2", 0, ...
"dc_level_weights_a2", [0.6 0.6 0.6 0.6]};
eq_dc_tracking_settings = { ...
"dc_tracking_mu", 0.002, ...
"dc_tracking_adaptive_enabled", 0, ...
"dc_tracking_persistence_gain", 0, ...
"dc_tracking_buffer_len", options.userParameters.block_update};
eq_ffe_update_settings = { ...
"ffe_update_buffer_len", 1};
eq_optimizer_settings = { ...
"optmize_mus", 0, ...
"plot_mu_optimization", options.debug_plots, ...
"optimize_dc_tracking_params", 1, ...
"optimize_a2_level_weights", 0, ...
"a2_level_weight_optimization_len", 2^15, ...
"a2_level_weight_optimization_max_evals", 30, ...
"a2_level_weight_max", 1};
eq_debug_settings = { ...
"save_debug", false};
eq_settings = [ ...
eq_core_settings, ...
eq_training_settings, ...
eq_dd_settings, ...
eq_a1_settings, ...
eq_a2_settings, ...
eq_dc_tracking_settings, ...
eq_ffe_update_settings, ...
eq_optimizer_settings, ...
eq_debug_settings];
eq_ffe = FFE(eq_settings{:});
if 1
[ffe_results,equalized_signal] = ffe(eq_ffe, options.M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", options.duob_mode, ...
'showAnalysis', options.debug_plots, ...
"postFFE", [], ...
"eth_style_symbol_mapping", 0);
ffe_results.metrics.print("description",sprintf('Immediate A2; SIR %d dB',options.dataTable.sir));
output.tracking_fixmu_immediate_updates = ffe_results;
end
%% Tracking Adaptive
eq_core_settings = { ...
"sps", 2, ...
"order", 25, ...
"decide", 0, ...
"adaption_technique", "nlms"};
eq_training_settings = { ...
"len_tr", 4096, ...
"epochs_tr", 5, ...
"mu_tr", 0.04};
eq_dd_settings = { ...
"dd_mode", 1, ...
"epochs_dd", 3, ...
"mu_dd", 0.012};
eq_a1_settings = { ...
"dc_smoothing_a1", 0, ...
"dc_avg_bufferlength_a1", 0, ...
"dc_avg_update_blocklength_a1", 1};
eq_a2_settings = { ...
"dc_smoothing_a2", 0, ...
"dc_level_avg_bufferlength_a2", 0, ...
"dc_level_update_blocklength_a2", 0, ...
"dc_level_weights_a2", [0.6 0.6 0.6 0.6]};
eq_dc_tracking_settings = { ...
"dc_tracking_mu", 0.002, ...
"dc_tracking_adaptive_enabled", 1, ...
"dc_tracking_persistence_gain", 0, ...
"dc_tracking_buffer_len", options.userParameters.block_update};
eq_ffe_update_settings = { ...
"ffe_update_buffer_len", 1};
eq_optimizer_settings = { ...
"optmize_mus", 0, ...
"plot_mu_optimization", options.debug_plots, ...
"optimize_dc_tracking_params", 1, ...
"optimize_a2_level_weights", 0, ...
"a2_level_weight_optimization_len", 2^15, ...
"a2_level_weight_optimization_max_evals", 30, ...
"a2_level_weight_max", 1};
eq_debug_settings = { ...
"save_debug", false};
eq_settings = [ ...
eq_core_settings, ...
eq_training_settings, ...
eq_dd_settings, ...
eq_a1_settings, ...
eq_a2_settings, ...
eq_dc_tracking_settings, ...
eq_ffe_update_settings, ...
eq_optimizer_settings, ...
eq_debug_settings];
eq_ffe = FFE(eq_settings{:});
if 1
[ffe_results,equalized_signal] = ffe(eq_ffe, options.M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", options.duob_mode, ...
'showAnalysis', options.debug_plots, ...
"postFFE", [], ...
"eth_style_symbol_mapping", 0);
ffe_results.metrics.print("description",sprintf('Immediate A2; SIR %d dB',options.dataTable.sir));
output.tracking_adaptive_immediate_updates = ffe_results;
end
end

View File

@@ -0,0 +1,396 @@
function output = mpi_recipe_dev(Scpe_sig_raw, Symbols, Tx_bits, options)
%mpi_recipe_dev Minimal MPI equalizer comparison recipe.
arguments
Scpe_sig_raw
Symbols
Tx_bits
options.fsym
options.M
options.duob_mode
options.dataTable table
options.userParameters struct = struct()
options.debug_plots (1,1) logical = false
end
%% Execution toggles
run_conventional_ffe = 1;
run_ma = 0;
run_hpf = 0;
run_a2_tracked_levels = 0;
run_a2_residual = 0;
run_a1 = 0;
run_tracking_adaptive = 1;
plot_output_signals = 1;
%% Shared fixed EQ settings
eq_sps = 2;
eq_order = 50;
eq_adaption = "nlms";
eq_len_tr = 4096;
eq_epochs_tr = 5;
eq_mu_tr = 0.04;
eq_epochs_dd = 3;
eq_save_debug = false;
block_update = 1;
if isfield(options.userParameters,"block_update")
block_update = options.userParameters.block_update;
end
Scpe_sig = preprocessSignal(Scpe_sig_raw, Symbols, options.fsym, ...
"mode", "auto", ...
"debug_plots", options.debug_plots);
output = struct();
%%
if plot_output_signals
showLevelScatter(Scpe_sig, Symbols, ...
"fsym", options.fsym, ...
"fignum", 400, ...
"normalize", true);
end
%% Conventional FFE
if run_conventional_ffe
eq_ffe = FFE_plain( ...
"sps", eq_sps, ...
"order", eq_order, ...
"decide", false, ...
"adaption_technique", eq_adaption, ...
"len_tr", eq_len_tr, ...
"epochs_tr", eq_epochs_tr, ...
"mu_tr", eq_mu_tr, ...
"dd_mode", true, ...
"epochs_dd", eq_epochs_dd, ...
"mu_dd", 0.0012, ...
"optmize_mus", false, ...
"plot_mu_optimization", options.debug_plots, ...
"save_debug", eq_save_debug);
storageName = "conventional_ffe";
[ffe_results,equalized_signal] = runFfe(eq_ffe, "Conventional FFE", ...
Scpe_sig, Symbols, Tx_bits, options);
ffe_results = attachMpiReductionConfig(ffe_results, eq_ffe, storageName, ...
"plain_ffe", "baseline", block_update);
output.(char(storageName)) = ffe_results;
eq_noise_ffe = equalized_signal - Symbols;
if plot_output_signals
plotEqSignals(equalized_signal,Symbols,options,400,-1);
end
end
%% Moving Average Filter -> Conventional FFE
if run_ma
eq_ffe = FFE_plain( ...
"sps", eq_sps, ...
"order", eq_order, ...
"decide", false, ...
"adaption_technique", eq_adaption, ...
"len_tr", eq_len_tr, ...
"epochs_tr", eq_epochs_tr, ...
"mu_tr", eq_mu_tr, ...
"dd_mode", true, ...
"epochs_dd", eq_epochs_dd, ...
"mu_dd", 0.0012, ...
"optmize_mus", false, ...
"plot_mu_optimization", options.debug_plots, ...
"save_debug", eq_save_debug);
%%% APPLY MA filter
movavg_window_length = options.userParameters.bufferlen;
movavg_window_symmetry = "causal"; % "causal" or "noncausal"
switch movavg_window_symmetry
case "causal"
dc_estimate = movmean(Scpe_sig.signal, ...
[movavg_window_length-1,0],"Endpoints","shrink");
case "noncausal"
dc_estimate = movmean(Scpe_sig.signal, ...
movavg_window_length,"Endpoints","shrink");
otherwise
error("mpi_recipe_dev:InvalidMovAvgSymmetry", ...
"movavg_window_symmetry must be 'causal' or 'noncausal'.");
end
Scpe_sig_filt = Scpe_sig;
Scpe_sig_filt.signal = Scpe_sig.signal - dc_estimate;
clear dc_estimate;
storageName = "MovAvg_plus_conventional_FFE";
[ffe_results,equalized_signal] = runFfe(eq_ffe, "Moving-average + conventional FFE", ...
Scpe_sig_filt, Symbols, Tx_bits, options);
clear Scpe_sig_filt;
ffe_results = attachMpiReductionConfig(ffe_results, eq_ffe, storageName, ...
"MovAvg_plus_conventional_FFE", char(movavg_window_symmetry), block_update);
output.(char(storageName)) = ffe_results;
if plot_output_signals
plotEqSignals(equalized_signal,Symbols,options,400,-1);
end
end
%% High Pass Filter -> Conventional FFE
if run_hpf
eq_ffe = FFE_plain( ...
"sps", eq_sps, ...
"order", eq_order, ...
"decide", false, ...
"adaption_technique", eq_adaption, ...
"len_tr", eq_len_tr, ...
"epochs_tr", eq_epochs_tr, ...
"mu_tr", eq_mu_tr, ...
"dd_mode", true, ...
"epochs_dd", eq_epochs_dd, ...
"mu_dd", 0.0012, ...
"optmize_mus", false, ...
"plot_mu_optimization", options.debug_plots, ...
"save_debug", eq_save_debug);
%%% APPLY HPF in MHZ order
f = Filter("f_cutoff",options.userParameters.hpf,"filterType","butterworth","lowpass",0,"filtdegree",4,"fs",options.fsym*2);
Scpe_sig_filt = f.process(Scpe_sig);
storageName = "HPF_plus_conventional_FFE";
[ffe_results,equalized_signal] = runFfe(eq_ffe, "Conventional FFE", ...
Scpe_sig_filt, Symbols, Tx_bits, options);
clear Scpe_sig_filt;
ffe_results = attachMpiReductionConfig(ffe_results, eq_ffe, storageName, ...
"HPF_plus_conventional_FFE", "hpf", block_update);
output.(char(storageName)) = ffe_results;
if plot_output_signals
plotEqSignals(equalized_signal,Symbols,options,400,-1);
end
end
%% A2 tracked-level decision
if run_a2_tracked_levels
eq_ffe = FFE_A2TrackedLevels( ...
"sps", eq_sps, ...
"order", eq_order, ...
"decide", true, ...
"adaption_technique", eq_adaption, ...
"len_tr", eq_len_tr, ...
"epochs_tr", eq_epochs_tr, ...
"mu_tr", eq_mu_tr, ...
"dd_mode", true, ...
"epochs_dd", eq_epochs_dd, ...
"mu_dd", 0.012, ...
"dc_smoothing_a2", 1, ...
"dc_level_avg_bufferlength_a2", 112, ... %war 112
"dc_level_update_blocklength_a2", block_update, ... % war block_update
"dc_level_weights_a2", [1], ...
"save_debug", eq_save_debug);
storageName = "a2_adaptive_levels";
[ffe_results,equalized_signal] = runFfe(eq_ffe, "A2 tracked levels", ...
Scpe_sig, Symbols, Tx_bits, options);
ffe_results = attachMpiReductionConfig(ffe_results, eq_ffe, storageName, ...
"a2_tracked_levels", "tracked_levels", block_update);
output.(char(storageName)) = ffe_results;
if plot_output_signals
plotEqSignals(equalized_signal,Symbols,options,410,-1);
end
end
%% A2 residual correction
if run_a2_residual
eq_ffe = FFE_A2Residual( ...
"sps", eq_sps, ...
"order", eq_order, ...
"decide", false, ...
"adaption_technique", eq_adaption, ...
"len_tr", eq_len_tr, ...
"epochs_tr", eq_epochs_tr, ...
"mu_tr", eq_mu_tr, ...
"dd_mode", true, ...
"epochs_dd", eq_epochs_dd, ...
"mu_dd", 0.012, ...
"dc_smoothing_a2", 1, ...
"dc_level_avg_bufferlength_a2", 112, ...
"dc_level_update_blocklength_a2", block_update, ...
"dc_level_weights_a2", [0.6], ...
"save_debug", eq_save_debug, "optmize_mus",0);
storageName = "a2_residual";
[ffe_results,equalized_signal] = runFfe(eq_ffe, "A2 residual", ...
Scpe_sig, Symbols, Tx_bits, options);
ffe_results = attachMpiReductionConfig(ffe_results, eq_ffe, storageName, ...
"a2_residual", "residual_correction", block_update);
output.(char(storageName)) = ffe_results;
if plot_output_signals
plotEqSignals(equalized_signal,Symbols,options,420,-1);
end
end
%% A1 moving-average input suppression
if run_a1
eq_ffe = FFE_A1( ...
"sps", eq_sps, ...
"order", eq_order, ...
"decide", false, ...
"adaption_technique", eq_adaption, ...
"len_tr", eq_len_tr, ...
"epochs_tr", eq_epochs_tr, ...
"mu_tr", eq_mu_tr, ...
"dd_mode", false, ...
"epochs_dd", eq_epochs_dd, ...
"mu_dd", 0.012, ...
"dc_smoothing_a1", 1, ...
"dc_avg_bufferlength_a1", 2048, ... %was 2048
"dc_avg_update_blocklength_a1", block_update, ...
"save_debug", eq_save_debug);
storageName = "a1_ff_dc_avg";
[ffe_results,equalized_signal] = runFfe(eq_ffe, "A1", ...
Scpe_sig, Symbols, Tx_bits, options);
ffe_results = attachMpiReductionConfig(ffe_results, eq_ffe, storageName, ...
"a1_moving_average", "ff_dc_avg", block_update);
output.(char(storageName)) = ffe_results;
if plot_output_signals
plotEqSignals(equalized_signal,Symbols,options,430,-1);
end
end
%% DC tracking, adaptive/persistence path
if run_tracking_adaptive
eq_ffe = FFE_DCTracking( ...
"sps", eq_sps, ...
"order", eq_order, ...
"decide", false, ...
"adaption_technique", eq_adaption, ...
"len_tr", eq_len_tr, ...
"epochs_tr", eq_epochs_tr, ...
"mu_tr", eq_mu_tr, ...
"dd_mode", true, ...
"epochs_dd", eq_epochs_dd, ...
"mu_dd", 0.012, ...
"dc_tracking_mu", 0.002, ...
"dc_tracking_adaptive_enabled", true, ...
"dc_tracking_persistence_gain", 0, ...
"dc_tracking_buffer_len", block_update, ...
"optmize_mus", false, ...
"optimize_dc_tracking_params", false, ...
"dc_tracking_optimization_len", 2^15, ...
"dc_tracking_optimization_max_evals", 30, ...
"plot_mu_optimization", options.debug_plots, ...
"save_debug", eq_save_debug);
storageName = "dc_tracking";
[ffe_results,equalized_signal] = runFfe(eq_ffe, "DC tracking adaptive", ...
Scpe_sig, Symbols, Tx_bits, options);
ffe_results = attachMpiReductionConfig(ffe_results, eq_ffe, storageName, ...
"dc_tracking", "adaptive", block_update);
output.(char(storageName)) = ffe_results;
fignum = 106;
eq_noise = equalized_signal - Symbols;
dn = sprintf("FFE DCT; SIR: %d dB",options.dataTable.sir);
showEQNoisePSD(eq_noise, "fignum", fignum, "displayname", dn,"colormode","diverging");
ylim([-70 -30]);
%
% dn = sprintf("FFE only; SIR: %d dB",options.dataTable.sir);
% showEQNoisePSD(eq_noise_ffe, "fignum", fignum+1, "displayname", dn,"colormode","diverging");
% ylim([-70 -30]);
if plot_output_signals
plotEqSignals(equalized_signal,Symbols,options,450,-1);
end
end
end
function [ffe_results,equalized_signal] = runFfe(eq_ffe,description,Scpe_sig,Symbols,Tx_bits,options)
[ffe_results,equalized_signal] = ffe(eq_ffe, options.M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", options.duob_mode, ...
"showAnalysis", options.debug_plots, ...
"postFFE", [], ...
"eth_style_symbol_mapping", 0);
ffe_results.metrics.print("description",resultDescription(description,options));
end
function ffe_results = attachMpiReductionConfig(ffe_results, eq_ffe, storageName, ...
algorithm, algorithmVariant, block_update)
ffe_results.mpi_reduction_config = struct( ...
"storage_name", char(storageName), ...
"algorithm", char(algorithm), ...
"algorithm_variant", char(algorithmVariant), ...
"eq_class", class(eq_ffe), ...
"params", collectMpiReductionParams(eq_ffe, block_update));
end
function params = collectMpiReductionParams(eq_ffe, block_update)
params = struct();
params.block_update = block_update;
whitelistedProps = [ ...
"sps", ...
"order", ...
"decide", ...
"adaption_technique", ...
"len_tr", ...
"epochs_tr", ...
"mu_tr", ...
"dd_mode", ...
"epochs_dd", ...
"mu_dd", ...
"optmize_mus", ...
"plot_mu_optimization", ...
"save_debug", ...
"dc_smoothing_a1", ...
"dc_avg_bufferlength_a1", ...
"dc_avg_update_blocklength_a1", ...
"dc_smoothing_a2", ...
"dc_level_avg_bufferlength_a2", ...
"dc_level_update_blocklength_a2", ...
"dc_level_weights_a2", ...
"dc_tracking_mu", ...
"dc_tracking_adaptive_enabled", ...
"dc_tracking_persistence_gain", ...
"dc_tracking_buffer_len", ...
"dc_tracking_mu_eff_max", ...
"optimize_dc_tracking_params", ...
"dc_tracking_optimization_len", ...
"dc_tracking_optimization_max_evals", ...
"dc_tracking_optimization_delay_weight", ...
"dc_tracking_optimization_smoothing_len"];
for propIdx = 1:numel(whitelistedProps)
propName = char(whitelistedProps(propIdx));
if isprop(eq_ffe, propName)
params.(propName) = eq_ffe.(propName);
end
end
end
function description = resultDescription(prefix,options)
sir = options.dataTable.sir;
if numel(sir) > 1
sir = sir(1);
end
description = sprintf('%s; SIR %g dB',prefix,sir);
end
function plotEqSignals(equalized_signal,Symbols,options,fignum,output_scale)
showLevelScatter(equalized_signal .* output_scale, Symbols, ...
"fsym", options.fsym, ...
"fignum", fignum + 1, ...
"normalize", true);
end

View File

@@ -1,319 +0,0 @@
function [output] = dsp_runid(run_id, options)
arguments
run_id
options.append_to_db = 0;
options.max_occurences = 4;
options.parameters = struct();
options.database_type
options.dataBase
options.load_file_path = struct();
options.storage_path
options.mode
end
try
% Initialize output structures
output.ffe_package = {};
output.mlse_package = {};
output.vnle_package = {};
output.dbtgt_package = {};
output.dbenc_package = {};
output.mlmlse_package = {};
if options.mode == "load_run_id" || options.append_to_db
% Initialize database connection
database = DBHandler("dataBase", [options.dataBase], "type", options.database_type );
if 0
% 2. Check if an equalizer configuration with the same hash exists
queryStr = sprintf('SELECT COUNT(DISTINCT eq_id) AS unique_eq_count, COUNT(*) AS entries_for_run FROM `Results` WHERE run_id = %d', run_id);
existing_results = database.fetch(queryStr);
if existing_results.unique_eq_count >= 6
if (existing_results.entries_for_run / existing_results.unique_eq_count) > 5
return
end
end
end
end
if options.mode == "load_run_id"
dataTable = queryRunid(run_id, database);
fsym = dataTable.symbolrate;
M = double(dataTable.pam_level);
duob_mode = db_mode(strrep(dataTable.db_mode,'"',''));
% if database.checkIfRunExists('Results','run_id',run_id)
% disp(['Already got at least one reulst for run id: ',num2str(run_id),' '])
% return
% end
% Load and Sync signal data from DB
[Tx_bits, Symbols, Scpe_cell, ~] = loadAndSyncSignalDataFromDb(dataTable, options);
elseif options.mode == "load_files"
Tx_bits = load(options.load_file_path.tx_bits_path);
Symbols = load(options.load_file_path.tx_symbols_path);
Scpe_sig_raw = load(options.load_file_path.rx_raw_path);
Tx_bits = Tx_bits.Bits;
Symbols = Symbols.Symbols;
Scpe_sig_raw = Scpe_sig_raw.Scpe_sig_raw;
fsym = Symbols.fs;
M = Symbols.logbook.ModifierCopy{1}.M;
duob_mode = Symbols.logbook.ModifierCopy{1}.duobinary_mode;
Scpe_sig_resampled = Scpe_sig_raw.resample("fs_in", Scpe_sig_raw.fs, "fs_out", 2*fsym);
[~, Scpe_cell, ~, found_sync] = Scpe_sig_resampled.tsynch("reference", Symbols, "fs_ref", fsym, "debug_plots", 1);
else
% Run quick Simulation
tx_simulation;
end
% Handle Settings and argument replacement
len_tr = 4096*2;
ffe_order = [50, 5, 5];
dfe_order = [0, 0, 0];
pf_ncoeffs = 1;
mu_ffe = [0.0001, 0.0008, 0.001];
mu_dfe = 0.0004;
mu_dc = 0.005;
dc_buffer_len = 1;
mu_tr = 0;
mu_dd = 0.05;
adaption= 1;
use_dd_mode = 1;
use_ffe = 1;
use_dfe = 0;
use_vnle_mlse = 0;
use_dbtgt = 0;
use_dbenc = 0;
use_ml_mlse = 1;
addProcessingResultToDatabase = 0;
% Overwrite default parameters if given in options.parameters
paramStruct = options.parameters;
if ~isempty(paramStruct)
paramNames = fieldnames(paramStruct);
for i = 1:numel(paramNames)
thisName = paramNames{i};
thisValue = paramStruct.(thisName);
eval([thisName ' = thisValue;']);
end
end
% Configure equalizers
options.max_occurences = min(options.max_occurences,length(Scpe_cell));
for r = 1:options.max_occurences
%FFE
% eq_dfe = FFE("epochs_tr",5,"epochs_dd",2,"len_tr",2^13,"mu_dd",mu_dd,"mu_tr",mu_tr,"order",25,"sps",2,"decide",0, "adaption",adaption_method(adaption),"dd_mode",use_dd_mode);
%
pf_ = Postfilter("ncoeff",pf_ncoeffs,"useBurg",1);
% mlse_ = MLSE_viterbi("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels);
mlse_ = MLSE("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels);
eq_post = FFE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",1e-4,"mu_tr",0,"order",2001,"sps",1,"decide",0,"adaption_technique","lms");
eq_post = FFE("epochs_tr",5,"epochs_dd",2,"len_tr",2^13,"mu_dd",mu_dd,"mu_tr",mu_tr,"order",25,"sps",2,"decide",0, "adaption",adaption_method(adaption),"dd_mode",use_dd_mode);
% Duobinary signaling (db encoded)
mlse_db_enc = MLSE("DIR", [1,1], "duobinary_output", 0, "M", M, "trellis_states", PAMmapper(M,0).levels);
eq_db_enc = EQ("Ne", ffe_order, "Nb", dfe_order, "training_length", len_tr, ...
"training_loops", 5, "dd_loops", 5, "K", 2, "DCmu", mu_dc, ...
"DDmu", [mu_ffe mu_dfe], "DFEmu", 0.005, "FFEmu", 0, "plotfinal", 0, "ideal_dfe", 1);
% Preprocess signal
Scpe_sig = preprocessSignal(Scpe_cell{r}, Symbols, fsym);
Scpe_sig.spectrum("fignum",200,"normalizeTo0dB",1,"displayname",'Rx','addDCoffset',-6);
Scpe_sig.spectrum("fignum",201,"normalizeTo0dB",0,"displayname",'Rx');
ylim([-30,3]);
xlim([-5,100]);
% Scpe_sig.spectrum("fignum",22233,"normalizeTo0dB",0,"displayname",'Rx');
% Scpe_sig.eye(fsym,M,"fignum",1024);
Scpe_sig.signal = Scpe_sig.signal(1:2*Symbols.length);
Scpe_sig.signal = real(Scpe_sig.signal);
if duob_mode ~= db_mode.db_encoded
if use_ffe
ffe_order = [50, 0, 0];
eq_dfe = EQ("Ne",ffe_order,"Nb",[0,0,0],"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",0);
ffe_results = ffe(eq_dfe,M,Scpe_sig,Symbols,Tx_bits,...
"precode_mode",duob_mode,...
'showAnalysis',0,...
"postFFE",[],...
"eth_style_symbol_mapping",0);
output.ffe_package{r} = ffe_results;
ffe_results.metrics.print;
ffe_results.config.equalizer_structure = "ffe";
if options.append_to_db
database.addProcessingResult(run_id, ffe_results.metrics, ffe_results.config);
end
end
if use_dfe
ffe_order = [50, 5, 5];
eq_dfe = EQ("Ne",ffe_order,"Nb",[2,0,0],"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",0);
dfe_results = ffe(eq_dfe,M,Scpe_sig,Symbols,Tx_bits,...
"precode_mode",duob_mode,...
'showAnalysis',0,...
"postFFE",[],...
"eth_style_symbol_mapping",0);
output.ffe_package{r} = dfe_results;
dfe_results.config.equalizer_structure = "dfe";
dfe_results.metrics.print;
if options.append_to_db
database.addProcessingResult(run_id, dfe_results.metrics, dfe_results.config);
end
end
if use_vnle_mlse
pf_ncoeffs = 1;
ffe_order = [50, 5, 5];
eq_ = EQ("Ne",ffe_order,"Nb",dfe_order,"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",0);
% eq_ = VNLE("epochs_tr",5,"epochs_dd",5,"len_tr",4096*2,"mu_dd",[0.0004 0.0005 0.0006],"mu_tr",0.0004,"order",[50,5,5],"sps",2,"decide",0);
pf_ = Postfilter("ncoeff",pf_ncoeffs,"useBurg",1);
useviterbi = 0;
if useviterbi
mlse_ = MLSE_viterbi("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels);
else
if duob_mode == db_mode.no_db && M == 6 %only for PAM-6 and no duobinary precoding, otherwise leads to false sequence estimation
trellexlusion = 1;
else
trellexlusion = 0;
end
%state_mode 3 -> stat lvl; state_mode 2 -> use target lvls
%scale_mode 2 -> mmse adaption
mlse_ = MLSE("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels,'scale_mode',2,'trellis_exclusion',trellexlusion,'trellis_state_mode',2);
end
[ffe_results, mlse_results] = vnle_postfilter_mlse(eq_, pf_, mlse_, M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", duob_mode,...
'showAnalysis', 0, ...
"postFFE", [],...
"eth_style_symbol_mapping", 0);
ffe_results.metrics.print;
ffe_results.config.equalizer_structure = "vnle";
mlse_results.metrics.print;
output.mlse_package{r} = mlse_results;
output.vnle_package{r} = ffe_results;
if options.append_to_db
database.addProcessingResult(run_id, mlse_results.metrics, mlse_results.config);
database.addProcessingResult(run_id, ffe_results.metrics, ffe_results.config);
end
end
if use_ml_mlse
%ML-based MLSE (L=2)
mu_ml = 0.01; training_epochs = 100;
ml_mlse_equalizer = ML_MLSE("epochs_tr",training_epochs,"epochs_dd",1, ...
"len_tr",length(Scpe_sig)/4,"mu_dd",mu_ml,"mu_tr",mu_ml,"order",11,"sps",2, ...
"traceback_depth",128,"L",1,"delta",4,"adaptive_mu",0);
[ml_mlse_results] = ml_mlse(ml_mlse_equalizer, M, Scpe_sig, Symbols, Tx_bits,"precode_mode",duob_mode);
output.mlmlse_package{r} = ml_mlse_results;
if options.append_to_db
database.addProcessingResult(run_id, ml_mlse_results.metrics, ml_mlse_results.config);
end
end
if use_dbtgt
useviterbi = 0;
if useviterbi
mlse_db_ = MLSE_viterbi("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels);
else
if duob_mode == db_mode.no_db && M == 6 %only for PAM-6 and no duobinary precoding, otherwise leads to false sequence estimation
trellexlusion = 1;
else
trellexlusion = 0;
end
mlse_db_ = MLSE("DIR",[1,1],"duobinary_output",0,"M",M,"trellis_states",PAMmapper(M,0).levels,'scale_mode',2,'trellis_exclusion',trellexlusion,'trellis_state_mode',3);
end
ffe_order = [50, 5, 5];
eq_ = EQ("Ne",ffe_order,"Nb",dfe_order,"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",1);
dbt_results = duobinary_target(eq_, mlse_db_, M, Scpe_sig, Symbols, Tx_bits, ...
"precode_mode", duob_mode, ...
'showAnalysis', 0,...
"postFFE", []);
dbt_results.metrics.print;
output.dbtgt_package{r} = dbt_results;
if options.append_to_db
database.addProcessingResult(run_id, dbt_results.metrics, dbt_results.config);
end
end
end
if duob_mode == db_mode.db_encoded
mlse_db_enc = MLSE_viterbi("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels);
mlse_db_enc = MLSE("DIR", [1,1], "duobinary_output", 0, "M", M, "trellis_states", PAMmapper(M,0).levels);
db_results = duobinary_signaling(eq_db_enc, mlse_db_enc, M, Scpe_sig, Symbols, Tx_bits, "precode_mode",duob_mode, "showAnalysis",0,"postFFE",[]);
output.dbenc_package{r} = db_results;
if options.append_to_db
database.addProcessingResult(run_id, db_results.metrics, db_results.config);
end
end
end
catch ME
save('workerError.mat','ME');
rethrow(ME);
end
end

View File

@@ -1,153 +0,0 @@
function [db_results] = duobinary_target(eq_, mlse_,M, rx_signal, tx_symbols, tx_bits, options)
arguments
eq_
mlse_
M
rx_signal
tx_symbols
tx_bits
options.precode_mode db_mode
options.showAnalysis = 0;
options.eth_style_symbol_mapping = 0;
options.postFFE = [];
end
%Duobinary Targeting
db_ref_sequence = Duobinary().encode(tx_symbols);
db_ref_constellation = unique(db_ref_sequence.signal);
[eq_signal, eq_noise] = eq_.process(rx_signal,db_ref_sequence);
if ~isempty(options.postFFE)
[eq_signal,eq_noise] = options.postFFE.process(eq_signal,db_ref_sequence);
end
mlse_.DIR = [1,1];
%
if isa(mlse_,'MLSE_viterbi')
mlse_sig_sd = mlse_.process(eq_signal);
else
[mlse_sig_sd,LLR,GMI_MLSE] = mlse_.process(eq_signal,tx_symbols);
end
mlse_sig_hd = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).quantize(mlse_sig_sd);
% precoding to mitigate error propagation, most prominently used in
% combination with duobinary signaling to avoid catastrophic error
% behavior (see J.W.M. Bergmans, Digital Baseband Transmission and Recording -> partial response signaling)
switch options.precode_mode
case db_mode.no_db
% TX Data is not precoded:
% A) Emulate diff precoding
mlse_sig_hd_precoded = Duobinary().encode(mlse_sig_hd,"M",M);
mlse_sig_hd_precoded = Duobinary().decode(mlse_sig_hd_precoded,"M",M);
tx_symbols_precoded = Duobinary().encode(tx_symbols);
tx_symbols_precoded = Duobinary().decode(tx_symbols_precoded);
tx_bits_precoded = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(tx_symbols_precoded);
rx_bits_mlse = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(mlse_sig_hd_precoded);
[~,errors_db_diff_precoded,ber_db_diff_precoded,~] = calc_ber(rx_bits_mlse.signal,tx_bits_precoded.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1);
%B) Just determine BER
rx_bits_mlse = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(mlse_sig_hd);
[bits_mlse,errors_mlse,ber_db,~] = calc_ber(rx_bits_mlse.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1);
case db_mode.db_precoded
% Daten SIND TATSÄCHLICH precoded auf TX Seite:
% A) Decode at Rx if no DB targeting was applied (we are in VNLE or MLSE EQ structure here!
mlse_sig_hd_decoded = Duobinary().encode(mlse_sig_hd,"M",M);
mlse_sig_hd_decoded = Duobinary().decode(mlse_sig_hd_decoded,"M",M);
rx_bits_mlse_decoded = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(mlse_sig_hd_decoded);
[~,errors_db_diff_precoded,ber_db_diff_precoded,a] = calc_ber(rx_bits_mlse_decoded.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1);
burst_db_precoded = count_error_bursts(a, 40);
% B) Omit the Coding by comparing with demapped TX symbol sequence
tx_bits = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(tx_symbols);
rx_bits_mlse = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(mlse_sig_hd);
[bits_db,errors_db,ber_db,a] = calc_ber(rx_bits_mlse.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1);
burst_db = count_error_bursts(a, 40);
cols = linspecer(8);
figure();hold on;
stem(1:40,burst_db,'LineWidth',1,'Color',cols(4,:),'Marker','_','DisplayName','w/o diff. precoder');
stem(1:40,burst_db_precoded,'LineWidth',1,'Color',cols(3,:),'Marker','.','LineStyle','-','DisplayName','w diff. precoder');
xlabel('Bit Error Burst Length')
ylabel('Occurence')
set(gca, 'yscale', 'log');
end
% M = numel(unique(tx_symbols.signal));
rx_bits = PAMmapper(M,0,"eth_style",options.eth_style_symbol_mapping).demap(mlse_sig_hd);
[bits_db,errors_db,ber_db,errorIndice_db] = calc_ber(rx_bits.signal,tx_bits.signal,"skip_front",100,"skip_end",150,"returnErrorLocation",1);
alpha = arburg(eq_noise.signal,1);%pf_.coefficients(2);
alpha = alpha(2);
if isa(mlse_,'MLSE_viterbi')
gmi_mlse = NaN;
air_mlse = NaN;
else
gmi_mlse = GMI_MLSE;
air_mlse = tx_symbols.fs .* floor(log2(double(M))*10)/10 .* gmi_mlse ./ log2(double(M));
end
db_results = struct();
db_results.metrics = Metricstruct;
db_results.metrics.result_id = NaN;
db_results.metrics.run_id = NaN;
db_results.metrics.eqParam_id = NaN;
db_results.metrics.date_of_processing = datetime('now');
db_results.metrics.BER = ber_db;
db_results.metrics.numBits = bits_db;
db_results.metrics.numBitErr = errors_db;
db_results.metrics.BER_precoded = ber_db_diff_precoded;
db_results.metrics.numBitErr_precoded = errors_db_diff_precoded;
db_results.metrics.GMI = gmi_mlse;
db_results.metrics.AIR = air_mlse;
db_results.metrics.MLSE_dir = mlse_.DIR;
db_results.metrics.Alpha = alpha;
% Create DB results structure
db_results.config = Equalizerstruct();
eq_.e = [];
eq_.e2 = [];
eq_.e3 = [];
db_results.config.eq = jsonencode(eq_);
% mlse_.DIR = [];
db_results.config.mlse = jsonencode(mlse_);
db_results.config.equalizer_structure = int32(equalizer_structure.vnle_db_mlse);
db_results.config.comment = 'function: Duobinary tgt. (VNLE -> MLSE)';
if options.showAnalysis
eq_signal.eye(eq_signal.fs,M,"fignum",249);
eq_noise = eq_noise - mean(eq_noise.signal);
rx_signal.spectrum("normalizeTo0dB",1,"fignum",250,"displayname","Rx Spectrum");
Duobinary().encode(tx_symbols).spectrum("normalizeTo0dB",1,"fignum",10,"displayname","DB encoded reference");
showEQNoisePSD(eq_noise,"fignum",250,"displayname",'Duobinary Target Noise after Equalization');
fprintf('DB tgt BER: %.2e \n',ber_db);
figure(341); clf;
showLevelHistogram(eq_signal, db_ref_sequence, "fignum", 341);
end
end

View File

@@ -0,0 +1,320 @@
function exportLevelScatterTikz(sourceAx, pngFile, tikzFile, pngTikzPath, options)
%EXPORTLEVELSCATTERTIKZ Export showLevelScatter axes as PNG-backed TikZ.
% Rasterizes scatter objects into a PNG layer and writes average lines plus
% text annotations as PGFPlots objects.
arguments
sourceAx
pngFile (1,1) string
tikzFile (1,1) string
pngTikzPath (1,1) string
options.resolutionDpi (1,1) double {mustBePositive, mustBeInteger} = 150
options.xLabel (1,1) string = "Time in $\mu$s"
options.yLabel (1,1) string = "Normalized Amplitude"
options.generatedBy (1,1) string = "exportLevelScatterTikz.m"
end
outputDir = fileparts(pngFile);
if ~exist(outputDir, "dir")
mkdir(outputDir);
end
exportScatterLayerPng(sourceAx, pngFile, options.resolutionDpi);
linePlots = collectTikzLinePlots(sourceAx);
textAnnotations = collectTikzTextAnnotations(sourceAx);
writeTikzPngAxis(tikzFile, pngTikzPath, sourceAx, linePlots, textAnnotations, options);
end
function exportScatterLayerPng(sourceAx, pngFile, resolutionDpi)
sourceFig = ancestor(sourceAx, "figure");
xLimits = sourceAx.XLim;
yLimits = sourceAx.YLim;
scatterHandles = findGraphicsObjectsByType(sourceAx, "scatter");
exportFig = figure( ...
"Visible", "off", ...
"Color", "white", ...
"Units", sourceFig.Units, ...
"Position", sourceFig.Position);
cleanupFigure = onCleanup(@() close(exportFig));
exportAx = copyobj(sourceAx, exportFig);
exportAx.Units = "normalized";
exportAx.Position = [0 0 1 1];
exportAx.XLim = xLimits;
exportAx.YLim = yLimits;
exportAx.XLimMode = "manual";
exportAx.YLimMode = "manual";
stripAxesToScatterOnly(exportAx);
hideAxesInkForRasterExport(exportAx);
fprintf("Exporting scatter PNG with XLim=[%g %g], YLim=[%g %g], scatter objects=%d: %s\n", ...
xLimits(1), xLimits(2), yLimits(1), yLimits(2), numel(scatterHandles), pngFile);
drawnow;
exportFig.PaperPositionMode = "auto";
print(exportFig, char(pngFile), "-dpng", sprintf("-r%d", resolutionDpi));
end
function stripAxesToScatterOnly(ax)
plotObjects = findall(ax);
for objIdx = 1:numel(plotObjects)
obj = plotObjects(objIdx);
if obj == ax || ~isvalid(obj)
continue
end
objType = string(get(obj, "Type"));
if lower(objType) ~= "scatter"
delete(obj);
end
end
end
function hideAxesInkForRasterExport(ax)
ax.Visible = "on";
ax.Color = "white";
ax.Box = "off";
ax.XGrid = "off";
ax.YGrid = "off";
ax.XMinorGrid = "off";
ax.YMinorGrid = "off";
ax.XTick = [];
ax.YTick = [];
ax.XColor = "white";
ax.YColor = "white";
ax.Title.String = "";
ax.XLabel.String = "";
ax.YLabel.String = "";
end
function handles = findGraphicsObjectsByType(parentHandle, objectType)
allHandles = findall(parentHandle);
matches = false(size(allHandles));
for handleIdx = 1:numel(allHandles)
currentHandle = allHandles(handleIdx);
if ~isvalid(currentHandle)
continue
end
matches(handleIdx) = lower(string(get(currentHandle, "Type"))) == lower(string(objectType));
end
handles = allHandles(matches);
end
function linePlots = collectTikzLinePlots(sourceAx)
lineHandles = findGraphicsObjectsByType(sourceAx, "line");
lineHandles = flipud(lineHandles(:));
linePlots = repmat(struct( ...
"xData", [], ...
"yData", [], ...
"color", [], ...
"lineWidth", []), numel(lineHandles), 1);
validLineCount = 0;
for lineIdx = 1:numel(lineHandles)
lineHandle = lineHandles(lineIdx);
xData = lineHandle.XData(:);
yData = lineHandle.YData(:);
validSamples = isfinite(xData) & isfinite(yData);
if ~any(validSamples)
continue
end
validLineCount = validLineCount + 1;
linePlots(validLineCount).xData = xData(validSamples);
linePlots(validLineCount).yData = yData(validSamples);
linePlots(validLineCount).color = lineHandle.Color;
linePlots(validLineCount).lineWidth = lineHandle.LineWidth;
end
linePlots = linePlots(1:validLineCount);
end
function textAnnotations = collectTikzTextAnnotations(sourceAx)
textHandles = findGraphicsObjectsByType(sourceAx, "text");
textHandles = flipud(textHandles(:));
xLimits = sourceAx.XLim;
yLimits = sourceAx.YLim;
textAnnotations = repmat(struct( ...
"x", [], ...
"y", [], ...
"text", "", ...
"anchor", "center"), numel(textHandles), 1);
validTextCount = 0;
for textIdx = 1:numel(textHandles)
textHandle = textHandles(textIdx);
labelText = string(textHandle.String);
if strlength(strtrim(labelText)) == 0
continue
end
textPosition = textHandle.Position;
if textPosition(1) < xLimits(1) || textPosition(1) > xLimits(2) || ...
textPosition(2) < yLimits(1) || textPosition(2) > yLimits(2)
continue
end
validTextCount = validTextCount + 1;
textAnnotations(validTextCount).x = textPosition(1);
textAnnotations(validTextCount).y = textPosition(2);
textAnnotations(validTextCount).text = labelText;
textAnnotations(validTextCount).anchor = matlabTextAlignmentToTikzAnchor( ...
textHandle.HorizontalAlignment, textHandle.VerticalAlignment);
end
textAnnotations = textAnnotations(1:validTextCount);
end
function writeTikzPngAxis(tikzFile, pngTikzPath, sourceAx, linePlots, textAnnotations, options)
outputDir = fileparts(tikzFile);
if ~exist(outputDir, "dir")
mkdir(outputDir);
end
xLimits = sourceAx.XLim;
yLimits = sourceAx.YLim;
fid = fopen(tikzFile, "w");
if fid < 0
error("exportLevelScatterTikz:TikzOpenFailed", ...
"Could not open TikZ export file: %s", tikzFile);
end
cleanupFile = onCleanup(@() fclose(fid));
axisOptions = {
" every axis/.append style={font=\scriptsize},"
"width=\fwidth,"
"height=\fheight,"
"at={(0\fwidth,0\fheight)},"
"scale only axis,"
"axis on top,"
"xmin=" + formatPgfNumber(xLimits(1)) + ","
"xmax=" + formatPgfNumber(xLimits(2)) + ","
"xlabel style={font=\color{white!15!black}\small},"
"xlabel={" + options.xLabel + "},"
"ymin=" + formatPgfNumber(yLimits(1)) + ","
"ymax=" + formatPgfNumber(yLimits(2)) + ","
"ylabel style={font=\color{white!15!black}\small},"
"ylabel={" + options.yLabel + "},"
"axis background/.style={fill=white},"
"xmajorgrids,"
"ymajorgrids,"
"grid style={line width=0.4pt, dotted, color=black!20},"
"scaled ticks=false,"
"tick label style={/pgf/number format/fixed, /pgf/number format/1000 sep={}},"
"legend columns=1"
};
fprintf(fid, "%% This file was generated by %s.\n", options.generatedBy);
fprintf(fid, "%%\n");
for lineIdx = 1:numel(linePlots)
fprintf(fid, "%s\n", formatTikzColorDefinition(lineIdx, linePlots(lineIdx).color));
end
if ~isempty(linePlots)
fprintf(fid, "%%\n");
end
fprintf(fid, "%s\n\n", "\begin{tikzpicture}");
fprintf(fid, "%s\n", "\begin{axis}[%");
for optionIdx = 1:numel(axisOptions)
fprintf(fid, "%s\n", axisOptions{optionIdx});
end
fprintf(fid, "%s\n", "]");
graphicsLine = sprintf( ...
"\\addplot [forget plot] graphics [xmin=%s, xmax=%s, ymin=%s, ymax=%s] {%s};", ...
formatPgfNumber(xLimits(1)), ...
formatPgfNumber(xLimits(2)), ...
formatPgfNumber(yLimits(1)), ...
formatPgfNumber(yLimits(2)), ...
char(strrep(pngTikzPath, "\", "/")));
fprintf(fid, "%s\n\n", graphicsLine);
writeTikzLinePlots(fid, linePlots);
writeTikzTextAnnotations(fid, textAnnotations);
fprintf(fid, "%s\n\n", "\end{axis}");
fprintf(fid, "%s", "\end{tikzpicture}%");
end
function valueText = formatPgfNumber(value)
valueText = string(sprintf("%.15g", value));
end
function colorDefinition = formatTikzColorDefinition(colorIdx, rgbColor)
colorDefinition = sprintf( ...
"\\definecolor{mycolor%d}{rgb}{%.5f,%.5f,%.5f}%%", ...
colorIdx, rgbColor(1), rgbColor(2), rgbColor(3));
end
function writeTikzLinePlots(fid, linePlots)
for lineIdx = 1:numel(linePlots)
fprintf(fid, "\\addplot [color=mycolor%d, line width=%.1fpt, forget plot]\n", ...
lineIdx, linePlots(lineIdx).lineWidth);
fprintf(fid, " table[row sep=crcr]{%%\n");
for sampleIdx = 1:numel(linePlots(lineIdx).xData)
fprintf(fid, "%s\t%s\\\\\n", ...
formatPgfNumber(linePlots(lineIdx).xData(sampleIdx)), ...
formatPgfNumber(linePlots(lineIdx).yData(sampleIdx)));
end
fprintf(fid, "};\n\n");
end
end
function writeTikzTextAnnotations(fid, textAnnotations)
for textIdx = 1:numel(textAnnotations)
fprintf(fid, "\\node[font=\\scriptsize, anchor=%s, fill=white, draw=black!40, rounded corners=2pt, inner sep=2pt]\n", ...
textAnnotations(textIdx).anchor);
fprintf(fid, " at (axis cs:%s,%s) {%s};\n\n", ...
formatPgfNumber(textAnnotations(textIdx).x), ...
formatPgfNumber(textAnnotations(textIdx).y), ...
escapeTikzText(textAnnotations(textIdx).text));
end
end
function anchor = matlabTextAlignmentToTikzAnchor(horizontalAlignment, verticalAlignment)
horizontalAlignment = string(horizontalAlignment);
verticalAlignment = string(verticalAlignment);
if horizontalAlignment == "left"
horizontalAnchor = "west";
elseif horizontalAlignment == "right"
horizontalAnchor = "east";
else
horizontalAnchor = "";
end
if verticalAlignment == "top"
verticalAnchor = "north";
elseif verticalAlignment == "bottom"
verticalAnchor = "south";
else
verticalAnchor = "";
end
if strlength(horizontalAnchor) > 0 && strlength(verticalAnchor) > 0
anchor = verticalAnchor + " " + horizontalAnchor;
elseif strlength(horizontalAnchor) > 0
anchor = horizontalAnchor;
elseif strlength(verticalAnchor) > 0
anchor = verticalAnchor;
else
anchor = "center";
end
end
function textOut = escapeTikzText(textIn)
textOut = char(textIn);
% textOut = strrep(textOut, "\", "\textbackslash{}");
textOut = strrep(textOut, "{", "\{");
textOut = strrep(textOut, "}", "\}");
textOut = strrep(textOut, "%", "\%");
textOut = strrep(textOut, "&", "\&");
textOut = strrep(textOut, "#", "\#");
textOut = strrep(textOut, "_", "\_");
% textOut = strrep(textOut, "$", "\$");
end

View File

@@ -4,6 +4,12 @@ arguments
ref_symbols
options.fignum (1,1) double = NaN % Default to NaN if not provided
options.displayname (1,:) char = '' % Default to an empty string if not provided
options.color = [];
options.clear = true;
options.rasterize = false;
options.raster_resolution (1,1) double = 1000;
options.raster_markersize (1,1) double = 1;
options.raster_clim = [];
end
plot_shit = 1;
@@ -22,7 +28,9 @@ if plot_shit
fig = figure; % Create a new figure and get its handle
else
fig = figure(options.fignum); % Use the specified figure number
clf;
if options.clear
clf;
end
end
end
@@ -30,7 +38,9 @@ end
rx_symbols = eq_signal; %./ rms(eq_signal);
correct_symbols = ref_symbols;
col = cbrewer2('Paired',numel(unique(correct_symbols))*2);
col = cbrewer2('Paired',numel(unique(correct_symbols))*3);
useDefaultColormap = isempty(options.color);
scatterColor = options.color;
ccnt = -1;
levels = unique(correct_symbols);
@@ -66,20 +76,146 @@ D = sqrt(D_even.^2 + D_odd.^2);
[X,Y] = meshgrid(levels, levels);
[X_,Y_] = meshgrid(statistical_mean, statistical_mean);
plot_xlim = [floor(min(X_(:)))-1, ceil(max(X_(:)))+1];
plot_ylim = [floor(min(Y_(:)))-1, ceil(max(Y_(:)))+1];
hold on;
scatter(rx_even,rx_odd,5*ones(1,length(D)),D,'.','DisplayName',['Even/Odd PAM-',num2str(M)],'MarkerEdgeColor',col(2,:));
% colormap(gca,flip(cbrewer2('Spectral',100)))
colormap(gca,'hsv');
scatter(X_(:), Y_(:), 2, 'x', 'LineWidth', 10, 'MarkerEdgeColor', col(6,:),'DisplayName','Statistical Rx Levels');
scatter(X(:), Y(:), 2, 'x', 'LineWidth', 10, 'MarkerEdgeColor', col(4,:),'DisplayName','Tx Levels');
xlim([floor(min(X_(:)))-1, ceil(max(X_(:)))+1]);
ylim([floor(min(Y_(:)))-1, ceil(max(Y_(:)))+1]);
if isempty(options.displayname)
displayName = ['Even/Odd PAM-',num2str(M)];
else
displayName = options.displayname;
end
if useDefaultColormap
cols = flip(cbrewer2('RdYlBu',100));
cols=(cbrewer2("RdYlBu",4096));
% cols(45:55,:) = [];
colormap(gca,cols);
if isempty(options.raster_clim)
cLim = [0 max(D).*0.45];
else
cLim = options.raster_clim;
end
if cLim(2) <= cLim(1)
cLim(2) = cLim(1) + eps;
end
clim(cLim);
% colormap(gca,'hsv');
if options.rasterize
nBins = options.raster_resolution;
xEdges = linspace(plot_xlim(1),plot_xlim(2),nBins+1);
yEdges = linspace(plot_ylim(1),plot_ylim(2),nBins+1);
xBin = discretize(rx_even,xEdges);
yBin = discretize(rx_odd,yEdges);
valid = ~isnan(xBin) & ~isnan(yBin) & isfinite(D);
yticks((levels(1:end-1) + levels(2:end)) / 2);
xticks((levels(1:end-1) + levels(2:end)) / 2);
countImg = accumarray([yBin(valid), xBin(valid)],1,[nBins nBins],@sum,0);
dImg = accumarray([yBin(valid), xBin(valid)],D(valid),[nBins nBins],@mean,NaN);
countImgRaw = countImg;
markerRadius = max(0, round((options.raster_markersize - 1) / 2));
if markerRadius > 0
kernel = ones(2*markerRadius+1);
countImg = conv2(countImg,kernel,'same');
weightedD = dImg;
weightedD(~isfinite(weightedD)) = 0;
weightedD = conv2(weightedD .* countImgRaw,kernel,'same');
dWeight = conv2(isfinite(dImg) .* countImgRaw,kernel,'same');
dImg = weightedD ./ dWeight;
end
colorIdx = round((dImg-cLim(1)) ./ diff(cLim) * (size(cols,1)-1)) + 1;
colorIdx = min(max(colorIdx,1),size(cols,1));
density = log1p(countImg);
if max(density(:)) > 0
density = density ./ max(density(:));
end
rgbImg = ones(nBins,nBins,3);
for rgbIdx = 1:3
colorPlane = reshape(cols(colorIdx,rgbIdx),nBins,nBins);
colorPlane(~isfinite(dImg)) = 1;
rgbImg(:,:,rgbIdx) = (1-density) + density .* colorPlane;
end
hImg = image(linspace(plot_xlim(1),plot_xlim(2),nBins), ...
linspace(plot_ylim(1),plot_ylim(2),nBins), ...
rgbImg);
hImg.HandleVisibility = 'off';
set(gca,'YDir','normal');
uistack(hImg,'bottom');
plot(nan,nan,'.','DisplayName',displayName,'MarkerEdgeColor',col(2,:));
else
sz = ones(1,length(D)).*1;
scatter(rx_even,rx_odd,sz,D,'.','DisplayName',displayName);
end
rxLevelColor = col(6,:);
else
if options.rasterize
nBins = options.raster_resolution;
xEdges = linspace(plot_xlim(1),plot_xlim(2),nBins+1);
yEdges = linspace(plot_ylim(1),plot_ylim(2),nBins+1);
xBin = discretize(rx_even,xEdges);
yBin = discretize(rx_odd,yEdges);
valid = ~isnan(xBin) & ~isnan(yBin);
countImg = accumarray([yBin(valid), xBin(valid)],1,[nBins nBins],@sum,0);
markerRadius = max(0, round((options.raster_markersize - 1) / 2));
if markerRadius > 0
kernel = ones(2*markerRadius+1);
countImg = conv2(countImg,kernel,'same');
end
density = log1p(countImg);
if max(density(:)) > 0
density = density ./ max(density(:));
end
rgbImg = ones(nBins,nBins,3);
for rgbIdx = 1:3
rgbImg(:,:,rgbIdx) = (1-density) + density .* scatterColor(rgbIdx);
end
hImg = image(linspace(plot_xlim(1),plot_xlim(2),nBins), ...
linspace(plot_ylim(1),plot_ylim(2),nBins), ...
rgbImg);
hImg.HandleVisibility = 'off';
set(gca,'YDir','normal');
uistack(hImg,'bottom');
plot(nan,nan,'.','DisplayName',displayName,'MarkerEdgeColor',scatterColor);
else
scatter(rx_even,rx_odd,5*ones(1,length(D)),'.','DisplayName',displayName,'MarkerEdgeColor',scatterColor);
end
rxLevelColor = scatterColor;
end
scatter(X_(:), Y_(:), 2, 'x', 'LineWidth', 10, 'MarkerEdgeColor', rxLevelColor,'DisplayName',[displayName, ' Rx Levels']);
scatter(X(:), Y(:), 2, 'x', 'LineWidth', 10, 'MarkerEdgeColor', col(4,:),'DisplayName','Tx Levels');
xlim(plot_xlim);
ylim(plot_ylim);
decisionBoundaries = (levels(1:end-1) + levels(2:end)) / 2;
ax = gca;
xticks(levels);
yticks(levels);
xticklabels(compose('%g',levels));
yticklabels(compose('%g',levels));
try
ax.XAxis.MinorTickValues = decisionBoundaries;
ax.YAxis.MinorTickValues = decisionBoundaries;
end
ax.XMinorTick = 'on';
ax.YMinorTick = 'on';
ax.XMinorGrid = 'on';
ax.YMinorGrid = 'on';
ax.GridLineStyle = '--';
ax.MinorGridLineStyle = ':';
ax.GridColor = [0.65 0.65 0.65];
ax.MinorGridColor = [0.35 0.35 0.35];
ax.GridAlpha = 0.35;
ax.MinorGridAlpha = 0.45;
legend
xlabel('even symbols');
ylabel('odd symbols');
axis equal; grid on;
axis equal;
grid on;
grid minor;
ax.Layer = 'top';
end

View File

@@ -4,9 +4,15 @@ arguments
options.postfilter_taps = NaN
options.fignum (1,1) double = NaN % Default to NaN if not provided
options.displayname (1,:) char = '' % Default to an empty string if not provided
options.color = [0.2157 0.4941 0.7216];
options.color = [];
options.colormode (1,1) string {mustBeMember(options.colormode, ["diverging", "qualitative", "paired"])} = "diverging";
end
persistent nextGroupIdx
if isempty(nextGroupIdx)
nextGroupIdx = 0;
end
% Determine the figure number to use or create a new figure
if isnan(options.fignum)
fig = figure; % Create a new figure and get its handle
@@ -15,13 +21,16 @@ end
end
hold on
ax = gca;
% N = numel(ax.Children);
N = sum(arrayfun(@(x) strcmp(x.LineStyle, '-'), ax.Children));
cmap = linspecer(8);
options.color = cmap(mod(N, size(cmap, 1)) + 1, :);
linesBefore = findall(ax, 'Type', 'Line');
useAutomaticColor = isempty(options.color);
if useAutomaticColor
options.color = selectEQNoisePSDColor(ax, options.colormode);
end
% Ensure the figure is ready before calling spectrum
eq_noise.spectrum("displayname", options.displayname, "fignum", fig.Number, "normalizeTo0dB", 1,"color",options.color);
eq_noise = eq_noise - mean(eq_noise.signal);
eq_noise.spectrum("displayname", options.displayname, "fignum", fig.Number, "normalizeTo0dB", 0,"color",options.color,"fft_length",4096);
if ~isnan(options.postfilter_taps)
% Hold on to the figure for further plotting
@@ -41,7 +50,88 @@ end
legend('show');
end
if useAutomaticColor
nextGroupIdx = nextGroupIdx + 1;
newLines = getNewLines(ax, linesBefore);
tagEQNoisePSDLines(newLines, nextGroupIdx);
recolorEQNoisePSDLines(ax, options.colormode);
end
xlim([-eq_noise.fs/2* 1e-9 eq_noise.fs/2* 1e-9]);
% ylim([-15, 0]);
end
function color = selectEQNoisePSDColor(ax, colormode)
if colormode == "qualitative"
N = sum(arrayfun(@(x) strcmp(x.LineStyle, '-'), ax.Children));
cmap = linspecer(8);
color = cmap(mod(N, size(cmap, 1)) + 1, :);
else
color = [0 0 0];
end
end
function newLines = getNewLines(ax, linesBefore)
linesAfter = findall(ax, 'Type', 'Line');
newLines = linesAfter(~ismember(linesAfter, linesBefore));
end
function tagEQNoisePSDLines(lines, groupIdx)
for lineIdx = 1:numel(lines)
setappdata(lines(lineIdx), 'showEQNoisePSDGroupIdx', groupIdx);
end
end
function recolorEQNoisePSDLines(ax, colormode)
if colormode == "qualitative"
return
end
lines = findall(ax, 'Type', 'Line');
groupIdx = NaN(size(lines));
for lineIdx = 1:numel(lines)
if isappdata(lines(lineIdx), 'showEQNoisePSDGroupIdx')
groupIdx(lineIdx) = getappdata(lines(lineIdx), 'showEQNoisePSDGroupIdx');
end
end
lines = lines(~isnan(groupIdx));
groupIdx = groupIdx(~isnan(groupIdx));
if isempty(lines)
return
end
groups = unique(groupIdx, 'stable');
groups = sort(groups);
cols = getEQNoisePSDColormap(colormode, numel(groups));
for groupColorIdx = 1:numel(groups)
sameGroup = groupIdx == groups(groupColorIdx);
set(lines(sameGroup), 'Color', cols(groupColorIdx, :));
end
end
function cols = getEQNoisePSDColormap(colormode, numColors)
switch colormode
case "paired"
cols = cbrewer2('paired', numColors);
otherwise
cols = getDivergingWithoutBrightCenter(numColors);
end
end
function cols = getDivergingWithoutBrightCenter(numColors)
numBaseColors = max(6, numColors);
centerExcludeFraction = 0.3;
baseCols = cbrewer2('RdYlBu', numBaseColors);
centerIdx = (numBaseColors + 1) / 2;
excludeHalfWidth = centerExcludeFraction * numBaseColors / 2;
keepIdx = find(abs((1:numBaseColors) - centerIdx) > excludeHalfWidth);
sampleIdx = round(linspace(1, numel(keepIdx), numColors));
cols = baseCols(keepIdx(sampleIdx), :);
end

View File

@@ -6,7 +6,8 @@ arguments
options.fs_rx
options.fignum (1,1) double = NaN % Default to NaN if not provided
options.displayname (1,:) char = '' % Default to an empty string if not provided
options.color = [0.2157 0.4941 0.7216];
options.color = [];
options.linestyle = '-';
end
% Determine the figure number to use or create a new figure
@@ -30,42 +31,52 @@ end
ax = gca;
% N = numel(ax.Children);
N = sum(arrayfun(@(x) strcmp(x.LineStyle, '-'), ax.Children));
cmap = linspecer(8);
options.color = cmap(mod(N, size(cmap, 1)) + 1, :);
% Ensure the figure is ready before calling spectrum
title('SNR of received Signal')
if isempty(options.color)
N = sum(arrayfun(@(x) strcmp(x.LineStyle, '-'), ax.Children));
cmap = linspecer(8);
options.color = cmap(mod(N, size(cmap, 1)) + 1, :);
end
fft_length = 2^(nextpow2(length(eq_signal))-7);
[s_lin,w] = pwelch(eq_signal,hanning(fft_length),fft_length/2,fft_length,options.fs_tx,"centered","psd","mean");
[n_lin,w] = pwelch(noise_signal,hanning(fft_length),fft_length/2,fft_length,options.fs_rx,"centered","psd","mean");
[n_lin,w_noise] = pwelch(noise_signal,hanning(fft_length),fft_length/2,fft_length,options.fs_rx,"centered","psd","mean");
if numel(w_noise) ~= numel(w) || any(abs(w_noise - w) > max(options.fs_tx,options.fs_rx)*eps)
n_lin = interp1(w_noise,n_lin,w,"linear",NaN);
end
w = w.*1e-9;
snr_dbm = 10*log10(s_lin./n_lin);
snr_lin = s_lin./n_lin;
snr_lin = movmean(snr_lin,10);
snr_dbm = 10*log10(snr_lin);
% figure(231)
hold on
plot(w,snr_dbm,'DisplayName','SNR','LineWidth',0.5,'Color',options.color);
yline(mean(snr_dbm),'HandleVisibility','off','Color',options.color);
if isempty(options.displayname)
options.displayname = 'SNR';
end
plot(w,snr_dbm,'DisplayName',options.displayname,'LineWidth',1,'Color',options.color,'LineStyle',options.linestyle);
% yline(mean(snr_dbm),'HandleVisibility','off','Color',options.color,'LineStyle','--','LineWidth',1);
xlabel("Frequency in GHz");
edgetick = 2^(nextpow2(options.fs_tx*1e-9));
ticks = -edgetick:16:edgetick;
xticks(ticks);
[~,b]=min(abs((-edgetick:16:edgetick)-max(w)));
xlim([-ticks(b+1) ticks(b+1)]);
ylabel("SNR (dB)");
xlim([min(w) max(w)]);
max_snr = ceil(max(snr_dbm)/10)*10;
min_snr = floor(min(snr_dbm)/10)*10;
ylim([min_snr,max_snr]);
yticks(-200:10:100);
y_min = min(snr_dbm(:));
y_max = max(snr_dbm(:));
y_range = y_max - y_min;
if y_range == 0
y_range = 10;
end
y_margin = 0.05 * y_range;
ylim([y_min - y_margin, y_max + y_margin]);
try
yticks(round(linspace(y_min, y_max, min(10, max(4, ceil(y_range/10))))));
end
grid on; grid minor;
legend('Interpreter','none');
title('Noise of soft decision signal (not MLSE)');
grid on;
if isempty(get(gca, 'Legend'))
legend('Interpreter','none');
end
end

View File

@@ -6,6 +6,7 @@ arguments
fs
options.fignum (1,1) double = NaN % Default to NaN if not provided
options.displayname (1,:) char = '' % Default to an empty string if not provided
options.color = [];
end
% Assuming that obj.e contains the final FFE filter coefficients.
@@ -34,20 +35,24 @@ end
end
% Magnitude response (in dB)
subplot(2,1,1);
% subplot(2,1,1);
hold on
plot(f.*1e-9, H_db,'DisplayName',options.displayname);
if isempty(options.color)
plot(f.*1e-9, H_db,'DisplayName',options.displayname,'LineWidth',1);
else
plot(f.*1e-9, H_db,'DisplayName',options.displayname,'Color',options.color,'LineWidth',1);
end
title('(Inverted) Magnitude Response of FFE Filter');
xlabel('Frequency (GHz)');
ylabel('Magnitude (dB)');
grid on;
% Phase response
subplot(2,1,2);
plot(f.*1e-9, unwrap(angle(H)),'DisplayName',options.displayname);
title('Phase Response of FFE Filter');
xlabel('Frequency (GHz)');
ylabel('Phase');
grid on;
% % Phase response
% subplot(2,1,2);
% plot(f.*1e-9, unwrap(angle(H)),'DisplayName',options.displayname);
% title('Phase Response of FFE Filter');
% xlabel('Frequency (GHz)');
% ylabel('Phase');
% grid on;
end

View File

@@ -4,6 +4,7 @@ arguments
ref_symbols
options.fignum (1,1) double = NaN % Default to NaN if not provided
options.displayname (1,:) char = '' % Default to an empty string if not provided
options.max_burst_length (1,1) double = 20
end
% Determine the figure number to use or create a new figure
@@ -18,30 +19,17 @@ end
eq_signal = PAMmapper(M,0).quantize(eq_signal);
end
diff_indices = eq_signal.signal == ref_symbols.signal;
errpos = find(eq_signal.signal(:) ~= ref_symbols.signal(:));
burst_len = 1:options.max_burst_length;
burst_count = count_error_bursts(errpos, options.max_burst_length);
% Identify the start of new sequences (when the difference is not 1)
sequence_starts = [1, find(diff_indices ~= 1) + 1]; % Include the first index
sequence_ends = [sequence_starts(2:end) - 1, length(errpos)]; % Calculate end indices
% Initialize burst count and print bursts longer than 10
burst_len = 1:10;
burst_count = zeros(length(burst_len),1);
for t = 1:numel(burst_len)
for i = 1:length(sequence_starts)
% Extract current sequence
current_burst = errpos(sequence_starts(i):sequence_ends(i));
% Check if the sequence length matches criterion
if length(current_burst) == burst_len(t)
burst_count(t) = burst_count(t) + 1;
end
end
bar(burst_len, burst_count, "DisplayName", options.displayname);
grid on;
xlabel("Error burst length");
ylabel("Count");
title("Error burst count");
if ~isempty(options.displayname)
legend("show");
end
burst_symbols = burst_count .* burst_len';
burst_rate = burst_symbols ;%./ length(rx_signal);
end
end

View File

@@ -4,6 +4,8 @@ arguments
ref_symbols
options.fignum (1,1) double = NaN % Default to NaN if not provided
options.displayname (1,:) char = '' % Default to an empty string if not provided
options.ref_symbol_uncoded = []
options.nbins (1,1) double = 1000
end
if isa(eq_signal,'Signal')
@@ -12,6 +14,20 @@ end
if isa(ref_symbols,'Signal')
ref_symbols = ref_symbols.signal;
end
if isa(options.ref_symbol_uncoded,'Signal')
options.ref_symbol_uncoded = options.ref_symbol_uncoded.signal;
end
eq_signal = eq_signal(:);
ref_symbols = ref_symbols(:);
ref_symbol_uncoded = options.ref_symbol_uncoded(:);
assert(numel(eq_signal) == numel(ref_symbols), ...
'showLevelHistogram:LengthMismatch', ...
'eq_signal and ref_symbols must have the same number of samples.');
assert(isempty(ref_symbol_uncoded) || numel(ref_symbol_uncoded) == numel(eq_signal), ...
'showLevelHistogram:LengthMismatch', ...
'options.ref_symbol_uncoded must have the same number of samples as eq_signal.');
% Determine the figure number to use or create a new figure
if isnan(options.fignum)
@@ -22,31 +38,89 @@ end
eq_signal = max(min(eq_signal,3),-3);
%%% Separate Classes
constellation = unique(ref_symbols);
received_sd = NaN(numel(constellation),length(ref_symbols));
lvlcol = cbrewer2('Paired',numel(constellation)*2);
lvlcol = lvlcol(2:2:end,:);
lvlcol = linspecer(numel(constellation));
% lvlcol = cbrewer2('Set1',numel(constellation));
for lvl = 1:numel(constellation)
%Separate the equalized signal into the
%respective levels based on the actually
%transmitted level!
received_sd(lvl,ref_symbols==constellation(lvl)) = eq_signal(ref_symbols==constellation(lvl));
end
%%% FFE histogram
eq_signal = eq_signal .* -1;
%%% histogram
clf
for lvl = 1:numel(constellation)
intermediate = received_sd(lvl,:);
cnt(lvl) = round(numel(intermediate(~isnan(intermediate)))./length(eq_signal),3).*100;
hold on
warning off
histogram(received_sd(lvl,:),1000,"EdgeAlpha",0,'DisplayName',['Lvl ',num2str(lvl),' ; ',num2str(cnt(lvl)),' '],'FaceColor',lvlcol(lvl,:),'Normalization','pdf');
warning on
if isempty(ref_symbol_uncoded)
% Normal mode: split the received signal by the actually
% transmitted reference level.
constellation = unique(ref_symbols);
received_sd = NaN(numel(constellation),numel(ref_symbols));
lvlcol = linspecer(numel(constellation));
lvlcol = cbrewer2('RdBu',numel(constellation)+4);
% remove 4 colors from the middle to avoid the bright central colors
mid = ceil(size(lvlcol,1)/2);
rm_idx = mid + (-1:2); % two before mid and two after (2x2 removal centered)
rm_idx = max(1,min(size(lvlcol,1),rm_idx));
lvlcol(rm_idx,:) = [];
lvlcol = lvlcol(1:numel(constellation),:);
for lvl = 1:numel(constellation)
class_mask = ref_symbols == constellation(lvl);
received_sd(lvl,class_mask) = eq_signal(class_mask);
end
for lvl = 1:numel(constellation)
intermediate = received_sd(lvl,:);
cnt(lvl) = round(numel(intermediate(~isnan(intermediate)))./numel(eq_signal),3).*100;
hold on
warning off
histogram(received_sd(lvl,:),options.nbins, ...
"EdgeAlpha",0, ...
"DisplayName",['Lvl ',num2str(lvl),' ; ',num2str(cnt(lvl)),' '], ...
"FaceColor",lvlcol(lvl,:), ...
"Normalization","pdf");
warning on
end
else
% Plot p(y | x_n): y is the noisy observation and x_n is the
% uncoded PAM class. For duobinary this naturally gives multi-modal
% PDFs because one x_n class can map to multiple DB amplitudes. Each
% DB lobe is drawn separately so the DB-level structure stays visible.
db_constellation = unique(ref_symbols);
classes = unique(ref_symbol_uncoded);
% lvlcol = linspecer(numel(classes));
lvlcol = cbrewer2('RdBu',numel(classes)+4);
% remove 4 colors from the middle to avoid the bright central colors
mid = ceil(size(lvlcol,1)/2);
rm_idx = mid + (-1:2); % two before mid and two after (2x2 removal centered)
rm_idx = max(1,min(size(lvlcol,1),rm_idx));
lvlcol(rm_idx,:) = [];
lvlcol = lvlcol(1:numel(sir_group_labels),:);
for db_lvl = 1:numel(db_constellation)
db_mask = ref_symbols == db_constellation(db_lvl);
mapped_class = mode(ref_symbol_uncoded(db_mask));
[~,class_idx] = min(abs(classes - mapped_class));
cnt = round(nnz(ref_symbol_uncoded == mapped_class)./numel(eq_signal),3).*100;
if db_lvl == findFirstMappedDbLevel(ref_symbols,ref_symbol_uncoded,db_constellation,mapped_class)
display_name = ['p(y|x_',num2str(class_idx),') ; ',num2str(cnt),' '];
display_name = ['p(y|x_',num2str(class_idx),')'];
handle_visibility = "on";
else
display_name = ['p(y|x_',num2str(class_idx),') ; ',num2str(cnt),' '];
display_name = ['p(y|x_',num2str(class_idx),')'];
handle_visibility = "off";
end
weighted_lobe = NaN(size(eq_signal));
weighted_lobe(db_mask) = eq_signal(db_mask);
hold on
warning off
histogram(weighted_lobe,options.nbins, ...
"EdgeAlpha",0, ...
"DisplayName",display_name, ...
"FaceColor",lvlcol(class_idx,:), ...
"HandleVisibility",handle_visibility, ...
"Normalization","pdf");
warning on
end
end
xlim([-3 3]);
legend
@@ -58,3 +132,15 @@ end
end
function first_db_lvl = findFirstMappedDbLevel(ref_symbols,ref_symbol_uncoded,db_constellation,mapped_class)
first_db_lvl = NaN;
for db_lvl = 1:numel(db_constellation)
db_mask = ref_symbols == db_constellation(db_lvl);
if mode(ref_symbol_uncoded(db_mask)) == mapped_class
first_db_lvl = db_lvl;
return
end
end
end

View File

@@ -1,133 +1,420 @@
function [symbols_for_lvl,avg_for_lvl] = showLevelScatter(eq_signal,ref_symbols,options)
function [symbols_for_lvl, avg_for_lvl, xAxisUs, info] = showLevelScatter(rxInput, refSymbols, options)
%SHOWLEVELSCATTER Plot received samples separated by reference PAM level.
% Supports plain numeric vectors, Signal objects, synchronized scope cell
% arrays, and raw unsynchronized Signal input. Raw Signal input is
% synchronized to refSymbols and stitched before plotting.
arguments
eq_signal
ref_symbols
options.fignum (1,1) double = NaN % Default to NaN if not provided
options.displayname (1,:) char = '' % Default to an empty string if not provided
options.f_sym =1e6;
rxInput
refSymbols
options.fignum (1,1) double = NaN
options.displayname (1,:) char = ''
options.f_sym double = []
options.fsym double = []
options.syncFs (1,1) double = 0
options.shiftFs (1,1) double = 0
options.shifts double = []
options.maxOccurences (1,1) double = Inf
options.normalize (1,1) logical = false
options.debug_plots (1,1) logical = false
options.showPlot (1,1) logical = true
options.clear (1,1) logical = true
options.windowLength (1,1) double {mustBePositive, mustBeInteger} = 500
options.xLimits double = []
options.yLimits (1,2) double = [-2.5 2.5]
options.showStdAnnotations (1,1) logical = true
options.scatterAlpha (1,1) double {mustBeGreaterThanOrEqual(options.scatterAlpha, 0), mustBeLessThanOrEqual(options.scatterAlpha, 1)} = 0.2
options.scatterSize (1,1) double {mustBePositive} = 1
options.avgLineMaxPoints (1,1) double {mustBePositive, mustBeInteger} = 1000
options.avgLineSmoothWindow (1,1) double {mustBePositive, mustBeInteger} = 1
end
plot_shit = 1;
fsym = resolveSymbolRate(rxInput, refSymbols, options);
[symbols_for_lvl, avg_for_lvl, xAxisUs, info] = prepareLevelScatterData(rxInput, refSymbols, fsym, options);
if isa(eq_signal,'Signal')
options.f_sym = eq_signal.fs;
eq_signal = eq_signal.signal;
assert(~isempty(options.f_sym),'No fsym given');
if options.showPlot
info.plot = plotLevelScatter(symbols_for_lvl, avg_for_lvl, refSymbols, xAxisUs, options);
end
if isa(ref_symbols,'Signal')
ref_symbols = ref_symbols.signal;
end
function fsym = resolveSymbolRate(rxInput, refSymbols, options)
if ~isempty(options.fsym)
fsym = options.fsym;
elseif ~isempty(options.f_sym)
fsym = options.f_sym;
elseif isa(refSymbols, "Signal") && ~isempty(refSymbols.fs)
fsym = refSymbols.fs;
elseif isa(rxInput, "Signal") && ~isempty(rxInput.fs)
fsym = rxInput.fs;
elseif iscell(rxInput) && ~isempty(rxInput) && isa(rxInput{1}, "Signal") && ~isempty(rxInput{1}.fs)
fsym = rxInput{1}.fs;
else
fsym = 1e6;
end
end
if plot_shit
% Determine the figure number to use or create a new figure
if isnan(options.fignum)
fig = figure; % Create a new figure and get its handle
function [symbols_for_lvl, avg_for_lvl, xAxisUs, info] = prepareLevelScatterData(rxInput, refSymbols, fsym, options)
refSignal = numericSignal(refSymbols);
info = defaultInfo(fsym);
if iscell(rxInput)
[symbols_for_lvl, avg_for_lvl, info] = prepareCellInput(rxInput, refSymbols, fsym, options, info);
elseif isa(rxInput, "Signal")
[symbols_for_lvl, avg_for_lvl, info] = prepareSignalInput(rxInput, refSymbols, fsym, options, info);
else
rxSymbols = numericSignal(rxInput);
[symbols_for_lvl, avg_for_lvl] = levelScatterForOneSequence(rxSymbols, refSignal, options.windowLength);
end
xAxisUs = ((1:size(avg_for_lvl, 2)) / fsym) * 1e6;
end
function info = defaultInfo(fsym)
info = struct();
info.found_sync = true;
info.startSamples = 1;
info.shifts = [];
info.fsym = fsym;
info.shiftFs = fsym;
info.varianceByLevel = [];
info.plot = struct();
end
function [symbols_for_lvl, avg_for_lvl, info] = prepareSignalInput(rxSignal, refSymbols, fsym, options, info)
rxAtSymbolRate = rxSignal.resample("fs_in", rxSignal.fs, "fs_out", fsym);
refSignal = numericSignal(refSymbols);
if numel(rxAtSymbolRate.signal) == numel(refSignal)
rxSymbols = rxAtSymbolRate.signal;
if options.normalize
rxSymbols = normalizeNumericRms(rxSymbols);
end
[symbols_for_lvl, avg_for_lvl] = levelScatterForOneSequence(rxSymbols, refSignal, options.windowLength);
info.varianceByLevel = var(symbols_for_lvl, 0, 2, "omitnan");
return
end
[scopeCell, shifts, shiftFs, foundSync] = synchronizeRawSignal(rxSignal, refSymbols, fsym, options);
info.found_sync = foundSync;
info.shifts = shifts;
info.shiftFs = shiftFs;
if isempty(scopeCell)
symbols_for_lvl = [];
avg_for_lvl = [];
warning("showLevelScatter:NoScopeCells", ...
"No synchronized scope signal occurrences available.");
return
end
[symbols_for_lvl, avg_for_lvl, startSamples] = stitchScopeCells(scopeCell, refSymbols, fsym, shifts, shiftFs, options);
info.startSamples = startSamples;
info.varianceByLevel = var(symbols_for_lvl, 0, 2, "omitnan");
end
function [symbols_for_lvl, avg_for_lvl, info] = prepareCellInput(scopeCell, refSymbols, fsym, options, info)
scopeCell = scopeCell(:);
if isempty(scopeCell)
symbols_for_lvl = [];
avg_for_lvl = [];
info.found_sync = false;
warning("showLevelScatter:NoScopeCells", ...
"No synchronized scope signal occurrences available.");
return
end
shiftFs = options.shiftFs;
if shiftFs <= 0
shiftFs = fsym;
end
[symbols_for_lvl, avg_for_lvl, startSamples] = stitchScopeCells(scopeCell, refSymbols, fsym, options.shifts, shiftFs, options);
info.found_sync = true;
info.shifts = options.shifts;
info.shiftFs = shiftFs;
info.startSamples = startSamples;
info.varianceByLevel = var(symbols_for_lvl, 0, 2, "omitnan");
end
function [scopeCell, shifts, shiftFs, foundSync] = synchronizeRawSignal(rxSignal, refSymbols, fsym, options)
syncFs = options.syncFs;
if syncFs <= 0
syncFs = 2*fsym;
end
syncSignal = rxSignal.resample("fs_in", rxSignal.fs, "fs_out", syncFs);
if options.normalize
syncSignal = syncSignal.normalize("mode", "rms");
end
[~, scopeCell, ~, foundSync, shifts] = syncSignal.tsynch( ...
"reference", refSymbols, ...
"fs_ref", fsym, ...
"debug_plots", options.debug_plots);
if options.shiftFs > 0
shiftFs = options.shiftFs;
else
shiftFs = syncFs;
end
end
function [symbols_for_lvl, avg_for_lvl, startSamples] = stitchScopeCells(scopeCell, refSymbols, fsym, shifts, shiftFs, options)
recordOccurrences = min(numel(scopeCell), options.maxOccurences);
scopeCell = scopeCell(1:recordOccurrences);
refSignal = numericSignal(refSymbols);
startSamples = getStartSamples(shifts, recordOccurrences, shiftFs, fsym, numel(refSignal));
levelScatter = cell(1, recordOccurrences);
levelAverage = cell(1, recordOccurrences);
for occurrenceIdx = 1:recordOccurrences
occurrence = scopeCell{occurrenceIdx};
if isa(occurrence, "Signal")
occurrence = occurrence.resample("fs_out", fsym);
occurrence = occurrence.signal;
end
[levelScatter{occurrenceIdx}, levelAverage{occurrenceIdx}] = ...
levelScatterForOneSequence(occurrence, refSignal, options.windowLength);
end
numLevels = size(levelScatter{1}, 1);
traceLength = max(startSamples(:).' + cellfun(@(x) size(x, 2), levelScatter) - 1);
symbols_for_lvl = NaN(numLevels, traceLength);
avg_for_lvl = NaN(numLevels, traceLength);
for occurrenceIdx = 1:recordOccurrences
writeIdx = startSamples(occurrenceIdx):(startSamples(occurrenceIdx) + size(levelScatter{occurrenceIdx}, 2) - 1);
symbols_for_lvl(:, writeIdx) = levelScatter{occurrenceIdx};
avg_for_lvl(:, writeIdx) = levelAverage{occurrenceIdx};
end
end
function startSamples = getStartSamples(shifts, recordOccurrences, shiftFs, fsym, symbolLength)
if isempty(shifts)
startSamples = ((0:recordOccurrences-1) .* symbolLength) + 1;
return
end
shifts = shifts(:);
if numel(shifts) ~= recordOccurrences
positiveShifts = shifts(shifts >= 0);
if numel(positiveShifts) >= recordOccurrences
shifts = positiveShifts(1:recordOccurrences);
else
fig = figure(options.fignum); % Use the specified figure number
warning("showLevelScatter:ShiftCountMismatch", ...
"Shift count does not match scope cell count. Using sequential stitching.");
startSamples = ((0:recordOccurrences-1) .* symbolLength) + 1;
return
end
end
startSamples = round((shifts(1:recordOccurrences) - shifts(1)) ./ shiftFs .* fsym) + 1;
end
function [symbols_for_lvl, avg_for_lvl] = levelScatterForOneSequence(rxSymbols, refSymbols, windowLength)
rxSymbols = numericSignal(rxSymbols);
refSymbols = numericSignal(refSymbols);
assert(numel(rxSymbols) == numel(refSymbols), ...
'showLevelScatter:LengthMismatch', ...
'rxInput and refSymbols must have the same number of samples after resampling/synchronization.');
levels = unique(refSymbols);
[symbols_for_lvl, levels] = splitByReferenceLevels(rxSymbols, refSymbols, levels);
avg_for_lvl = NaN(numel(levels), numel(refSymbols));
for levelIdx = 1:numel(levels)
levelMask = ~isnan(symbols_for_lvl(levelIdx, :));
levelSamples = symbols_for_lvl(levelIdx, levelMask);
if isempty(levelSamples)
continue
end
smoothWindowLength = min(windowLength, numel(levelSamples));
avg_for_lvl(levelIdx, levelMask) = movmean(levelSamples, smoothWindowLength, 'Endpoints', 'shrink');
avg_for_lvl(levelIdx, :) = interpolateMissingLevelAverage(avg_for_lvl(levelIdx, :));
end
end
function [symbols_for_lvl, levels] = splitByReferenceLevels(rxSymbols, refSymbols, levels)
supportedPamLevels = [2 4 6 8 16];
if ismember(numel(levels), supportedPamLevels)
[symbols_for_lvl, levels] = PAMmapper(numel(levels), 0).splitByReferenceLevels( ...
rxSymbols, refSymbols, ...
"levels", levels);
else
symbols_for_lvl = NaN(numel(levels), numel(refSymbols));
for levelIdx = 1:numel(levels)
levelMask = refSymbols == levels(levelIdx);
symbols_for_lvl(levelIdx, levelMask) = rxSymbols(levelMask);
end
end
end
function plotInfo = plotLevelScatter(symbols_for_lvl, avg_for_lvl, refSymbols, xAxisUs, options)
plotInfo = struct();
if isempty(symbols_for_lvl)
return
end
if isnan(options.fignum)
figHandle = figure;
else
figHandle = figure(options.fignum);
if options.clear
clf;
end
end
ax = gca;
hold(ax, "on");
numLevels = size(symbols_for_lvl, 1);
cols = cbrewer2("RdBu", numLevels);
% cols = linspecer(numLevels);
xLimits = getXLimits(options.xLimits, xAxisUs, symbols_for_lvl);
rx_symbols = eq_signal; %./ rms(eq_signal);
correct_symbols = ref_symbols;
f_sym = options.f_sym;
if 1
for levelIdx = numLevels:-1:1
scatter(ax, xAxisUs, symbols_for_lvl(levelIdx, :), options.scatterSize, ".", ...
"MarkerEdgeColor", cols(levelIdx, :), ...
"MarkerEdgeAlpha", options.scatterAlpha);
end
end
col = cbrewer2('Paired',numel(unique(correct_symbols))*2);
ccnt = -1;
for levelIdx = 1:numLevels
[xReduced, yReduced] = reduceAverageLineForPlot( ...
xAxisUs, avg_for_lvl(levelIdx, :), ...
options.avgLineMaxPoints, options.avgLineSmoothWindow);
plot(ax, xReduced, yReduced, ...
"LineWidth", 1, ...
"Color", cols(levelIdx, :));
end
levels = unique(correct_symbols);
symbols_for_lvl = NaN(numel(levels),length(correct_symbols));
start = 1;
ende = length(correct_symbols);
% refValues = unique(numericSignal(refSymbols));
% for refIdx = 1:numel(refValues)
% yline(ax, refValues(refIdx), "--", ...
% "Color", [0.35 0.35 0.35], ...
% "LineWidth", 0.75, ...
% "HandleVisibility", "off");
% end
for l = 1:numel(levels)
ccnt = ccnt+2;
if options.showStdAnnotations
annotateLevelStd(symbols_for_lvl, avg_for_lvl, xAxisUs, options.xLimits);
end
level_amplitude = levels(l);
xlabel(ax, 'Time in $\mu$s');
ylabel(ax, 'Normalized Amplitude');
xlim(ax, xLimits);
ylim(ax, options.yLimits);
grid(ax, "on");
figure(figHandle);
drawnow;
end
symbols_for_lvl(l,correct_symbols==level_amplitude) = rx_symbols(correct_symbols==level_amplitude);
std_lvl(l) = std(symbols_for_lvl(l,:),'omitnan');
xax_in_sec = ((1:length(correct_symbols)) / f_sym) * 1e6;
function [xReduced, yReduced] = reduceAverageLineForPlot(xAxisUs, yTrace, maxPoints, smoothWindow)
valid = isfinite(xAxisUs) & isfinite(yTrace);
xValid = xAxisUs(valid);
yValid = yTrace(valid);
if plot_shit
scatter(xax_in_sec(start:ende),symbols_for_lvl(l,start:ende),10,'.','MarkerFaceAlpha',0.5,'MarkerEdgeAlpha',0.5,'MarkerEdgeColor',col(ccnt,:));
hold on;
if isempty(xValid)
xReduced = [];
yReduced = [];
return
end
[xValid, uniqueIdx] = unique(xValid, "stable");
yValid = yValid(uniqueIdx);
if smoothWindow > 1
yValid = smoothdata(yValid, "movmean", smoothWindow);
end
numOut = min(maxPoints, numel(xValid));
if numOut >= numel(xValid)
xReduced = xValid;
yReduced = yValid;
return
end
xReduced = linspace(xValid(1), xValid(end), numOut);
yReduced = interp1(xValid, yValid, xReduced, "pchip");
end
function xLimits = getXLimits(configuredLimits, xAxisUs, symbols_for_lvl)
if ~isempty(configuredLimits)
xLimits = configuredLimits;
return
end
filledColumns = any(~isnan(symbols_for_lvl), 1);
if ~any(filledColumns)
xLimits = [xAxisUs(1), xAxisUs(end)];
return
end
lastFilledColumn = find(filledColumns, 1, "last");
xMax = xAxisUs(lastFilledColumn);
xLimits = [0, 1*xMax];
end
function annotateLevelStd(symbols_for_lvl, avg_for_lvl, xAxisUs, configuredXLimits)
xLimits = getXLimits(configuredXLimits, xAxisUs, symbols_for_lvl);
xText = xLimits(1) + 0.2*diff(xLimits);
for levelIdx = 1:size(symbols_for_lvl, 1)
levelSamples = symbols_for_lvl(levelIdx, :);
levelStd = std(levelSamples, 0, 2, 'omitnan');
if isnan(levelStd)
continue
end
end
std_lvl = round(std_lvl,2);
ccnt = 0;
avg_for_lvl = NaN(numel(levels),length(correct_symbols));
% Add the windowed/ smoothed curves
for l = 1:numel(levels)
ccnt = ccnt+2;
level_amplitude = levels(l);
L = 500;
movmean = 1/L .* movsum(rx_symbols(correct_symbols==level_amplitude),[L/2,L/2], 'Endpoints', 'fill');
avg_for_lvl(l,correct_symbols==level_amplitude) = movmean;
nanx = isnan(avg_for_lvl(l,:));
t = 1:numel(avg_for_lvl(l,:));
avg_for_lvl(l,nanx) = interp1(t(~nanx), avg_for_lvl(l,~nanx), t(nanx));
xax_in_sec = ((1:length(correct_symbols)) / f_sym) * 1e6;
% xax_in_sec = 1:length(correct_symbols);
if plot_shit
plot(xax_in_sec(start:ende),avg_for_lvl(l,start:ende),'Color',col(ccnt,:));
levelAverage = avg_for_lvl(levelIdx, :);
yText = median(levelAverage(~isnan(levelAverage)), 'omitnan');
if isnan(yText)
yText = median(levelSamples(~isnan(levelSamples)), 'omitnan');
end
hold on
end
if 0
annotation(fig,'textbox',...
[0.660523809523809 0.844444444444448 0.133523809523809 0.0603174603174607],...
'String',['\sigma = ',num2str(std_lvl(4))],...
'LineWidth',1.8,...
'LineStyle','none',...
'FontSize',12,...
'FitBoxToText','off');
% Create textbox
annotation(fig,'textbox',...
[0.667666666666665 0.642857142857147 0.133523809523809 0.0603174603174607],...
'String',['\sigma = ',num2str(std_lvl(3))],...
'LineWidth',1.8,...
'LineStyle','none',...
'FontSize',12,...
'FitBoxToText','off');
% Create textbox
annotation(fig,'textbox',...
[0.671238095238093 0.442857142857148 0.133523809523809 0.0603174603174608],...
'String',['\sigma = ',num2str(std_lvl(2))],...
'LineWidth',1.8,...
'LineStyle','none',...
'FontSize',12,...
'FitBoxToText','off');
% Create textbox
annotation(fig,'textbox',...
[0.670047619047616 0.265079365079371 0.133523809523809 0.0603174603174608],...
'String',['\sigma = ',num2str(std_lvl(1))],...
'LineWidth',1.8,...
'LineStyle','none',...
'FontSize',12,...
'FitBoxToText','off');
end
if plot_shit
% yline(levels);
xlabel('Time in $\mu$s');
ylabel('Normalized Amplitude');
ylim([-3 3]);
label = ['$\sigma^2_',sprintf('%d', levelIdx),'$ = ', sprintf('%.3f', levelStd)];
text(xText, yText, label, ...
'Interpreter', 'latex', ...
'HorizontalAlignment', 'right', ...
'VerticalAlignment', 'middle', ...
'FontSize', 10, ...
'BackgroundColor', 'w', ...
'Margin', 2, ...
'EdgeColor', [0.8 0.8 0.8]);
end
end
function levelAverage = interpolateMissingLevelAverage(levelAverage)
validSamples = ~isnan(levelAverage);
if nnz(validSamples) == 0
return
elseif nnz(validSamples) == 1
levelAverage(:) = levelAverage(validSamples);
return
end
t = 1:numel(levelAverage);
levelAverage(~validSamples) = interp1(t(validSamples), levelAverage(validSamples), ...
t(~validSamples), 'linear', 'extrap');
end
function values = numericSignal(signalLike)
if isa(signalLike, "Signal")
values = signalLike.signal;
else
values = signalLike;
end
values = values(:).';
end
function values = normalizeNumericRms(values)
values = values ./ sqrt(mean(values.^2, "omitnan"));
end

View File

@@ -0,0 +1,35 @@
function showMarkovDiagram(sequence,M)
if isa(sequence,'Signal')
sequence = sequence.signal;
end
sequence = sequence(2+mod(1,length(sequence)):end); %filtered sequences often have one "old" sample at idx=1
x = sequence;
levels = sort(unique(x)).'; % or provide known 1x6 level values
[~,ix] = min(abs(x - levels),[],2);
x = levels(ix);
K = numel(levels);
% map to state indices 1..K
[tf, idx] = ismember(x, levels);
idx = idx(:);
from = idx(1:end-1);
to = idx(2:end);
from = idx(1:2:end);
to = idx(2:2:end);
% counts C(from,to)
C = accumarray([from,to], 1, [K K], @sum, 0);
% row-stochastic transition matrix P(to|from)
rowSums = sum(C,2);
P = C ./ max(rowSums,1);
mc = dtmc(P, 'StateNames', string(levels.*PAMmapper(M,0).get_scaling));
figure('Name','Markov Graph (dtmc)');
gp = graphplot(mc, 'ColorEdges',true, 'LabelEdges',true);
end

View File

@@ -0,0 +1,40 @@
function [sep_sig, avg_sig, info] = showMpiLevelScatter(scopeInput, Symbols, options)
%SHOWMPILEVELSCATTER Backward-compatible wrapper around showLevelScatter.
% Prefer showLevelScatter directly for new code. This wrapper keeps older
% MPI call sites working while sharing one plotting/synchronization path.
arguments
scopeInput
Symbols
options.fsym double = []
options.syncFs (1,1) double = 0
options.shiftFs (1,1) double = 0
options.shifts double = []
options.maxOccurences (1,1) double = Inf
options.normalize (1,1) logical = true
options.debug_plots (1,1) logical = false
options.fignum (1,1) double = NaN
options.clear (1,1) logical = true
options.showPlot (1,1) logical = true
options.xLimits double = []
options.yLimits (1,2) double = [-3 3]
options.showStdAnnotations (1,1) logical = true
options.windowLength (1,1) double {mustBePositive, mustBeInteger} = 500
end
[sep_sig, avg_sig, info] = showLevelScatter(scopeInput, Symbols, ...
"fsym", options.fsym, ...
"syncFs", options.syncFs, ...
"shiftFs", options.shiftFs, ...
"shifts", options.shifts, ...
"maxOccurences", options.maxOccurences, ...
"normalize", options.normalize, ...
"debug_plots", options.debug_plots, ...
"fignum", options.fignum, ...
"clear", options.clear, ...
"showPlot", options.showPlot, ...
"xLimits", options.xLimits, ...
"yLimits", options.yLimits, ...
"showStdAnnotations", options.showStdAnnotations, ...
"windowLength", options.windowLength);
end

View File

@@ -0,0 +1,40 @@
function showTransitionProbabilities(sequence)
if isa(sequence,'Signal')
sequence = sequence.signal;
end
sequence = sequence(2+mod(1,length(sequence)):end); %filtered sequences often have one "old" sample at idx=1
x = sequence;
levels = sort(unique(x)).'; % or provide known 1x6 level values
[~,ix] = min(abs(x - levels),[],2);
x = levels(ix);
K = numel(levels);
% map to state indices 1..K
[tf, idx] = ismember(x, levels);
idx = idx(:);
% from = idx(1:end-1);
% to = idx(2:end);
from = idx(1:2:end);
to = idx(2:2:end);
% counts C(from,to)
C = accumarray([from,to], 1, [K K], @sum, 0);
% row-stochastic transition matrix P(to|from)
rowSums = sum(C,2);
P = C ./ max(rowSums,1);
%% 1) HEATMAP (which transitions are more probable?)
figure('Name','Transition Probabilities (to | from)');
h = heatmap(levels, levels, P, 'Colormap', parula, 'ColorbarVisible','on');
colormap(gca,[[1,1,1];flip(cbrewer2('Spectral',100))]);clim([0,ceil(max(P(:))*10)/10]);
h.XLabel = 'From state (level)';
h.YLabel = 'To state (level)';
h.Title = 'P(to | from)';
end

View File

@@ -0,0 +1,120 @@
function output = dsp_runid(run_id, options)
arguments
run_id
options.append_to_db = 0;
options.append_mpi_reduction_db (1,1) logical = false;
options.mpi_reduction_study_name string = "mpi_reduction_v1";
options.mpi_reduction_writer_path string = "";
options.max_occurences = 4;
options.start_occurence = 1;
options.userParameters = struct();
options.database_type
options.dataBase
options.server = "134.245.243.254";
options.port = 3306;
options.user = "silas";
options.password = "silas";
options.load_file_path = struct();
options.storage_path
options.mode
options.recipe = @dsp_scope_signal;
options.debug_plots (1,1) logical = false;
end
try
output = initializeOutput();
database = [];
inputSource = normalizeDspInputSource(options.mode);
if options.append_mpi_reduction_db && strlength(options.mpi_reduction_writer_path) > 0
addpath(options.mpi_reduction_writer_path);
end
if inputSource == "run_id" || options.append_to_db || options.append_mpi_reduction_db
database = DBHandler("dataBase", [options.dataBase], "type", options.database_type, ...
"user", options.user, "password", options.password, ...
"server", options.server, "port", options.port);
end
switch inputSource
case "run_id"
dspInput = loadDspInputFromRunId(run_id, database, options);
case "file_paths"
dspInput = loadDspInputFromFilePaths(run_id, options);
end
num_occurences = length(dspInput.Scpe_cell);
for r = 1:num_occurences
%%%%%%%% CORE EQUALIZATION CALL (Scpe, Symbols, Bits, 'Options') %%%%%%%
dspOutput = options.recipe(dspInput.Scpe_cell{r}, dspInput.Symbols, dspInput.Tx_bits, ...
"fsym", dspInput.fsym, ...
"M", dspInput.M, ...
"duob_mode", dspInput.duob_mode, ...
"dataTable", dspInput.dataTable, ...
"userParameters", options.userParameters, ...
"debug_plots", options.debug_plots);
%%%%%%%% CORE EQUALIZATION CALL %%%%%%%
fieldNames = fieldnames(dspOutput);
for fieldIdx = 1:numel(fieldNames)
fieldName = fieldNames{fieldIdx};
output.(fieldName){r} = dspOutput.(fieldName);
end
if options.append_to_db
appendDspOutputToDatabase(database, run_id, dspOutput);
end
if options.append_mpi_reduction_db
occurrence_idx = options.start_occurence + r - 1;
appendMpiReductionDspOutput(database, run_id, occurrence_idx, dspOutput, options, ...
"study_name", options.mpi_reduction_study_name, ...
"verbose", false);
end
end
catch ME
save('workerError.mat', 'ME');
rethrow(ME);
end
end
function output = initializeOutput()
output.ffe_package = {};
output.dfe_package = {};
output.mlse_package = {};
output.vnle_package = {};
output.dbtgt_package = {};
output.dbenc_package = {};
output.mlmlse_package = {};
end
function inputSource = normalizeDspInputSource(mode)
mode = string(mode);
switch mode
case {"load_run_id", "run_id"}
inputSource = "run_id";
case {"load_files", "file_paths"}
inputSource = "file_paths";
otherwise
error('dsp_runid:UnsupportedMode', ...
'Mode "%s" is not supported. Use "run_id" or "file_paths".', mode);
end
end
function appendDspOutputToDatabase(database, run_id, dspOutput)
packageNames = fieldnames(dspOutput);
for i = 1:numel(packageNames)
package = dspOutput.(packageNames{i});
if isempty(package)
continue
end
database.addProcessingResult(run_id, package.metrics, package.config);
end
end

View File

@@ -0,0 +1,86 @@
function pulseformer = findSignalPulseformer(signalLike)
%FINDSIGNALPULSEFORMER Try to recover TX pulseformer metadata from a signal logbook.
pulseformer = [];
if isempty(signalLike) || ~isprop(signalLike, 'logbook') || isempty(signalLike.logbook)
return
end
if ~ismember('ModifierCopy', signalLike.logbook.Properties.VariableNames)
return
end
modifierCopies = signalLike.logbook.ModifierCopy;
for idx = numel(modifierCopies):-1:1
pulseformer = extractPulseformerCandidate(modifierCopies{idx});
if ~isempty(pulseformer)
return
end
end
end
function pulseformer = extractPulseformerCandidate(candidate)
pulseformer = [];
if isempty(candidate)
return
end
if isa(candidate, 'Pulseformer')
pulseformer = candidate;
return
end
if iscell(candidate)
for cellIdx = 1:numel(candidate)
pulseformer = extractPulseformerCandidate(candidate{cellIdx});
if ~isempty(pulseformer)
return
end
end
return
end
if isstruct(candidate)
if isfield(candidate, 'pulseformer')
pulseformer = extractPulseformerCandidate(candidate.pulseformer);
if ~isempty(pulseformer)
return
end
end
if hasPulseformerFields(candidate)
pulseformer = candidate;
end
return
end
try
if isprop(candidate, 'pulseformer')
pulseformer = extractPulseformerCandidate(candidate.pulseformer);
if ~isempty(pulseformer)
return
end
end
catch
end
try
if hasPulseformerFields(candidate)
pulseformer = candidate;
end
catch
end
end
function tf = hasPulseformerFields(candidate)
tf = false;
if isstruct(candidate)
tf = isfield(candidate, 'pulse') && isfield(candidate, 'pulselength');
return
end
tf = isobject(candidate) && isprop(candidate, 'pulse') && isprop(candidate, 'pulselength');
end

View File

@@ -0,0 +1,201 @@
function [Bits, Symbols, Scpe_cell, found_sync] = loadAndSyncRunSignals(dataTable, options)
%LOADANDSYNCRUNSIGNALS Load and synchronize signal files for one run.
%
% Inputs:
% dataTable - one-row table with run metadata and signal file paths
% options - struct with storage_path, start_occurence and max_occurences
%
% Outputs:
% Bits - transmitted bit reference
% Symbols - transmitted symbol reference
% Scpe_cell - synchronized received signal occurrences
% found_sync - true when a valid synchronization was found
found_sync = 0;
tempLocalStorage = 1;
Scpe_cell = {};
loaded_from_cache = false;
storage_dir = fullfile(prefdir, 'temp_sync_data');
if tempLocalStorage == 1
local_filename = fullfile(storage_dir, sprintf('sync_data_run_%s.mat', num2str(dataTable.run_id)));
if exist(local_filename, 'file')
try
cacheData = load(local_filename, 'Bits', 'Symbols', 'Scpe_cell');
if isValidSyncCache(cacheData)
Bits = cacheData.Bits;
Symbols = cacheData.Symbols;
Scpe_cell = cacheData.Scpe_cell;
found_sync = 1;
loaded_from_cache = true;
else
warning('loadAndSyncRunSignals:InvalidSyncCache', ...
'Ignoring incomplete sync cache file "%s".', local_filename);
safeDelete(local_filename);
end
catch
safeDelete(local_filename);
found_sync = 0;
end
end
end
if ~found_sync
Bits = load(composeStoragePath(options.storage_path, dataTable.tx_bits_path));
Bits = Bits.Bits;
M = double(dataTable.pam_level);
fsym = dataTable.symbolrate;
Symbols_mapped = PAMmapper(M,0).map(Bits);
Symbols_mapped.fs = fsym;
Symbols = load(composeStoragePath(options.storage_path, dataTable.tx_symbols_path));
Symbols = Symbols.Symbols;
found_sync = 0;
try
Scpe_load = load(composeStoragePath(options.storage_path, dataTable.rx_sync_path));
Scpe_cell = Scpe_load.S;
[~,~,~,found_sync] = Scpe_cell{1}.tsynch("reference", Symbols, ...
"fs_ref", fsym, ...
"debug_plots", 0);
catch
% Continue with raw data if pre-synchronized data is unavailable.
end
end
if ~found_sync
try
Scpe_sig_raw = load(composeStoragePath(options.storage_path, dataTable.rx_raw_path));
Scpe_sig_raw = Scpe_sig_raw.Scpe_sig_raw;
Scpe_sig_resampled = Scpe_sig_raw.resample("fs_in", Scpe_sig_raw.fs, "fs_out", 2*fsym);
[~, Scpe_cell, ~, found_sync] = Scpe_sig_resampled.tsynch("reference", Symbols, ...
"fs_ref", fsym, ...
"debug_plots", 0);
catch
% Continue to mapped-symbol fallback if raw data sync fails.
end
end
if ~found_sync && exist('Scpe_sig_raw', 'var')
if length(Symbols_mapped.signal) ~= sum(Symbols_mapped.signal == Symbols.signal)
[~, Scpe_cell, ~, found_sync] = Scpe_sig_raw.tsynch("reference", Symbols_mapped, ...
"fs_ref", fsym, ...
"debug_plots", 0);
end
end
if tempLocalStorage == 1 && found_sync && ~loaded_from_cache
if ~exist(storage_dir, 'dir')
mkdir(storage_dir);
end
max_local_files = 10;
files = dir(fullfile(storage_dir, 'sync_data_run_*.mat'));
if length(files) >= max_local_files
[~, fileOrder] = sort([files.datenum]);
delete(fullfile(storage_dir, files(fileOrder(1)).name));
end
writeSyncCache(local_filename, Bits, Symbols, Scpe_cell);
end
if found_sync
Scpe_cell = selectSyncedOccurrences(Scpe_cell, options);
else
warning('Could not synchronize the received signal with the stored symbols!');
end
end
function path = composeStoragePath(storagePath, relativePath)
relativePath = firstValue(relativePath);
path = char(string(storagePath) + string(relativePath));
if ismac
path = strrep(path,'\','/');
else
path = strrep(path,'/','\');
end
end
function value = firstValue(value)
if iscell(value)
value = value{1};
elseif ~ischar(value) && ~isscalar(value)
value = value(1);
end
end
function Scpe_cell = selectSyncedOccurrences(Scpe_cell, options)
available_occurences = length(Scpe_cell);
start_occurence = floor(getOption(options, 'start_occurence', 1));
max_occurences = floor(getOption(options, 'max_occurences', available_occurences));
if available_occurences < 1
warning('loadAndSyncRunSignals:NoSyncedOccurrences', ...
'Synchronization reported success, but no synced occurrences are available.');
return
end
if start_occurence < 1
error('loadAndSyncRunSignals:InvalidStartOccurrence', ...
'start_occurence must be >= 1.');
end
if max_occurences < 1
Scpe_cell = Scpe_cell(1:0);
return
end
if start_occurence > available_occurences
warning('loadAndSyncRunSignals:StartOccurrenceTooHigh', ...
['Requested start_occurence %d, but only %d synced occurrences are available. ', ...
'Processing the last occurrence only.'], ...
start_occurence, available_occurences);
Scpe_cell = Scpe_cell(available_occurences);
return
end
stop_occurence = min(available_occurences, start_occurence + max_occurences - 1);
Scpe_cell = Scpe_cell(start_occurence:stop_occurence);
end
function value = getOption(options, name, defaultValue)
if isfield(options, name)
value = options.(name);
else
value = defaultValue;
end
end
function valid = isValidSyncCache(cacheData)
valid = isfield(cacheData, 'Bits') && ...
isfield(cacheData, 'Symbols') && ...
isfield(cacheData, 'Scpe_cell') && ...
iscell(cacheData.Scpe_cell) && ...
~isempty(cacheData.Scpe_cell);
end
function writeSyncCache(local_filename, Bits, Symbols, Scpe_cell)
cache_dir = fileparts(local_filename);
temp_filename = [tempname(cache_dir), '.mat'];
try
save(temp_filename, 'Bits', 'Symbols', 'Scpe_cell');
movefile(temp_filename, local_filename, 'f');
catch ME
safeDelete(temp_filename);
rethrow(ME);
end
end
function safeDelete(filename)
if exist(filename, 'file')
try
delete(filename);
catch
% Another parallel worker may already have removed or replaced it.
end
end
end

View File

@@ -1,114 +0,0 @@
function [Bits, Symbols, Scpe_cell, found_sync] = loadAndSyncSignalDataFromDb(dataTable, options)
% LOADSIGNALDATA Loads and synchronizes signal data from storage
%
% Inputs:d
% dataTable - Table with file paths and configuration
% options - Struct with storage_path and max_occurences
%
% Outputs:
% Symbols_mapped - Mapped symbols from bits
% Symbols - Original symbols
% Scpe_cell - Cell array of synchronized signals
% found_sync - Boolean indicating if synchronization was successful
found_sync = 0;
tempLocalStorage = 1;
% Define the fixed storage directory relative to the user's MATLAB preferences directory
storage_dir = fullfile(prefdir, 'temp_sync_data');
% Part A: Check and load from local storage if available-
if tempLocalStorage == 1
local_filename = fullfile(storage_dir, sprintf('sync_data_run_%s.mat', num2str(dataTable.run_id)));
if exist(local_filename, 'file')
% Load from local storage and return
try
load(local_filename, 'Bits', 'Symbols', 'Scpe_cell');
found_sync = 1;
return
catch
delete(local_filename);
end
end
end
% If not locally saved, load from storage
if ~found_sync
% Load transmitted bits
Bits = load(fullfile([options.storage_path, char(dataTable.tx_bits_path)]));
Bits = Bits.Bits;
% Map bits to symbols
M = double(dataTable.pam_level);
fsym = dataTable.symbolrate;
Symbols_mapped = PAMmapper(M,0).map(Bits);
Symbols_mapped.fs = fsym;
% Load original symbols
Symbols = load(fullfile([options.storage_path, char(dataTable.tx_symbols_path)]));
Symbols = Symbols.Symbols;
found_sync = 0;
Scpe_cell = {};
% Try to load pre-synchronized data
try
Scpe_load = load(fullfile([options.storage_path, char(dataTable.rx_sync_path)]));
Scpe_cell = Scpe_load.S;
[~,~,~,found_sync] = Scpe_cell{2}.tsynch("reference", Symbols, "fs_ref", fsym, "debug_plots", 1);
catch
% Continue to next method if this fails
end
end
% If not found, try with raw data
if ~found_sync
try
Scpe_sig_raw = load([options.storage_path, char(dataTable.rx_raw_path(1))]);
Scpe_sig_raw = Scpe_sig_raw.Scpe_sig_raw;
Scpe_sig_resampled = Scpe_sig_raw.resample("fs_in", Scpe_sig_raw.fs, "fs_out", 2*fsym);
[~, Scpe_cell, ~, found_sync] = Scpe_sig_resampled.tsynch("reference", Symbols, "fs_ref", fsym, "debug_plots", 1);
catch
% Continue to next method if this fails
end
end
% Last attempt with mapped symbols
if ~found_sync && exist('Scpe_sig_raw', 'var')
if length(Symbols_mapped.signal) ~= sum(Symbols_mapped.signal == Symbols.signal)
[~, Scpe_cell, ~, found_sync] = Scpe_sig_raw.tsynch("reference", Symbols_mapped, "fs_ref", fsym, "debug_plots", 0);
end
end
% Part B: Save to local storage if data was loaded and synced
if tempLocalStorage == 1 && found_sync
% Create directory if it doesn't exist
if ~exist(storage_dir, 'dir')
mkdir(storage_dir);
end
% local_filename = fullfile(storage_dir, sprintf('sync_data_run_%s.mat', num2str(dataTable.run_id)));
% List existing files and remove oldest if more than N
max_local_files = 10; % Store up to N files
files = dir(fullfile(storage_dir, 'sync_data_run_*.mat'));
if length(files) >= max_local_files
% Sort by date
[~, idx] = sort([files.datenum]);
% Delete oldest file
delete(fullfile(storage_dir, files(idx(1)).name));
end
% Save current data
save(local_filename, 'Bits', 'Symbols', 'Scpe_cell');
end
% Limit number of occurrences
if found_sync
record_realizations = min(options.max_occurences, length(Scpe_cell));
Scpe_cell = Scpe_cell(1:record_realizations);
else
warning('Could not synchronize the received signal with the stored symbols!');
end
end

View File

@@ -0,0 +1,97 @@
function dspInput = loadDspInputFromFilePaths(run_id, options)
%LOADDSPINPUTFROMFILEPATHS Load explicit signal files and prepare DSP input.
arguments
run_id
options struct
end
Tx_bits = load(textScalar(options.load_file_path.tx_bits_path));
Symbols = load(textScalar(options.load_file_path.tx_symbols_path));
Scpe_sig_raw = load(textScalar(options.load_file_path.rx_raw_path));
Tx_bits = Tx_bits.Bits;
Symbols = Symbols.Symbols;
Scpe_sig_raw = Scpe_sig_raw.Scpe_sig_raw;
fsym = Symbols.fs;
M = Symbols.logbook.ModifierCopy{1}.M;
duob_mode = Symbols.logbook.ModifierCopy{1}.duobinary_mode;
Scpe_sig_resampled = Scpe_sig_raw.resample("fs_in", Scpe_sig_raw.fs, "fs_out", 2*fsym);
[~, Scpe_cell, ~, found_sync] = Scpe_sig_resampled.tsynch("reference", Symbols, ...
"fs_ref", fsym, ...
"debug_plots", 1);
if found_sync
Scpe_cell = selectSyncedOccurrences(Scpe_cell, options);
else
warning('Could not synchronize the received signal with the stored symbols!');
end
dspInput = struct();
dspInput.run_id = run_id;
dspInput.dataTable = table();
dspInput.Tx_bits = Tx_bits;
dspInput.Symbols = Symbols;
dspInput.Scpe_cell = Scpe_cell;
dspInput.found_sync = found_sync;
dspInput.fsym = fsym;
dspInput.M = M;
dspInput.duob_mode = duob_mode;
end
function value = textScalar(value)
value = firstValue(value);
value = char(string(value));
end
function value = firstValue(value)
if iscell(value)
value = value{1};
elseif ~ischar(value) && ~isscalar(value)
value = value(1);
end
end
function Scpe_cell = selectSyncedOccurrences(Scpe_cell, options)
available_occurences = length(Scpe_cell);
start_occurence = floor(getOption(options, 'start_occurence', 1));
max_occurences = floor(getOption(options, 'max_occurences', available_occurences));
if available_occurences < 1
warning('loadDspInputFromFilePaths:NoSyncedOccurrences', ...
'Synchronization reported success, but no synced occurrences are available.');
return
end
if start_occurence < 1
error('loadDspInputFromFilePaths:InvalidStartOccurrence', ...
'start_occurence must be >= 1.');
end
if max_occurences < 1
Scpe_cell = Scpe_cell(1:0);
return
end
if start_occurence > available_occurences
warning('loadDspInputFromFilePaths:StartOccurrenceTooHigh', ...
['Requested start_occurence %d, but only %d synced occurrences are available. ', ...
'Processing the last occurrence only.'], ...
start_occurence, available_occurences);
Scpe_cell = Scpe_cell(available_occurences);
return
end
stop_occurence = min(available_occurences, start_occurence + max_occurences - 1);
Scpe_cell = Scpe_cell(start_occurence:stop_occurence);
end
function value = getOption(options, name, defaultValue)
if isfield(options, name)
value = options.(name);
else
value = defaultValue;
end
end

View File

@@ -0,0 +1,40 @@
function dspInput = loadDspInputFromRunId(run_id, database, options)
%LOADDSPINPUTFROMRUNID Query run metadata and prepare canonical DSP input.
arguments
run_id
database
options struct
end
dataTable = queryRunid(run_id, database);
% Load signal files referenced by the run metadata, verify/synchronize the
% received signal, optionally cache the sync result, and cap occurrences.
[Tx_bits, Symbols, Scpe_cell, found_sync] = loadAndSyncRunSignals(dataTable, options);
dspInput = struct();
dspInput.run_id = run_id;
dspInput.dataTable = dataTable;
dspInput.Tx_bits = Tx_bits;
dspInput.Symbols = Symbols;
dspInput.Scpe_cell = Scpe_cell;
dspInput.found_sync = found_sync;
dspInput.fsym = dataTable.symbolrate;
dspInput.M = double(dataTable.pam_level);
dspInput.duob_mode = parseDbMode(dataTable.db_mode);
end
function mode = parseDbMode(rawMode)
if isnumeric(rawMode)
mode = db_mode(rawMode);
return
end
if iscell(rawMode)
rawMode = rawMode{1};
end
mode = db_mode(strrep(string(rawMode), '"', ''));
end

View File

@@ -1,32 +0,0 @@
function Scpe_sig = preprocessSignal(Scpe_sig, Symbols, fsym)
% PREPROCESSSIGNAL Performs standard preprocessing on a signal
%
% Inputs:
% Scpe_sig - Input signal
% Symbols - Reference symbols for synchronization
% fsym - Symbol frequency
%
% Outputs:
% Scpe_sig - Preprocessed signal
% Resample to 2x symbol rate
Scpe_sig = Scpe_sig.resample("fs_out", 2*fsym);
% Synchronize with reference
[Scpe_sig, ~] = Scpe_sig.tsynch("reference", Symbols, "fs_ref", fsym, "debug_plots", 0);
% Apply Gaussian filter
if 1
Scpe_sig = Filter('filtdegree', 8, "f_cutoff", Symbols.fs.*0.52, ...
"fs", Scpe_sig.fs, "filterType", filtertypes.gaussian, ...
"active", true).process(Scpe_sig);
else
Scpe_sig = Filter('filtdegree', 4, "f_cutoff", Symbols.fs.*0.6, ...
"fs", Scpe_sig.fs, "filterType", filtertypes.gaussian, ...
"active", true).process(Scpe_sig);
end
%Remove DC offset
Scpe_sig = Scpe_sig - mean(Scpe_sig.signal);
end

View File

@@ -0,0 +1,291 @@
function batchResults = runBatch(workerFcn, jobs, options)
%RUNBATCH Execute a list of jobs in serial or parallel.
% batchResults = runBatch(workerFcn, jobs) executes each jobs(i).args cell
% with workerFcn and returns a cell array aligned with the input jobs.
%
% Supported job fields:
% .args cell array of positional arguments for workerFcn
% .label string/char used for progress messages
% .meta arbitrary metadata for wrapper-specific bookkeeping
arguments
workerFcn (1,1) function_handle
jobs (1,:) struct = struct('args', {})
options.mode = processingMode.serial
options.waitbar (1,1) logical = true
options.waitbarMessage (1,1) string = "Processing Jobs..."
options.numWorkers (1,1) double {mustBeNonnegative, mustBeInteger} = 0
options.idleTimeout (1,1) double {mustBePositive} = 300
options.cancelExistingQueue (1,1) logical = true
options.resultHandler = []
options.errorHandler = []
end
mode = normalizeProcessingMode(options.mode);
jobs = normalizeJobs(jobs);
nJobs = numel(jobs);
batchResults = cell(1, nJobs);
jobDurations = nan(1, nJobs);
lastJobDuration = nan;
batchStart = tic;
effectiveWorkerCount = 1;
h = [];
if nJobs == 0
return
end
if options.waitbar
h = waitbar(0, char(options.waitbarMessage));
cleanupWaitbar = onCleanup(@() closeWaitbar(h));
updateWaitbar(options.waitbar, h, 0, nJobs, jobDurations, ...
lastJobDuration, 0, effectiveWorkerCount);
end
switch mode
case processingMode.parallel
pool = setupParallelPool(options.numWorkers, options.idleTimeout, options.cancelExistingQueue);
effectiveWorkerCount = min(pool.NumWorkers, nJobs);
futures = parallel.FevalFuture.empty(nJobs, 0);
for jobIdx = 1:nJobs
fprintf('[%s] Submitted.\n', jobs(jobIdx).label);
futures(jobIdx) = parfeval(pool, @runTimedJob, 3, workerFcn, jobs(jobIdx).args);
end
consumedIdx = false(nJobs, 1);
for completedCount = 1:nJobs
try
[jobIdx, jobResult, jobRuntime, jobError] = fetchNext(futures);
consumedIdx(jobIdx) = true;
jobDurations(jobIdx) = jobRuntime;
lastJobDuration = jobRuntime;
if isempty(jobError)
batchResults{jobIdx} = jobResult;
fprintf('[%s] Completed (%d/%d, runtime %s).\n', ...
jobs(jobIdx).label, completedCount, nJobs, formatDuration(jobRuntime));
if ~isempty(options.resultHandler)
options.resultHandler(jobResult, jobs(jobIdx), jobIdx);
end
else
batchResults{jobIdx} = jobError;
if ~isempty(options.errorHandler)
options.errorHandler(jobError, jobs(jobIdx), jobIdx);
else
defaultErrorHandler(jobError, jobs(jobIdx), jobIdx);
end
end
catch fetchErr
errorJobIdx = findErroredFuture(futures, consumedIdx);
if isempty(errorJobIdx)
rethrow(fetchErr);
end
consumedIdx(errorJobIdx) = true;
errInfo = extractFutureError(futures(errorJobIdx));
batchResults{errorJobIdx} = errInfo;
if ~isempty(options.errorHandler)
options.errorHandler(errInfo, jobs(errorJobIdx), errorJobIdx);
else
defaultErrorHandler(errInfo, jobs(errorJobIdx), errorJobIdx);
end
end
updateWaitbar(options.waitbar, h, completedCount, nJobs, ...
jobDurations, lastJobDuration, toc(batchStart), effectiveWorkerCount);
end
case processingMode.serial
for jobIdx = 1:nJobs
jobStart = tic;
try
fprintf('[%s] Running.\n', jobs(jobIdx).label);
jobResult = workerFcn(jobs(jobIdx).args{:});
jobDurations(jobIdx) = toc(jobStart);
lastJobDuration = jobDurations(jobIdx);
batchResults{jobIdx} = jobResult;
fprintf('[%s] Completed (%d/%d, runtime %s).\n', ...
jobs(jobIdx).label, jobIdx, nJobs, formatDuration(jobDurations(jobIdx)));
if ~isempty(options.resultHandler)
options.resultHandler(jobResult, jobs(jobIdx), jobIdx);
end
catch ME
jobDurations(jobIdx) = toc(jobStart);
lastJobDuration = jobDurations(jobIdx);
batchResults{jobIdx} = ME;
if ~isempty(options.errorHandler)
options.errorHandler(ME, jobs(jobIdx), jobIdx);
else
defaultErrorHandler(ME, jobs(jobIdx), jobIdx);
end
end
updateWaitbar(options.waitbar, h, jobIdx, nJobs, ...
jobDurations, lastJobDuration, toc(batchStart), effectiveWorkerCount);
end
otherwise
error('runBatch:UnknownMode', 'Unknown processing mode "%s".', string(mode));
end
end
function mode = normalizeProcessingMode(mode)
if isa(mode, 'processingMode')
return
end
modeString = string(mode);
switch modeString
case "serial"
mode = processingMode.serial;
case "parallel"
mode = processingMode.parallel;
otherwise
error('runBatch:InvalidMode', 'Unsupported processing mode "%s".', modeString);
end
end
function jobs = normalizeJobs(jobs)
if isempty(jobs)
jobs = struct('args', {}, 'label', {}, 'meta', {});
return
end
for jobIdx = 1:numel(jobs)
if ~isfield(jobs, 'args') || isempty(jobs(jobIdx).args)
jobs(jobIdx).args = {};
end
if ~iscell(jobs(jobIdx).args)
error('runBatch:InvalidArgs', 'jobs(%d).args must be a cell array.', jobIdx);
end
if ~isfield(jobs, 'label') || isempty(jobs(jobIdx).label)
jobs(jobIdx).label = sprintf('Job %d', jobIdx);
else
jobs(jobIdx).label = string(jobs(jobIdx).label);
end
if ~isfield(jobs, 'meta')
jobs(jobIdx).meta = struct();
end
end
end
function [jobResult, jobRuntime, jobError] = runTimedJob(workerFcn, jobArgs)
jobStart = tic;
jobError = [];
try
jobResult = workerFcn(jobArgs{:});
catch ME
jobResult = [];
jobError = ME;
end
jobRuntime = toc(jobStart);
end
function updateWaitbar(useWaitbar, h, completedCount, totalCount, jobDurations, lastJobDuration, elapsedSeconds, effectiveWorkerCount)
if ~useWaitbar || ~isgraphics(h)
return
end
completedDurations = jobDurations(~isnan(jobDurations));
if isempty(completedDurations)
averageJobTimeText = 'calculating...';
lastJobTimeText = 'calculating...';
remainingTimeText = 'calculating...';
else
averageJobSeconds = mean(completedDurations);
remainingJobs = totalCount - completedCount;
remainingSeconds = averageJobSeconds * remainingJobs / max(1, effectiveWorkerCount);
averageJobTimeText = formatDuration(averageJobSeconds);
lastJobTimeText = formatDuration(lastJobDuration);
remainingTimeText = formatDuration(remainingSeconds);
end
message = sprintf(['Completed %d/%d jobs\n' ...
'Elapsed total: %s\n' ...
'Last job time: %s\n' ...
'Avg. time/job: %s\n' ...
'Estimated remaining: %s'], ...
completedCount, totalCount, formatDuration(elapsedSeconds), ...
lastJobTimeText, averageJobTimeText, remainingTimeText);
waitbar(completedCount / totalCount, h, message);
drawnow;
end
function text = formatDuration(seconds)
if isempty(seconds) || isnan(seconds) || isinf(seconds)
text = 'unknown';
return
end
seconds = max(0, seconds);
hours = floor(seconds / 3600);
minutes = floor(mod(seconds, 3600) / 60);
wholeSeconds = floor(mod(seconds, 60));
if hours > 0
text = sprintf('%d:%02d:%02d', hours, minutes, wholeSeconds);
else
text = sprintf('%02d:%02d', minutes, wholeSeconds);
end
end
function closeWaitbar(h)
if isgraphics(h)
delete(h);
end
end
function pool = setupParallelPool(numWorkers, idleTimeout, cancelExistingQueue)
pool = gcp('nocreate');
if isempty(pool)
if numWorkers > 0
pool = parpool('local', numWorkers, 'IdleTimeout', idleTimeout);
else
pool = parpool('local', 'IdleTimeout', idleTimeout);
end
elseif numWorkers > 0 && pool.NumWorkers ~= numWorkers
delete(pool);
pool = parpool('local', numWorkers, 'IdleTimeout', idleTimeout);
end
if cancelExistingQueue
queue = pool.FevalQueue;
if ~isempty(queue.QueuedFutures) || ~isempty(queue.RunningFutures)
queuedCount = numel(queue.QueuedFutures) + numel(queue.RunningFutures);
cancelAll(queue);
fprintf('Canceled %d unfinished jobs from the current pool queue.\n', queuedCount);
end
end
end
function errorJobIdx = findErroredFuture(futures, consumedIdx)
readMask = arrayfun(@(future) future.Read, futures).';
errorJobIdx = find(readMask & ~consumedIdx, 1);
end
function errInfo = extractFutureError(future)
errInfo = future.Error;
if iscell(errInfo)
errInfo = errInfo{1};
end
end
function defaultErrorHandler(ME, job, jobIndex)
fprintf('[%s | #%d] ERROR [%s]: %s\n', job.label, jobIndex, ME.identifier, ME.message);
for st = ME.stack'
fprintf(' %s:%d (%s)\n', st.file, st.line, st.name);
end
end

View File

@@ -9,9 +9,11 @@ function [results, wh] = submitJobs(run_ids, dsp_options, submit_mode, submit_op
% submit_options : struct with fields
% .waitbar (logical)
% .wh (DataStorage object)
% .storePackages (string array, optional package whitelist)
%
% results : cell(nJobsPerRunId, nRunIds)
% wh : updated DataStorage
% wh : updated DataStorage. For multiple run_ids, run_id is added as
% the first storage axis.
arguments
run_ids int32 = 0
@@ -19,172 +21,60 @@ arguments
submit_mode processingMode = processingMode.serial
submit_options.waitbar (1,1) logical = true
submit_options.wh = DataStorage(struct())
submit_options.storePackages string = string.empty()
end
% Normalize
run_ids = run_ids(:)';
nRunIds = numel(run_ids);
nJobsPerRunId = submit_options.wh.getLastLinIndice();
sweepWh = submit_options.wh;
validateNoRunIdSweepParameter(sweepWh);
nJobsPerRunId = sweepWh.getLastLinIndice();
totalJobs = nRunIds * nJobsPerRunId;
% Preallocate
wh = buildStorageWarehouse(sweepWh, run_ids);
results = cell(nJobsPerRunId, nRunIds);
futures = parallel.FevalFuture.empty(totalJobs,0);
jobIndices = zeros(totalJobs,2);
jobs = repmat(struct('args', {{}}, 'label', "", 'meta', struct()), 1, totalJobs);
% Optional waitbar
if submit_options.waitbar
h = waitbar(0, 'Processing Jobs...');
cleanupObj = onCleanup(@() delete(h));
end
jobCounter = 0;
for r = 1:nRunIds
for k = 1:nJobsPerRunId
jobCounter = jobCounter + 1;
userParameters = buildUserParameters(k, sweepWh);
jobOptions = dsp_options;
jobOptions.userParameters = userParameters;
switch submit_mode
case processingMode.parallel
% SET UP POOL & QUEUE
p = setupParallelPool(11, 300); % 10 workers, 300s idle timeout
% === submit all futures ===
jobCounter = 0;
for r = 1:nRunIds
for k = 1:nJobsPerRunId
jobCounter = jobCounter + 1;
jobIndices(jobCounter,:) = [r,k];
opt = buildOptionalVars(k, submit_options.wh);
futures(jobCounter) = parfeval( ...
p, @dsp_runid, 1, ...
run_ids(r), ...
"database_type", dsp_options.database_type, ...
"dataBase", dsp_options.dataBase, ...
"append_to_db", dsp_options.append_to_db, ...
"load_file_path", dsp_options.load_file_path, ...
"max_occurences", dsp_options.max_occurences, ...
"storage_path", dsp_options.storage_path, ...
"mode", dsp_options.mode, ...
"parameters", opt ...
);
fprintf('[RunID %d, Job %d] Submitted to pool.\n', run_ids(r), k);
end
end
% W A I T B A R U P D A T E
if submit_options.waitbar
futureArray = futures;
updateWB = @() waitbar( ...
sum(arrayfun(@(f) strcmp(f.State,'finished'), futureArray))/totalJobs, ...
h, sprintf('Completed %d/%d jobs', ...
sum(arrayfun(@(f) strcmp(f.State,'finished'), futureArray)), totalJobs) ...
);
afterEach(futureArray, updateWB, 0);
end
% before the loop
% Keep track of which futures we've already handled:
consumedIdx = false(totalJobs,1);
% fetch in completion order, handling successes and errors
for n = 1:totalJobs
try
% This returns the value AND the linear index in 'futures'
[idx, val] = fetchNext(futures);
duration = futures(idx).RunningDuration;
startDT = futures(idx).StartDateTime;
finishDT = futures(idx).FinishDateTime;
duration = finishDT - startDT;
% Mark it consumed
consumedIdx(idx) = true;
% Map back to (r,k) and store
r = jobIndices(idx,1);
k = jobIndices(idx,2);
fprintf('[%s] JobID %d/%d (%.1f%%) %s RunID %d Subjob %d fetched.\n', ...
datestr(now,'yyyy-mm-dd HH:MM:SS'), ...
r, totalJobs, 100*n/totalJobs,char(duration), ...
run_ids(r), k);
% Update waitbar
if submit_options.waitbar
waitbar(n/totalJobs, h, ...
sprintf('Fetched %d/%d (%.1f% Percent)', n, totalJobs, 100*n/totalJobs));
drawnow; % force the GUI to refresh
end
storeResult(val, k, submit_options.wh);
results{k,r} = val;
catch fetchErr
% fetchNext has already set Read=true on the errored future.
% Find the one Read==true that we have _not_ yet consumed.
readMask = arrayfun(@(f) f.Read, futures);
idxErr = find(readMask & ~consumedIdx', 1);
consumedIdx(idxErr) = true;
% Pull the _real_ exception out of the future object
errInfo = futures(idxErr).Error;
if iscell(errInfo)
origME = errInfo{1};
else
origME = errInfo;
end
% Map back to (r,k) and log
r = jobIndices(idxErr,1);
k = jobIndices(idxErr,2);
handleError(origME, k, run_ids(r));
results{k,r} = origME;
end
jobs(jobCounter).args = [{run_ids(r)}, structToNameValue(jobOptions)];
jobs(jobCounter).label = sprintf('RunID %d, Job %d', run_ids(r), k);
jobs(jobCounter).meta.runIndex = r;
jobs(jobCounter).meta.jobIndex = k;
jobs(jobCounter).meta.sweepJobIndex = k;
jobs(jobCounter).meta.storageJobIndex = buildStorageJobIndex(run_ids(r), userParameters, wh);
jobs(jobCounter).meta.run_id = run_ids(r);
end
case processingMode.serial
% SERIAL EXECUTION
jobCounter = 0;
for r = 1:nRunIds
for k = 1:nJobsPerRunId
jobCounter = jobCounter + 1;
optionalVars = buildOptionalVars(k, submit_options.wh);
try
fprintf('[RunID %d, Job %d] Running in linear mode...\n', run_ids(r), k);
val = dsp_runid( run_ids(r), ...
"database_type", dsp_options.database_type, ...
"dataBase", dsp_options.dataBase, ...
"append_to_db", dsp_options.append_to_db, ...
"load_file_path", dsp_options.load_file_path, ...
"max_occurences", dsp_options.max_occurences, ...
"storage_path", dsp_options.storage_path, ...
"mode", dsp_options.mode, ...
"parameters", optionalVars );
fprintf('[RunID %d, Job %d] Completed successfully.\n', run_ids(r), k);
storeResult(val, k, submit_options.wh);
results{k,r} = val;
catch ME
handleError(ME, k, run_ids(r));
results{k,r} = ME;
end
if submit_options.waitbar
waitbar(jobCounter/totalJobs, h, ...
sprintf('Completed %d/%d jobs', jobCounter, totalJobs));
end
end
end
otherwise
error('Unknown submit_mode "%s".', string(submit_mode))
end
wh = submit_options.wh;
linearResults = runBatch(@dsp_runid, jobs, ...
"mode", submit_mode, ...
"waitbar", submit_options.waitbar, ...
"waitbarMessage", "Processing Jobs...", ...
"numWorkers", 11, ...
"idleTimeout", 300, ...
"cancelExistingQueue", true, ...
"resultHandler", @storeResult, ...
"errorHandler", @handleError);
for idx = 1:totalJobs
r = jobs(idx).meta.runIndex;
k = jobs(idx).meta.jobIndex;
results{k, r} = linearResults{idx};
end
%% Local helpers %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function handleError(ME, jobIndex, run_id)
fprintf('[RunID %d, Job %d] ERROR [%s]: %s\n', run_id, jobIndex, ...
function handleError(ME, job, ~)
fprintf('[RunID %d, Job %d] ERROR [%s]: %s\n', job.meta.run_id, job.meta.jobIndex, ...
ME.identifier, ME.message);
for st = ME.stack'
fprintf(' %s:%d (%s)\n', st.file, st.line, st.name);
@@ -192,44 +82,116 @@ wh = submit_options.wh;
fprintf('Full report:\n%s\n', getReport(ME,'extended'));
end
function optionalVars = buildOptionalVars(jobIndex, wh)
optionalVars = struct();
function userParameters = buildUserParameters(jobIndex, wh)
userParameters = struct();
if ~isempty(wh.getDimension())
[vals, names] = wh.getPhysIndicesByLinIndex(jobIndex);
for pi = 1:numel(names)
optionalVars.(names{pi}) = vals{pi};
userParameters.(names{pi}) = vals{pi};
end
end
end
function storeResult(val, jobIndex, wh)
if ~isempty(wh)
wh.addValueToStorageByLinIdx(val.ffe_package, 'ffe_package', jobIndex);
wh.addValueToStorageByLinIdx(val.mlse_package, 'mlse_package', jobIndex);
wh.addValueToStorageByLinIdx(val.vnle_package, 'vnle_package', jobIndex);
wh.addValueToStorageByLinIdx(val.dbtgt_package,'dbtgt_package',jobIndex);
wh.addValueToStorageByLinIdx(val.dbenc_package,'dbenc_package',jobIndex);
wh.addValueToStorageByLinIdx(val.mlmlse_package,'mlmlse_package',jobIndex);
function validateNoRunIdSweepParameter(wh)
if isfield(wh.inputParams, "run_id")
error("submitJobs:RunIdParameterConflict", ...
"Do not define userParameters.run_id. submitJobs manages run_id as a storage axis.");
end
end
function p = setupParallelPool(numWorkers, idleTimeout)
% Ensure a pool exists at the right size & timeout
p = gcp('nocreate');
if isempty(p) || p.NumWorkers~=numWorkers
if ~isempty(p)
delete(p);
function storageWh = buildStorageWarehouse(sweepWh, runIds)
if isscalar(runIds)
storageWh = sweepWh;
return
end
storageParameters = struct();
storageParameters.run_id = runIds;
sweepParameterNames = fieldnames(sweepWh.inputParams);
for parameterIdx = 1:numel(sweepParameterNames)
parameterName = sweepParameterNames{parameterIdx};
storageParameters.(parameterName) = sweepWh.inputParams.(parameterName);
end
storageWh = DataStorage(storageParameters);
end
function storageJobIndex = buildStorageJobIndex(runId, userParameters, wh)
storageParameterNames = wh.fn;
if isempty(storageParameterNames)
storageJobIndex = 1;
return
end
storageSubscripts = cell(1, numel(storageParameterNames));
for parameterIdx = 1:numel(storageParameterNames)
parameterName = char(storageParameterNames(parameterIdx));
if strcmp(parameterName, "run_id")
parameterValue = runId;
else
parameterValue = userParameters.(parameterName);
end
p = parpool('local', numWorkers, 'IdleTimeout', idleTimeout);
storageSubscripts{parameterIdx} = wh.getIndexByPhys(parameterName, parameterValue);
end
% Cancel anything left in the pool's default queue
q = p.FevalQueue;
if ~isempty(q.QueuedFutures) || ~isempty(q.RunningFutures)
cancelAll(q);
fprintf('Canceled %d unfetched jobs from old queue.\n', ...
numel(q.QueuedFutures)+numel(q.RunningFutures));
if isscalar(storageSubscripts)
storageJobIndex = storageSubscripts{1};
else
storageJobIndex = sub2ind(wh.getStorageSize(), storageSubscripts{:});
end
end
function nameValue = structToNameValue(options)
names = fieldnames(options);
nameValue = cell(1, 2*numel(names));
for i = 1:numel(names)
nameValue{2*i - 1} = string(names{i});
nameValue{2*i} = options.(names{i});
end
end
function storeResult(val, job, ~)
if isempty(wh) || ~isstruct(val)
return
end
storageJobIndex = job.meta.storageJobIndex;
resultFields = fieldnames(val);
for resultIdx = 1:numel(resultFields)
storageName = resultFields{resultIdx};
if ~shouldStore(storageName)
continue
end
if isempty(val.(storageName))
continue
end
ensureStorage(storageName);
wh.addValueToStorageByLinIdx(val.(storageName), storageName, storageJobIndex);
end
end
function tf = shouldStore(storageName)
if isempty(submit_options.storePackages)
tf = true;
return
end
tf = any(string(storageName) == submit_options.storePackages);
end
function ensureStorage(storageName)
if ~isfield(wh.sto, storageName)
wh.addStorage(storageName);
end
end
end

View File

@@ -1,6 +1,6 @@
% Define the precomp path
precomp_path = "C:\Users\Silas\Documents\MATLAB\Datensätze\sioe_labor\precomp";
precomp_path = "W:\labdata\sioe_labor\precomp";
precomp_path = "C:\Users\Silas\Documents\MATLAB\imdd_simulation\projects\ECOC_2025_MPI";
% Step 1: Find all valid files (assume .mat files for ChannelFreqResp)
fileList = dir(fullfile(precomp_path, '*.mat'));
fileNames = {fileList.name};

View File

@@ -19,7 +19,7 @@ errorIndice= [];
if length(data_ref) == length(data_in)
bits = numel(data_in(:,options.skip_front+1:end));
bits = numel(data_in);
if options.returnErrorLocation == 0
errors = sum( data_in ~= data_ref,"all" );

View File

@@ -3,10 +3,14 @@ function burst_count = count_error_bursts(err_pos, max_burst_length)
% max_burst_length: Maximum length for which bursts are counted (e.g., 10)
% Sort the error positions to ensure they're in increasing order
err_pos = sort(err_pos);
err_pos = sort(err_pos(:).');
% Initialize burst_count array to hold the counts for each burst length
burst_count = zeros(1, max_burst_length);
if isempty(err_pos)
return
end
% Find the differences between consecutive error positions
diffs = diff(err_pos);

View File

@@ -1,55 +0,0 @@
% Gitter für lambda0 und S0
lambda0_vec = linspace(1260,1360,200);
S0_vec = linspace(0.06,0.1,200);
[Lambda0, S0] = meshgrid(lambda0_vec, S0_vec);
% Festen Betriebsparameter
lambda = 1293; % nm
L = 1; % km
% Dispersion berechnen (lineare Näherung)
D = S0 .* ( lambda - Lambda0 ) * L;
% D = (S0./4) .* ( lambda - (Lambda0.^4)./(lambda^3) ) * L;
%% 2D-Konturplot nur mit Linien und Text
figure('Color','w');
hold on
% Konturlinien
numLevels = 10;
levels = linspace(min(D(:)), max(D(:)), numLevels);
[C,h] = contour(S0, Lambda0, D, levels, ...
'LineWidth',1.5, ...
'ShowText','on', ...
'LabelFormat','%0.1f');
% cbrewer2-Colormap für die Linien
cmap = cbrewer2('div','RdYlGn', numLevels);
colormap(cmap);
% Achsenlinien
% yline(1310, '--k','ZDW_{mean}','LabelVerticalAlignment','top','LabelHorizontalAlignment','center');
x0 = 0.09;
% xline(x0, '--k','S_{0}','LabelHorizontalAlignment','left');
% Gaussian auf der x-Linie (S0 = 0.09)
mu_zwd = 1310; % nm
sigma_zwd = 2; % nm
zwd_vals = linspace(min(lambda0_vec), max(lambda0_vec), 500);
% PDF berechnen
gauss_pdf = (1/(sigma_zwd*sqrt(2*pi))) * exp(-0.5*((zwd_vals-mu_zwd)/sigma_zwd).^2);
% Normieren und auf eine sichtbare Breite skalieren
scale = 0.005; % passt die Maximal-Auslenkung in x-Richtung an
x_gauss = x0 + (gauss_pdf/max(gauss_pdf)) * scale;
% Plot
% plot(x_gauss, zwd_vals, 'LineWidth',2);
% Achsenbeschriftung & Titel
% Achsenbeschriftung & Titel
xlabel('S0 [ps / nm2 km]', 'FontSize', 12);
ylabel('ZDW [nm]', 'FontSize', 12);
title (sprintf('Dispersion: %d km; %d nm', L, lambda), 'FontSize', 14);
grid on
hold off

View File

@@ -1,146 +0,0 @@
%% ------------------------------------------------------------
% Contour plot: λ_null as function of bandwidth (f_target) and reach (L)
% ------------------------------------------------------------
% Parameters
lambda0 = 1310e-9; % [m]
S0 = 0.08; % [ps/(nm²·km)]
c = physconst('lightspeed');
% Sweep dimensions
f_targets = linspace(50e9, 120e9, 100); % [Hz] (x-axis)
L_values = linspace(0.5e3, 10e3, 100); % [m] (y-axis)
lambda_surface = zeros(numel(L_values), numel(f_targets));
Dacc_surface = zeros(numel(L_values), numel(f_targets));
% Outer loop over fiber length (since L must be scalar)
for iL = 1:numel(L_values)
L = L_values(iL);
[lambda_vec, Dacc_vec] = lambda_for_first_null_full(f_targets, L, lambda0, S0);
lambda_vec = 2*abs(lambda0 - lambda_vec);
if 0
fprintf('\n- %d km ------------------------------------\n',L);
fprintf(' f_null [GHz] lambda [nm] Dacc [ps/nm]\n');
fprintf('----------------------------------------------\n');
fprintf('%10.1f %8.2f %+8.3f\n',[f_targets(:)/1e9, lambda_vec(:)*1e9, Dacc_vec(:)].');
fprintf('----------------------------------------------\n\n');
end
lambda_surface(iL, :) = lambda_vec; % λ for each f_target
Dacc_surface(iL, :) = Dacc_vec; % corresponding accumulated dispersion
end
% Convert for plotting
lambda_surface_nm = lambda_surface * 1e9; % [nm]
L_km = L_values / 1000; % [km]
f_GHz = f_targets / 1e9; % [GHz]
%% Contour plot
figure('Color','w');
% Define wavelength contour levels [nm]
lambda_levels = [1260:10:1290, 1290:5:1300, 1300:2.5:1310];
lambda_levels = [100:-20:50, 50:-10:30,30:-5:0];
% Contour plot
contour(f_GHz, L_km, lambda_surface_nm, lambda_levels, ...
'LineWidth', 1.5, ...
'ShowText', 'on', ...
'LabelFormat', '%.0f nm');
% Colormap and colorbar
colormap((cbrewer2('RdYlGn',100)));
colorbar;
clim([0 100]);
% Axis formatting
xlabel('Signal Bandwidth [GHz]');
ylabel('Fiber length [km]');
legend('$\Delta \lambda$')
% X-axis ticks at 56 : 16 : 150 GHz
xticks(56:8:150);
grid on; box on;
%% Optional: overlay accumulated-dispersion contours
if 0
hold on;
[CS, h] = contour(f_GHz, L_km, Dacc_surface, 10, 'k--', 'LineWidth', 0.8);
clabel(CS, h, 'Color','k', 'FontSize',8);
end
function [lambda_vec, Dacc_vec] = lambda_for_first_null_full(f_target, L, lambda0, S0)
% lambda_for_first_null_full (stable, single-branch + validity checks)
% --------------------------------------------------------------------
% Computes the wavelength(s) at which the first IM/DD fading null
% occurs at frequency/ies f_target using the full dispersion model:
%
% D(lambda) = (S0/4)*(lambda - lambda0^4 / lambda^3)
%
% Restricted to the NORMAL-dispersion branch (λ < λ0),
% and valid only in the O-band (12601360 nm).
%
% Inputs:
% f_target - scalar or vector of target null frequencies [Hz]
% L - fiber length [m]
% lambda0 - zero-dispersion wavelength (ZDW) [m]
% S0 - dispersion slope at ZDW [ps/(nm²·km)]
%
% Outputs:
% lambda_vec - wavelength(s) [m] where first null occurs (clamped to O-band)
% Dacc_vec - accumulated dispersion(s) [ps/nm] (NaN if out of valid range)
% --------------------------------------------------------------------
c = physconst('lightspeed');
S0_si = S0 * 1e3; % ps/(nm²·km) -> s/(m³)
% Define O-band boundaries (in meters)
lambda_min = 1255e-9;
lambda_max = 1361e-9;
% Force column vector
f_target = f_target(:);
N = numel(f_target);
lambda_vec = NaN(N,1);
Dacc_vec = NaN(N,1);
for k = 1:N
RHS = c * 0.5 / (f_target(k)^2 * L);
% Normal-dispersion branch (λ < λ0)
fun = @(lambda) -(S0_si/4).*(lambda - (lambda0^4)./(lambda.^3)).*lambda.^2 - RHS;
% Limit the search to [λ_min, λ0)
try
lambda_sol = fzero(fun, [lambda_min, lambda0 * 0.999]);
catch
% If the zero is not within bounds, skip this point
lambda_sol = NaN;
end
% Validate solution
if isnan(lambda_sol) || lambda_sol < lambda_min || lambda_sol > lambda_max
lambda_vec(k) = NaN;
Dacc_vec(k) = NaN;
continue
end
% Compute D(lambda) and accumulated dispersion
D_lambda = (S0_si/4) * (lambda_sol - (lambda0^4)/(lambda_sol^3)) / 1e-6; % ps/(nm·km)
Dacc_val = D_lambda * (L/1000); % ps/nm
% Sanity bound on dispersion (avoid unphysical > ±100 ps/nm)
if abs(Dacc_val) > 100
lambda_vec(k) = NaN;
Dacc_vec(k) = NaN;
else
lambda_vec(k) = lambda_sol;
Dacc_vec(k) = Dacc_val;
end
end
end

View File

@@ -1,62 +0,0 @@
function tau_error = modifiedGodardTimingRecovery(rx, N, eta, beta)
% modifiedGodardTimingRecovery
%
% This function estimates the symbol timing error using the modified Godard
% approach in the frequency domain as described in:
%
% "Modified Godard Timing Recovery for Non-Integer Oversampling Receivers"
% Appl. Sci. 2017, 7, 655. :contentReference[oaicite:0]{index=0}&#8203;:contentReference[oaicite:1]{index=1}
%
% Inputs:
% rx - Received time-domain signal (vector)
% N - FFT size (should be an even integer)
% eta - Effective oversampling factor used for timing recovery (eta > 1)
% beta - Roll-off related parameter (0 < beta <= 1)
%
% Output:
% tau_error - Estimated timing error (in sample units)
%
% Implementation Notes:
% 1. The function computes an N-point FFT of the first N samples of rx.
% 2. It then determines an offset (Delta) defined as:
% offset = round((1 - 1/eta) * N)
% 3. To avoid index overflow, the summation is taken over indices k from 1 to
% floor(N/2) - offset.
% 4. The timing error is estimated as:
% tau_error = ( (1+beta)/(2*eta*N - 1) * sum(phase difference) ) / (2*pi)
% where the phase difference is (angle(R(k)) - angle(R(k+offset)))
%
% Make sure that the input signal rx contains at least N samples.
% Check input length
if length(rx) < N
error('Input signal length must be at least N.');
end
% Compute the N-point FFT of the first N samples of rx
R = fft(rx(1:N), N);
% Determine the offset based on the oversampling factor (eta)
offset = round((1 - 1/eta) * N);
% Define the summation range to avoid index overflow
k_min = 1;
k_max = floor(N/2) - offset;
if k_max < k_min
error('Chosen parameters result in an empty summation range. Adjust N, eta, or beta.');
end
% Compute the sum of phase differences over the selected frequency bins
phase_diff_sum = 0;
for k = k_min:k_max
phase_k = angle(R(k));
phase_k_offset = angle(R(k + offset));
phase_diff_sum = phase_diff_sum + (phase_k - phase_k_offset);
end
% Normalization factor as per the modified Godard algorithm
norm_factor = (1 + beta) / (2 * eta * N - 1);
% Estimate the timing error in sample units
tau_error = (norm_factor * phase_diff_sum) / (2 * pi);
end

View File

@@ -1,40 +0,0 @@
%% ============================================================
% Minimal IM/DD Power Fading Plot
% ============================================================
%% Fiber and system parameters
lambda0 = 1310e-9; % zero-dispersion wavelength [m]
lambda = 1275e-9; % operating wavelength [m]
S0 = 0.08; % dispersion slope [ps/(nm²·km)]
L = 10e3; % fiber length [m]
c = physconst('lightspeed');
%% Derived quantities
S0_si = S0 * 1e3; % s/m³
D_lambda = (S0/4) * (lambda*1e9 - (lambda0*1e9)^4/(lambda*1e9)^3); % ps/(nm·km)
D_si = D_lambda * 1e-6; % s/m²
b2 = -D_si * lambda^2 / (2*pi*c); % s²/m
%% Frequency grid
f_max = 150e9;
f = linspace(0, f_max, 4000); % [Hz]
%% IM/DD transfer function (power fading)
phi = 2*pi^2 * b2 * f.^2 * L;
H = abs(cos(phi));
%% Plot
figure('Color','w');
plot(f/1e9, 10*log10(H), 'LineWidth', 1.8);
grid on; box on;
xlabel('Frequency [GHz]');
ylabel('Magnitude [dB]');
title(sprintf('IM/DD Power Fading |H| for λ = %.1f nm, L = %.1f km', lambda*1e9, L/1000));
ylim([-30 0]);
%% Mark analytic first-null frequency
f_null = sqrt(c*(0.5)/(abs(D_si)*lambda^2*L));
xline(f_null/1e9, 'r--', 'LineWidth', 1.2, ...
'Label', sprintf('f_{null}=%.1f GHz', f_null/1e9), ...
'LabelOrientation', 'horizontal', 'LabelVerticalAlignment', 'bottom');

View File

@@ -6,10 +6,12 @@ function beautifyBERplot(options)
% beautifyBERplot
% beautifyBERplot("polyfit",true,"polyorder",1)
% beautifyBERplot("polyfit",true,"fitmethod","pchip")
% beautifyBERplot("changemarkers",true)
arguments
options.logscale (1,1) logical = 1
options.setmarkers (1,1) logical = 1
options.changemarkers (1,1) logical = 0
options.setcolors (1,1) logical = 1
options.polyfit (1,1) logical = 0
options.polyorder (1,1) double = 2
@@ -19,66 +21,87 @@ end
ax = gca;
% ---------------------------------------------------------
% Extract *only* real data lines from ax.Children
% Extract real data objects from ax.Children
% ---------------------------------------------------------
children = ax.Children;
isLine = arrayfun(@(h) isa(h,'matlab.graphics.chart.primitive.Line'), children);
lines = children(isLine);
isScatter = arrayfun(@(h) isa(h,'matlab.graphics.chart.primitive.Scatter'), children);
% Remove helper lines (e.g. yline, fit overlays)
lines = lines(~strcmp({lines.Tag},'ConstantLine'));
isConstantLine = arrayfun(@(h) isprop(h,'Tag') && strcmp(h.Tag,'ConstantLine'), children);
n = numel(lines);
if n == 0
return
end
dataObjects = children((isLine | isScatter) & ~isConstantLine);
n = numel(dataObjects);
% ---------------------------------------------------------
% Fixed color palette (deterministic across figures)
% ---------------------------------------------------------
try
cmap = linspecer(n);
catch
cmap = lines(n).Color; %#ok<NASGU>
cmap = linespecer_fallback(n);
if n > 0
try
cmap = linspecer(n);
catch
cmap = linespecer_fallback(n);
end
end
markers = {'o','s','o','o','^','v','d','>'};
lw = 0.8;
ms = 4;
% Simple marker set that exports cleanly with common MATLAB-to-TikZ workflows.
markers = {'o','square','diamond','^','v','>','<','pentagram'};
lw = 1;
ms = 5;
applyMarkers = options.setmarkers || options.changemarkers;
% ---------------------------------------------------------
% Apply line + marker styling
% Apply line/scatter + marker styling
% ---------------------------------------------------------
for i = 1:n
ln = lines(n-i+1); % preserve plotting order
ln.LineWidth = lw;
if options.setcolors
ln.Color = cmap(i,:);
end
dataObj = dataObjects(n-i+1); % preserve plotting order
if isa(dataObj,'matlab.graphics.chart.primitive.Line')
dataObj.LineWidth = lw;
if options.setmarkers
if strcmp(ln.Marker,'none')
ln.Marker = markers{mod(i-1,numel(markers))+1};
end
ln.MarkerSize = ms;
if options.setcolors
ln.MarkerEdgeColor = cmap(i,:);
dataObj.Color = cmap(i,:);
end
if applyMarkers
if options.changemarkers || strcmp(dataObj.Marker,'none')
dataObj.Marker = markers{mod(i-1,numel(markers))+1};
end
dataObj.MarkerSize = ms;
if options.setcolors
dataObj.MarkerEdgeColor = cmap(i,:);
end
dataObj.MarkerFaceColor = [1 1 1];
end
elseif isa(dataObj,'matlab.graphics.chart.primitive.Scatter')
if options.setcolors
dataObj.CData = cmap(i,:);
dataObj.MarkerEdgeColor = cmap(i,:);
markerFaceColor = dataObj.MarkerFaceColor;
hasNoFace = ischar(markerFaceColor) && strcmp(markerFaceColor,'none');
if ~hasNoFace
dataObj.MarkerFaceColor = cmap(i,:);
end
end
if applyMarkers
if options.changemarkers || strcmp(dataObj.Marker,'none')
dataObj.Marker = markers{mod(i-1,numel(markers))+1};
end
dataObj.SizeData = ms;
end
ln.MarkerFaceColor = [1 1 1];
end
end
% ---------------------------------------------------------
% Optional smoothing / fitting overlay
% ---------------------------------------------------------
if options.polyfit
if options.polyfit && n > 0
hold on
for i = 1:n
ln = lines(n-i+1);
x = ln.XData(:);
y = ln.YData(:);
dataObj = dataObjects(n-i+1);
x = dataObj.XData(:);
y = dataObj.YData(:);
valid = isfinite(x) & isfinite(y);
if sum(valid) < options.polyorder+1
@@ -112,7 +135,15 @@ if options.polyfit
yf = polyval(p,xf);
end
lightcol = ln.Color + 0.4*(1-ln.Color);
if isa(dataObj,'matlab.graphics.chart.primitive.Line')
basecol = dataObj.Color;
else
basecol = dataObj.CData(1,:);
if numel(basecol) ~= 3
basecol = cmap(i,:);
end
end
lightcol = basecol + 0.4*(1-basecol);
lightcol(lightcol>1)=1;
plot(xf,yf,'-','Color',lightcol,...

View File

@@ -1,5 +0,0 @@
function signal_out = awgn_channel(signal_in)
signal_out = signal_in;
end

View File

@@ -1,23 +1,29 @@
function mat2tikz_improved(filename)
function mat2tikz_improved(filename,options)
arguments
% Default to the path in your example if no argument is provided
filename (1,1) string = 'C:\Users\Silas\Documents\Dissertation\00_Examples\tikz\textfig.tikz';
options.cleanfigure = false;
options.cleanTargetResolution = 600;
options.cleanScalePrecision (1,1) double = 1;
end
if options.cleanfigure
cleanfigure( ...
'targetResolution', options.cleanTargetResolution, ...
'scalePrecision', options.cleanScalePrecision);
end
cleanfigure;
matlab2tikz(char(filename), ...
'width','\fwidth', ...
'height','\fheight', ...
'showInfo',false, ...
'extraAxisOptions',{ ...
'legend style={font=\footnotesize}', ...
'xlabel style={font=\color{white!15!black},font=\small},',...
'ylabel style={font=\color{white!15!black},font=\small},',...
'width', '\fwidth', ...
'height', '\fheight', ...
'showInfo', false, ...
'extraAxisOptions', { ...
'xlabel style={font=\color{white!15!black}\small}', ...
'ylabel style={font=\color{white!15!black}\small}', ...
'scaled ticks=false', ...
'tick label style={/pgf/number format/fixed, /pgf/number format/1000 sep={}}', ...
'every axis/.append style={font=\scriptsize}', ...
'legend columns=1', ...
'every axis/.append style={font=\scriptsize}',...
'legend columns=1',...
'legend style={at={(0.02,0.98)},font=\footnotesize,draw=black!60,rounded corners=2pt,inner sep=1pt,fill=white,column sep=6pt,anchor= north west}',...
'legend style={at={(0.02,0.98)},draw=white!0!white,font=\scriptsize,inner sep=0.1pt,fill=white,column sep=1pt,anchor= north west}',...
'every axis/.append style={font=\scriptsize}',...
'legend style={at={(0.98,0.98)}, anchor=north east, font=\scriptsize, draw=black!100, rounded corners=2pt, inner sep=1pt, fill=white, column sep=2pt}' ...
});
end
end

View File

@@ -0,0 +1,317 @@
%% Plot S-parameters from Touchstone S2P or S4P (schema-aware, no RF Toolbox)
clear; clc;
[file,path] = uigetfile({"*.s2p;*.S2P;*.s4p;*.S4P","Touchstone (*.s2p, *.s4p)"});
if isequal(file,0); return; end
filename = fullfile(path,file);
%%
% Decide port count by extension (minimal-invasive, robust)
[~,~,ext] = fileparts(file);
ext = upper(ext);
if strcmp(ext,'.S4P')
[matrix, meta] = loads4p_schema(filename);
N = 4;
elseif strcmp(ext,'.S2P')
[matrix, meta] = loads2p(filename); % your original loader kept (below)
N = 2;
else
error("Unsupported extension: %s", ext);
end
% Frequency in GHz
frex = matrix(:,1);
if isfield(meta,'freqScaleHz') && ~isempty(meta.freqScaleHz)
frex = frex * meta.freqScaleHz / 1e9; % convert to GHz
else
% fallback heuristic (your original)
for i = 1:numel(frex)
if frex(i) > 1e4
frex(i) = frex(i).*1e-9;
end
end
end
figure(101);
hold on;
% overlay indexing: count lines already there
if N == 2
lines = numel(findall(gcf, 'type','line'))/4 + 1;
else
lines = numel(findall(gcf, 'type','line'))/16 + 1;
end
% styles (kept)
[CC, LL, MM] = ndgrid({1,2,3,4}, {'-', ':', '-.', '--'}, {'none','+', 'o', '*', 'square', 'diamond'});
combinations = [CC(:), LL(:), MM(:)];
cols = [0.3467 0.5360 0.6907
0.9153 0.2816 0.2878
0.4416 0.7490 0.4322
1.0000 0.5984 0.2000];
% -------------------- Plotting --------------------
if N == 2
heads = ["S11","S12","S22","S21"];
% matrix layout from loads2p:
% col pairs:
% 2-3 S11, 4-5 S21, 6-7 S12, 8-9 S22
colPairs = [2 3; 6 7; 8 9; 4 5]; % order matches heads above
subplots = 1;
for k = 1:4
if subplots, subplot(2,2,k); else, figure('Color','w'); end
hold on; grid on;
reCol = colPairs(k,1);
imCol = colPairs(k,2);
% S-parameter magnitude in dB: 20*log10(|S|)
response = 20*log10(abs(matrix(:,reCol) + 1i.*matrix(:,imCol)));
response = movmean(response,1);
plot(frex, response, ...
"Color", cols(combinations{lines,1},:), ...
"DisplayName", file, ...
"LineStyle", combinations{lines,2}, ...
"Marker", combinations{lines,3}, ...
"MarkerSize", 3);
if k == 2 || k == 3
ylim([-6 2]);
else
ylim([-30 2]);
end
xlim([0 100]);
title(heads(k));
xlabel('Frequency in GHz');
ylabel('|S| (dB)');
legend('Location','best');
end
sgtitle(sprintf('S-Parameters (2-port) [%s]', meta.headerLine));
elseif N == 4
% For s4p schema we plot all Sij in a 4x4 grid
% matrix layout from loads4p_schema:
% columns: 1=f, then pairs in order:
% S11,S12,S13,S14, S21..S24, S31..S34, S41..S44
subplots = 1;
for i = 1:4
for j = 1:4
sp = (i-1)*4 + j;
if subplots, subplot(4,4,sp); else, figure('Color','w'); end
hold on; grid on;
idx = (i-1)*4 + j; % 1..16 for Sij in row-major
reCol = 1 + 2*(idx-1) + 1; % after freq col
imCol = 1 + 2*(idx-1) + 2;
response = 20*log10(abs(matrix(:,reCol) + 1i.*matrix(:,imCol)));
plot(frex, response, ...
"Color", cols(combinations{lines,1},:), ...
"DisplayName", file, ...
"LineStyle", combinations{lines,2}, ...
"Marker", combinations{lines,3}, ...
"MarkerSize", 2);
xlim([0 100]);
title(sprintf("S%d%d", i, j));
set(gca,'FontSize',8);
% reduce clutter
if j ~= 1, yticklabels([]); end
if i ~= 4, xticklabels([]); end
if j == 1, ylabel('|S| (dB)'); end
if i == 4, xlabel('GHz'); end
end
end
sgtitle(sprintf('S-Parameters (4-port) [%s]', meta.headerLine));
end
%% ========================================================================
% Loader for S4P with the EXACT provided schema (4 lines per frequency)
% ========================================================================
function [matrix, meta] = loads4p_schema(filepath)
meta = struct();
meta.headerLine = "";
meta.freqScaleHz = []; % Hz per file unit (e.g., GHz -> 1e9)
fid = fopen(filepath,'r');
if fid < 0, error("Cannot open file: %s", filepath); end
cleaner = onCleanup(@() fclose(fid));
% Parse header: find "#"
freUnit = "";
while true
t = fgetl(fid);
if ~ischar(t), break; end
ts = strtrim(t);
if startsWith(ts,'#')
meta.headerLine = ts;
parts = split(upper(string(ts)));
if numel(parts) >= 2, freUnit = parts(2); end
switch freUnit
case "HZ", meta.freqScaleHz = 1;
case "KHZ", meta.freqScaleHz = 1e3;
case "MHZ", meta.freqScaleHz = 1e6;
case "GHZ", meta.freqScaleHz = 1e9;
otherwise, meta.freqScaleHz = [];
end
break;
end
end
data = [];
while true
l1 = nextNumericLine(fid);
if strlength(l1) == 0
break;
end
% IMPORTANT: sscanf wants char for robust behavior
v1 = sscanf(char(l1), '%f').';
if numel(v1) < 9
error("S4P schema mismatch: expected 9 numbers on first line of a block, got %d.\nLine: %s", numel(v1), char(l1));
end
l2 = nextNumericLine(fid); if strlength(l2)==0, error("Unexpected EOF after first S4P line."); end
l3 = nextNumericLine(fid); if strlength(l3)==0, error("Unexpected EOF after second S4P line."); end
l4 = nextNumericLine(fid); if strlength(l4)==0, error("Unexpected EOF after third S4P line."); end
v2 = sscanf(char(l2), '%f').';
v3 = sscanf(char(l3), '%f').';
v4 = sscanf(char(l4), '%f').';
if numel(v2) < 8 || numel(v3) < 8 || numel(v4) < 8
error("S4P schema mismatch: expected 8 numbers on continuation lines.\nL2: %s\nL3: %s\nL4: %s", char(l2), char(l3), char(l4));
end
f = v1(1);
s1 = v1(2:9); % S11..S14 (re/im pairs)
s2 = v2(1:8); % S21..S24
s3 = v3(1:8); % S31..S34
s4 = v4(1:8); % S41..S44
row = [f, s1, s2, s3, s4]; % 1 + 32 = 33 columns
data = [data; row]; %#ok<AGROW>
end
matrix = data;
end
function ln = nextNumericLine(fid)
ln = "";
while true
t = fgetl(fid);
if ~ischar(t)
ln = "";
return;
end
ts = strtrim(t);
% Skip blanks and comments and header lines
if ts=="" || startsWith(ts,'!') || startsWith(ts,'#')
continue;
end
% Remove inline comment after '!' if present
excl = strfind(ts,'!');
if ~isempty(excl)
ts = strtrim(extractBefore(ts, excl(1)));
if ts==""; continue; end
end
% Keep only if there's at least one numeric token
if ~isempty(regexp(ts,'[-+]?\d*\.?\d+(?:[eEdD][-+]?\d+)?','once'))
ln = string(ts);
return;
end
end
end
%% ========================================================================
% Your original loads2p (kept as-is, but returns meta too)
% ========================================================================
function [matrix, meta] = loads2p(filepath)
meta = struct();
meta.headerLine = "";
meta.freqScaleHz = [];
% Try to read # line for unit scaling (minimal invasive)
fid = fopen(filepath,'r');
if fid >= 0
cleaner = onCleanup(@() fclose(fid));
while true
t = fgetl(fid);
if ~ischar(t), break; end
ts = strtrim(t);
if startsWith(ts,'#')
meta.headerLine = ts;
parts = split(upper(string(ts)));
if numel(parts) >= 2
switch parts(2)
case "HZ", meta.freqScaleHz = 1;
case "KHZ", meta.freqScaleHz = 1e3;
case "MHZ", meta.freqScaleHz = 1e6;
case "GHZ", meta.freqScaleHz = 1e9;
end
end
break;
end
end
end
startRow = 2;
formatSpec = '%20s%25s%23s%23s%23s%23s%23s%23s%23s%s%[^\n\r]';
fileID = fopen(filepath,'r');
dataArray = textscan(fileID, formatSpec, 'Delimiter', '', 'WhiteSpace', '', ...
'TextType', 'string', 'HeaderLines' ,startRow-1, 'ReturnOnError', false, 'EndOfLine', '\r\n');
fclose(fileID);
raw = repmat({''},length(dataArray{1}),length(dataArray)-1);
for col=1:length(dataArray)-1
raw(1:length(dataArray{col}),col) = mat2cell(dataArray{col}, ones(length(dataArray{col}), 1));
end
numericData = NaN(size(dataArray{1},1),size(dataArray,2));
for col=[1,2,3,4,5,6,7,8,9,10]
rawData = dataArray{col};
for row=1:size(rawData, 1)
regexstr = '(?<prefix>.*?)(?<numbers>([-]*(\d+[\,]*)+[\.]{0,1}\d*[eEdD]{0,1}[-+]*\d*[i]{0,1})|([-]*(\d+[\,]*)*[\.]{1,1}\d+[eEdD]{0,1}[-+]*\d*[i]{0,1}))(?<suffix>.*)';
try
result = regexp(rawData(row), regexstr, 'names');
numbers = result.numbers;
invalidThousandsSeparator = false;
if numbers.contains(',')
thousandsRegExp = '^[-/+]*\d+?(\,\d{3})*\.{0,1}\d*$';
if isempty(regexp(numbers, thousandsRegExp, 'once'))
numbers = NaN;
invalidThousandsSeparator = true;
end
end
if ~invalidThousandsSeparator
numbers = textscan(char(strrep(numbers, ',', '')), '%f');
numericData(row, col) = numbers{1};
raw{row, col} = numbers{1};
end
catch
raw{row, col} = rawData{row};
end
end
end
R = cellfun(@(x) ~isnumeric(x) && ~islogical(x),raw);
raw(R) = {NaN};
matrix = cell2mat(raw);
end

View File

@@ -0,0 +1,193 @@
function violins = violinplot(data, cats, varargin)
%Violinplots plots violin plots of some data and categories
% VIOLINPLOT(DATA) plots a violin of a double vector DATA
%
% VIOLINPLOT(DATAMATRIX) plots violins for each column in
% DATAMATRIX.
%
% VIOLINPLOT(DATAMATRIX, CATEGORYNAMES) plots violins for each
% column in DATAMATRIX and labels them according to the names in the
% cell-of-strings CATEGORYNAMES.
%
% In the cases above DATA and DATAMATRIX can be a vector or a matrix,
% respectively, either as is or wrapped in a cell.
% To produce violins which have one distribution on one half and another
% one on the other half, DATA and DATAMATRIX have to be cell arrays
% with two elements, each containing a vector or a matrix. The number of
% columns of the two data sets has to be the same.
%
% VIOLINPLOT(DATA, CATEGORIES) where double vector DATA and vector
% CATEGORIES are of equal length; plots violins for each category in
% DATA.
%
% VIOLINPLOT(TABLE), VIOLINPLOT(STRUCT), VIOLINPLOT(DATASET)
% plots violins for each column in TABLE, each field in STRUCT, and
% each variable in DATASET. The violins are labeled according to
% the table/dataset variable name or the struct field name.
%
% violins = VIOLINPLOT(...) returns an object array of
% <a href="matlab:help('Violin')">Violin</a> objects.
%
% VIOLINPLOT(..., 'PARAM1', val1, 'PARAM2', val2, ...)
% specifies optional name/value pairs for all violins:
% 'Width' Width of the violin in axis space.
% Defaults to 0.3
% 'Bandwidth' Bandwidth of the kernel density estimate.
% Should be between 10% and 40% of the data range.
% 'ViolinColor' Fill color of the violin area and data points. Accepts
% 1x3 color vector or nx3 color vector where n = num
% groups. In case of two data sets being compared it can
% be an array of up to two cells containing nx3
% matrices.
% Defaults to the next default color cycle.
% 'ViolinAlpha' Transparency of the violin area and data points.
% Can be either a single scalar value or an array of
% up to two cells containing scalar values.
% Defaults to 0.3.
% 'MarkerSize' Size of the data points, if shown.
% Defaults to 24
% 'MedianMarkerSize' Size of the median indicator, if shown.
% Defaults to 36
% 'EdgeColor' Color of the violin area outline.
% Defaults to [0.5 0.5 0.5]
% 'BoxColor' Color of the box, whiskers, and the outlines of
% the median point and the notch indicators.
% Defaults to [0.5 0.5 0.5]
% 'MedianColor' Fill color of the median and notch indicators.
% Defaults to [1 1 1]
% 'ShowData' Whether to show data points.
% Defaults to true
% 'ShowNotches' Whether to show notch indicators.
% Defaults to false
% 'ShowMean' Whether to show mean indicator
% Defaults to false
% 'ShowBox' Whether to show the box.
% Defaults to true
% 'ShowMedian' Whether to show the median indicator.
% Defaults to true
% 'ShowWhiskers' Whether to show the whiskers
% Defaults to true
% 'GroupOrder' Cell of category names in order to be plotted.
% Defaults to alphabetical ordering
% Copyright (c) 2016, Bastian Bechtold
% This code is released under the terms of the BSD 3-clause license
hascategories = exist('cats','var') && not(isempty(cats));
%parse the optional grouporder argument
%if it exists parse the categories order
% but also delete it from the arguments passed to Violin
grouporder = {};
idx=find(strcmp(varargin, 'GroupOrder'));
if ~isempty(idx) && numel(varargin)>idx
if iscell(varargin{idx+1})
grouporder = varargin{idx+1};
varargin(idx:idx+1)=[];
else
error('Second argument of ''GroupOrder'' optional arg must be a cell of category names')
end
end
% check and correct the structure of ViolinColor input
idx=find(strcmp(varargin, 'ViolinColor'));
if ~isempty(idx) && iscell(varargin{idx+1})
if length(varargin{idx+1}(:))>2
error('ViolinColor input can be at most a two element cell array');
end
elseif ~isempty(idx) && isnumeric(varargin{idx+1})
varargin{idx+1} = varargin(idx+1);
end
% check and correct the structure of ViolinAlpha input
idx=find(strcmp(varargin, 'ViolinAlpha'));
if ~isempty(idx) && iscell(varargin{idx+1})
if length(varargin{idx+1}(:))>2
error('ViolinAlpha input can be at most a two element cell array');
end
elseif ~isempty(idx) && isnumeric(varargin{idx+1})
varargin{idx+1} = varargin(idx+1);
end
% tabular data
if isa(data, 'dataset') || isstruct(data) || istable(data)
if isa(data, 'dataset')
colnames = data.Properties.VarNames;
elseif istable(data)
colnames = data.Properties.VariableNames;
elseif isstruct(data)
colnames = fieldnames(data);
end
catnames = {};
if isempty(grouporder)
for n=1:length(colnames)
if isnumeric(data.(colnames{n}))
catnames = [catnames colnames{n}]; %#ok<*AGROW>
end
end
catnames = sort(catnames);
else
for n=1:length(grouporder)
if isnumeric(data.(grouporder{n}))
catnames = [catnames grouporder{n}];
end
end
end
for n=1:length(catnames)
thisData = data.(catnames{n});
violins(n) = Violin({thisData}, n, varargin{:});
end
set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames);
set(gca,'Box','on');
return
elseif iscell(data) && length(data(:))==2 % cell input
if not(size(data{1},2)==size(data{2},2))
error('The two input data matrices have to have the same number of columns');
end
elseif iscell(data) && length(data(:))>2 % cell input
error('Up to two datasets can be compared');
elseif isnumeric(data) % numeric input
% 1D data, one category for each data point
if hascategories && numel(data) == numel(cats)
if isempty(grouporder)
cats = categorical(cats);
else
cats = categorical(cats, grouporder);
end
catnames = (unique(cats)); % this ignores categories without any data
catnames_labels = {};
for n = 1:length(catnames)
thisCat = catnames(n);
catnames_labels{n} = char(thisCat);
thisData = data(cats == thisCat);
violins(n) = Violin({thisData}, n, varargin{:});
end
set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames_labels);
set(gca,'Box','on');
return
else
data = {data};
end
end
% 1D data, no categories
if not(hascategories) && isvector(data{1})
violins = Violin(data, 1, varargin{:});
set(gca, 'XTick', 1);
% 2D data with or without categories
elseif ismatrix(data{1})
for n=1:size(data{1}, 2)
thisData = cellfun(@(x)x(:,n),data,'UniformOutput',false);
violins(n) = Violin(thisData, n, varargin{:});
end
set(gca, 'XTick', 1:size(data{1}, 2));
if hascategories && length(cats) == size(data{1}, 2)
set(gca, 'XTickLabels', cats);
end
end
set(gca,'Box','on');
end

View File

@@ -2234,12 +2234,12 @@ function [tikzMarkerSize, isDefault] = ...
% the acute angle (at the top and the bottom of the diamond)
% is a manually measured 75 degrees (in TikZ, and MATLAB
% probably very similar); use this as a base for calculations
tikzMarkerSize = matlabMarkerSize(:) / 2 / atan(75/2 *pi/180);
tikzMarkerSize = matlabMarkerSize(:) / 1.7 ;%/ atan(75/2 *pi/180);
case {'^','v','<','>'}
% for triangles, matlab takes the height
% and tikz the circumcircle radius;
% the triangles are always equiangular
tikzMarkerSize = matlabMarkerSize(:) / 2 * (2/3);
tikzMarkerSize = matlabMarkerSize(:) / 1.7;% * (2/3);
otherwise
error('matlab2tikz:translateMarkerSize', ...
'Unknown matlabMarker ''%s''.', matlabMarker);

473
Libs/move_it/prms_move_it.m Normal file
View File

@@ -0,0 +1,473 @@
function [data_out ,state] = prms(data_in, state, para)
global loop loopmax
if nargin==0
if loop==-1,
% Abfrage der Ein- und Ausgnge
noinp=0 ;
noout=1;
% Zuordnung zu den Ausgangsvariablen
data_out=noinp;
state=noout;
else
% Parameterdefinition
para.bl=128;
comment.bl='Block length (Length of output vector)';
comment.short.bl='length';
comment.type.bl='number';
comment.level.bl=1;
para.rand=0 ;
comment.rand='Random (True) or pseudo-random (False)';
comment.short.rand='rand/prms';
comment.type.rand='boolean';
comment.level.rand=1;
para.dimension=2;
comment.dimension='dimension (i.e. number of parallel bit streams)';
comment.type.dimension='number';
comment.short.dimension='dimension';
comment.level.dimension=1;
para.order=2;
comment.order='order of PRMS (needs to be adapted to memory of DUT)';
comment.type.order='number';
comment.short.order='order';
comment.level.order=1;
para.skip=0;
comment.skip='Skip the first n Bits of sequence';
comment.type.skip='number';
comment.short.skip='skip';
comment.level.skip=1;
para.bruijn=1;
comment.bruijn='extends the PRBS sequence of length 2^n-1 to a deBruijn sequence of 2^n';
comment.type.bruijn='boolean';
comment.short.bruijn='deBruijn';
comment.level.bruijn=1;
para.reset_prms=0;
comment.reset_prms='reset the sequence generator every loop';
comment.type.reset_prms='boolean';
comment.short.reset_prms='Reset every loop';
comment.level.reset_prms=1;
para.method=1;
comment.method='false= old method , true = new method';
comment.type.method='Fast (c-file)|Slow (Matlab file)';
comment.short.method='method';
comment.level.method=1;
data_out=para;
state=comment;
end;
else
% Initialisierung
if loop==0,
% check for correct parameters
checkskalar(getfield(inputname(2),{1:length(inputname(2))-6}),para,[{'bl'} {'skip'} {'dimension'} {'order'}],...
[{'length'} {'skip'} {'dimension'} {'order'}]);
checkinteger(getfield(inputname(2),{1:length(inputname(2))-6}),para,[{'bl'} {'skip'} {'dimension'} {'order'}],...
[{'length'} {'skip'} {'dimension'} {'order'}]);
checkpos(getfield(inputname(2),{1:length(inputname(2))-6}),para,[{'bl'} {'skip'} {'dimension'} {'order'}],...
[{'length'} {'skip'} {'dimension'} {'order'}]);
% determine order of periodicity
% initializing of all state parameters
state.dimension = para.dimension;
state.order = para.order;
state.periodicity = para.dimension*para.order;
state.bl = para.bl;
state.reset_prms = para.reset_prms;
if ~para.rand % PRMS
%------------------------------------------------------------------
% prepare Matrix to map PRBS bits to PRMS values
%------------------------------------------------------------------
% create lookup-table for primitive element
if state.dimension>2,
% general case
bchpolynomial=zeros(1,state.dimension+1);
bchpolynomial([1 srgtaps(state.dimension)+1])=1;
betatable=fliplr(cyclgen_local(2^state.dimension-1,bchpolynomial).');
elseif state.dimension==2
% special case, by-hand solution
betatable=[0 1;1 0;1 1];
elseif state.dimension==1
% special case, by-hand solution
betatable=1;
end;
binvec = 2.^(state.dimension-1:-1:0);
modulo_mask = 2^state.dimension-1;
betatable_sort_forward = sum(repmat(binvec,2^state.dimension-1,1).*betatable,2);
[dummy,betatable_sort_inverse] = sort(betatable_sort_forward);
betatable_sort_inverse = betatable_sort_inverse-1;
% perform polynomial division by x+beta
dividend=zeros(state.dimension+1,2);
dividend([state.dimension-srgtaps(state.dimension)+1 end],1)=1;
h=zeros(state.dimension,2);
for digit=1:state.dimension,
% add new coefficient
h(digit,1:2)=dividend(digit,1:2);
dividend(digit,1:2)=[0 0];
% update dividend
newdiv_binary=xor(betatable(dividend(digit+1,2)+1,:)*dividend(digit+1,1),...
betatable(rem(h(digit,2)+1,modulo_mask)+1,:)*h(digit,1));
dividend(digit+1,1)=sum(newdiv_binary)>0;
% compute new dividend, exclude case of last iteration where
% dividend should end up with 0
if (digit~=state.dimension) && dividend(digit+1,1),
dividend(digit+1,2)=dividend(digit+1,1)*betatable_sort_inverse(sum(newdiv_binary.*binvec));
else
dividend(digit+1,2)=0;
end;
end;
% initialize the prms
% init parameters for deBruijn sequence
state.bruijn = para.bruijn;
state.bruijn_counter = -1;
if para.method == 1, %new way
% Initialize the PRBS registers
tmp_srgtaps = srgtaps(state.periodicity);
state.srgtaps = zeros(4,1,'int64');
mat_table = betatable(h(:,2)+1,:);
state.mat_table = int64(mat_table * 2.^(0:state.dimension-1)');
for n=1:length(tmp_srgtaps)
state.srgtaps(n)=int64(2^(tmp_srgtaps(n)-1));
end
tmp_reg = int64(2^49-1);
state.reg = zeros(state.dimension,1,'int64');
tmp_reg = prms_c(int32(1),tmp_reg,state.srgtaps);
state.reg(1)= tmp_reg;
offset = (2^state.periodicity-1)/modulo_mask;
for n=2:state.dimension
tmp_reg = prms_c(int32(offset),tmp_reg,state.srgtaps);
state.reg(n)= tmp_reg;
end
if para.skip>0
[prbs_register, bruijn, bruijn_counter,data_out ]= prms_c(int32(para.skip),...
state.reg,...
state.srgtaps,...
int32(state.order),...
state.mat_table,...
int32(state.bruijn),...
int64(state.bruijn_counter));
state.reg = prbs_register;
state.bruijn = bruijn;
state.bruijn_counter = bruijn_counter;
end
else
% Initialize the PRBS registers
state.reg = ones(1,state.periodicity);
state.srgtaps = srgtaps(state.periodicity);
state.mat_table = betatable(h(:,2)+1,:);
% determine indices into past PRBS
state.PRBS_inx=1+(0:state.dimension-1)*(2^state.periodicity-1)/modulo_mask;
% fill PRBS_memory by creating the first part of the PRBS
[state.reg,state.PRBS_memory]=prms_prbs_init_mex(state.periodicity,state.srgtaps,state.PRBS_inx(end));
% init for deBruijn sequence
% skip first bits
skipped=0;
PRBS_memory_pointer=0;
while skipped<para.skip,
% consider deBruijn symbol
if state.bruijn==2 && skipped>1,
skipped = skipped+1;
state.bruijn = 1;
continue;
end;
% compute current PRMS-symbol
PRBS_inx=mod(state.PRBS_inx+PRBS_memory_pointer-1,state.PRBS_inx(end))+1;
symbol_temp=rem(sum(repmat(double(state.PRBS_memory(PRBS_inx).'),1,state.dimension).*...
state.mat_table,1),2).';
% compute new PRBS_memory
state.reg=[rem(sum(state.reg(state.srgtaps)),2) state.reg(1:end-1)];
state.PRBS_memory(PRBS_memory_pointer+1)=state.reg(end);
PRBS_memory_pointer=mod(PRBS_memory_pointer+1,state.PRBS_inx(end));
skipped=skipped+1;
% consider deBruijn option
if state.bruijn==1,
if state.bruijn_counter<0,
% counter not yet locked
if any([~sum(symbol_temp) state.order==1]),
% zero symbol detected
state.bruijn_counter=state.bruijn_counter-1;
if any([state.bruijn_counter== -state.order state.order==1]),
% lock counter
state.bruijn_counter=2^state.periodicity-1;
state.bruijn=2;
end;
else
% no zero, so reset counter
state.bruijn_counter=-1;
end;
else
% counter already locked
state.bruijn_counter=state.bruijn_counter-1;
if state.bruijn_counter==0,
% reset counter
state.bruijn_counter = 2^state.periodicity-1;
state.bruijn=2;
end;
end;
end;
end;
end;
end
data_out=zeros(state.dimension,state.bl);
else
% Execution Stage
% update status window
% status(getfield(inputname(2),{1:length(inputname(2))-6}));
bl = state.bl;
if ~para.rand % PRMS
if para.method == 1 % fast implementation
[prbs_register, bruijn, bruijn_counter,data_out ]= prms_c(int32(state.bl),...
state.reg,...
state.srgtaps,...
int32(state.order),...
state.mat_table,...
int32(state.bruijn),...
int64(state.bruijn_counter));
if ~state.reset_prms
state.reg = prbs_register;
state.bruijn = bruijn;
state.bruijn_counter = bruijn_counter;
end
data_out = double(data_out);
else %old way slow implementation
if ((loop==1) || ~state.reset_prms)
% Initialisierung des Ausgangsvektors
data_out = zeros(state.dimension,bl);
% Successives Berechnen der Ausgangsfolge
counter=0;
PRBS_memory_pointer=0;
while counter<bl,
% check for zero symbol to be inserted for de Bruijn
% sequence
if state.bruijn==2,
counter = counter+1;
state.bruijn = 1;
continue;
end;
% compute current PRMS-symbol
PRBS_inx=mod(state.PRBS_inx+PRBS_memory_pointer-1,state.PRBS_inx(end))+1;
data_out(:,counter+1)=rem(sum(repmat(double(state.PRBS_memory(PRBS_inx).'),1,state.dimension).*...
state.mat_table,1),2).';
% compute new PRBS_memory
state.reg=[rem(sum(state.reg(state.srgtaps)),2) state.reg(1:end-1)];
state.PRBS_memory(PRBS_memory_pointer+1)=state.reg(end);
counter=counter+1;
PRBS_memory_pointer=mod(PRBS_memory_pointer+1,state.PRBS_inx(end));
% consider deBruijn option
if state.bruijn==1,
if state.bruijn_counter<0,
% counter not yet locked
if any([~sum(data_out(:,counter)) state.order==1]),
% zero symbol detected
state.bruijn_counter=state.bruijn_counter-1;
if any([state.bruijn_counter== -state.order state.order==1]),
% lock counter
state.bruijn_counter=2^state.periodicity-1;
state.bruijn=2;
end;
else
% no zero, so reset counter
state.bruijn_counter=-1;
end;
else
% counter already locked
state.bruijn_counter=state.bruijn_counter-1;
if state.bruijn_counter==0,
% reset counter
state.bruijn_counter = 2^state.periodicity-1;
state.bruijn=2;
end;
end;
end;
end;
% resort PRBS_memory for next block
state.PRBS_memory=[state.PRBS_memory(PRBS_memory_pointer+1:end) state.PRBS_memory(1:PRBS_memory_pointer)];
if state.reset_prms
state.data_mem = data_out;
end
else
data_out=state.data_mem;
end
end
else
data_out = round(rand(para.dimension,bl)) ;
end
end
end
end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% A D D I T I O N A L F U N C T I O N S %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% generate feedback taps for shift register %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function taps=srgtaps(inx)
data={[1] ...
[2 1] ...
[3 1] ...
[4 1] ...
[5 2] ...
[6 1] ...
[7 1] ...
[8 7 2 1] ...
[9 4] ...
[10 3] ...
[11 2] ...
[12 10 2 1] ...
[13 8 5 3] ...
[14 12 11 1] ...
[15 1] ...
[16 15 12 10] ...
[17 3] ...
[18 7] ...
[19 10 9 3] ...
[20 3] ...
[21 2] ...
[22 1] ...
[23 5] ...
[24 11 5 2] ...
[25 3] ...
[26 23 15 13] ...
[27 23 22 17] ...
[28 3] ...
[29 2] ...
[30 27 10 9] ...
[31 3] ...
[32 16 7 2] ...
[33 13] ...
[34 17 12 8] ...
[35 2] ...
[36 11] ...
[37 22 14 2] ...
[38 27 6 5] ...
[39 4] ...
[40 29 27 23] ...
[41 3] ...
[42 34 31 30] ...
[43 27 22 5] ...
[44 39 35 18] ...
[45 39 28 4] ...
[46 40 31 18] ...
[47 5] ...
[48 19 9 1]};
taps=data{inx};
end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% adapted and faster version of generator for cyclic code %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function code=cyclgen_local(columns,polynomial)
rows=log2(columns+1);
code=zeros(rows,columns);
code(1,1)=1;
% create columns
for s=2:columns,
code(:,s)=[0;code(1:end-1,s-1)];
% check for overflow
if code(end,s-1),
code(:,s)=rem(code(:,s)+polynomial(1:end-1).',2);
end;
end;
end

938
RUN_TESTS.m Normal file
View File

@@ -0,0 +1,938 @@
function app = RUN_TESTS(options)
%TEST_DASHBOARD Minimal GUI for test discovery and execution.
% test_dashboard() opens a small dashboard that:
% - discovers implemented, placeholder, and missing class-backed tests
% - lists standalone tests such as integration or helper tests
% - runs the selected test file
% - runs the existing fast/full test profiles
%
% app = test_dashboard("Visible", false) builds the dashboard without
% showing it. This is useful for non-interactive validation.
arguments
options.Visible (1, 1) logical = true
end
app = struct();
app.Inventory = table();
app.SelectedRow = [];
app.ActiveProfileRowIndices = [];
app.LastRunStatusMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunSummaryMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunDateMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunCommitMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
testsRoot = fileparts(mfilename("fullpath"));
repoRoot = fileparts(testsRoot);
reportsRoot = fullfile(testsRoot, 'reports');
latestRunFile = fullfile(testsRoot, '.last_test_run.json');
createUi();
refreshInventory();
if options.Visible
app.Figure.Visible = "on";
end
function createUi()
app.Figure = uifigure( ...
"Name", "IMDD Test Dashboard", ...
"Position", [80, 80, 1360, 820], ...
"Color", [0.965, 0.973, 0.988], ...
"Visible", "off");
app.Layout = uigridlayout(app.Figure, [3, 1]);
app.Layout.RowHeight = {92, "1x", 150};
app.Layout.Padding = [14, 14, 14, 14];
app.Layout.RowSpacing = 12;
app.HeaderPanel = uipanel(app.Layout, ...
"Title", "Suite Overview", ...
"BackgroundColor", [0.985, 0.989, 0.997]);
app.HeaderPanel.Layout.Row = 1;
headerGrid = uigridlayout(app.HeaderPanel, [2, 10]);
headerGrid.RowHeight = {28, 32};
headerGrid.ColumnWidth = {110, 110, 110, 90, 90, 90, "1x", 120, 120, 120};
headerGrid.Padding = [10, 8, 10, 8];
app.ImplementedLabel = makeMetricLabel(headerGrid, "Implemented", 1);
app.PlaceholderLabel = makeMetricLabel(headerGrid, "Placeholder", 2);
app.MissingLabel = makeMetricLabel(headerGrid, "Missing", 3);
app.PassedLabel = makeMetricLabel(headerGrid, "Passed", 4);
app.FailedLabel = makeMetricLabel(headerGrid, "Failed", 5);
app.IncompleteLabel = makeMetricLabel(headerGrid, "Incomplete", 6);
app.TotalLabel = makeMetricLabel(headerGrid, "Total", 7);
app.FilterDropDown = uidropdown(headerGrid, ...
"Items", {'All', 'Implemented', 'Missing', 'Standalone'}, ...
"Value", 'All', ...
"ValueChangedFcn", @filterChanged);
app.FilterDropDown.Layout.Row = 2;
app.FilterDropDown.Layout.Column = 8;
app.RefreshButton = uibutton(headerGrid, ...
"Text", "Refresh", ...
"ButtonPushedFcn", @refreshInventoryButtonPushed);
app.RefreshButton.Layout.Row = 2;
app.RefreshButton.Layout.Column = 9;
app.OpenButton = uibutton(headerGrid, ...
"Text", "Open Selected", ...
"ButtonPushedFcn", @openSelectedTest);
app.OpenButton.Layout.Row = 2;
app.OpenButton.Layout.Column = 10;
app.TablePanel = uipanel(app.Layout, ...
"Title", "Discovered Tests", ...
"BackgroundColor", [0.985, 0.989, 0.997]);
app.TablePanel.Layout.Row = 2;
tableGrid = uigridlayout(app.TablePanel, [2, 1]);
tableGrid.RowHeight = {36, "1x"};
tableGrid.Padding = [10, 8, 10, 10];
buttonGrid = uigridlayout(tableGrid, [1, 6]);
buttonGrid.ColumnWidth = {150, 120, 120, 140, 170, "1x"};
buttonGrid.Padding = [0, 0, 0, 0];
buttonGrid.ColumnSpacing = 10;
app.RunSelectedButton = uibutton(buttonGrid, ...
"Text", "Run Selected", ...
"ButtonPushedFcn", @runSelectedTest);
app.RunFastButton = uibutton(buttonGrid, ...
"Text", "Run Fast", ...
"ButtonPushedFcn", @(~, ~) runProfile("fast"));
app.RunFullButton = uibutton(buttonGrid, ...
"Text", "Run Full", ...
"ButtonPushedFcn", @(~, ~) runProfile("full"));
app.RunIntegrationButton = uibutton(buttonGrid, ...
"Text", "Run Integration", ...
"ButtonPushedFcn", @(~, ~) runProfile("integration"));
app.GenerateButton = uibutton(buttonGrid, ...
"Text", "Generate Missing Tests", ...
"ButtonPushedFcn", @generateMissingTests);
app.TableHintLabel = uilabel(buttonGrid, ...
"Text", "Select a row to run or open the corresponding test file.", ...
"HorizontalAlignment", "right", ...
"FontColor", [0.28, 0.32, 0.38]);
app.TableHintLabel.Layout.Column = 6;
app.Table = uitable(tableGrid, ...
"ColumnName", {"Status", "Last Run", "Result", "Date", "Commit", "Scope", "Target", "Test File", "Tags"}, ...
"ColumnEditable", false(1, 9), ...
"ColumnSortable", true(1, 9), ...
"ColumnWidth", {95, 95, 90, 150, 90, 100, 240, 300, "auto"}, ...
"SelectionType", "row", ...
"Multiselect", "off", ...
"CellSelectionCallback", @tableSelectionChanged, ...
"FontName", "Consolas");
app.Table.Layout.Row = 2;
app.LogPanel = uipanel(app.Layout, ...
"Title", "Run Log", ...
"BackgroundColor", [0.985, 0.989, 0.997]);
app.LogPanel.Layout.Row = 3;
logGrid = uigridlayout(app.LogPanel, [1, 1]);
logGrid.Padding = [10, 8, 10, 10];
app.LogArea = uitextarea(logGrid, ...
"Editable", "off", ...
"FontName", "Consolas", ...
"Value", {'IMDD Test Dashboard ready.'});
end
function label = makeMetricLabel(parent, titleText, columnIdx)
label = uilabel(parent, ...
"Text", sprintf("%s: --", titleText), ...
"FontWeight", "bold", ...
"FontSize", 13, ...
"FontColor", [0.12, 0.18, 0.26]);
label.Layout.Row = 1;
label.Layout.Column = columnIdx;
end
function refreshInventoryButtonPushed(~, ~)
refreshInventory();
end
function refreshInventory()
app.Inventory = buildInventory(repoRoot, testsRoot);
app.SelectedRow = [];
loadPersistedRunState();
updateSummary(app.Inventory);
applyFilter();
appendLog(sprintf("[%s] Refreshed test inventory.", stamp()));
end
function filterChanged(~, ~)
applyFilter();
end
function applyFilter()
filtered = app.Inventory;
switch string(app.FilterDropDown.Value)
case "Implemented"
filtered = filtered(filtered.Status == "Implemented", :);
case "Missing"
filtered = filtered(filtered.Status == "Missing", :);
case "Standalone"
filtered = filtered(filtered.Scope == "Standalone", :);
otherwise
% keep all rows
end
app.Table.Data = filtered(:, ["Status", "LastRun", "ResultSummary", "LastRunDate", "LastRunCommit", "Scope", "Target", "TestFile", "Tags"]);
applyRowStyles(filtered);
end
function updateSummary(inventory)
implementedCount = nnz(inventory.Status == "Implemented");
missingCount = nnz(inventory.Status == "Missing");
passedCount = nnz(inventory.LastRun == "Passed");
failedCount = nnz(inventory.LastRun == "Failed");
incompleteCount = nnz(inventory.LastRun == "Incomplete");
totalCount = height(inventory);
app.ImplementedLabel.Text = sprintf("Implemented: %d", implementedCount);
app.PlaceholderLabel.Text = "Placeholder: hidden";
app.MissingLabel.Text = sprintf("Missing: %d", missingCount);
app.PassedLabel.Text = sprintf("Passed: %d", passedCount);
app.FailedLabel.Text = sprintf("Failed: %d", failedCount);
app.IncompleteLabel.Text = sprintf("Incomplete: %d", incompleteCount);
app.TotalLabel.Text = sprintf("Total: %d", totalCount);
app.PassedLabel.FontColor = metricColor(passedCount > 0, [0.18, 0.56, 0.24]);
app.FailedLabel.FontColor = metricColor(failedCount > 0, [0.78, 0.22, 0.22]);
app.IncompleteLabel.FontColor = metricColor(incompleteCount > 0, [0.72, 0.48, 0.08]);
end
function applyRowStyles(filtered)
removeStyle(app.Table);
greenStyle = uistyle("BackgroundColor", [0.90, 0.97, 0.92]);
amberStyle = uistyle("BackgroundColor", [0.995, 0.95, 0.85]);
redStyle = uistyle("BackgroundColor", [0.985, 0.90, 0.90]);
blueStyle = uistyle("BackgroundColor", [0.90, 0.94, 0.995]);
runningStyle = uistyle("BackgroundColor", [0.89, 0.95, 1.00]);
for rowIdx = 1:height(filtered)
if filtered.LastRun(rowIdx) == "Passed"
addStyle(app.Table, greenStyle, "row", rowIdx);
elseif filtered.LastRun(rowIdx) == "Failed"
addStyle(app.Table, redStyle, "row", rowIdx);
elseif filtered.LastRun(rowIdx) == "Incomplete"
addStyle(app.Table, amberStyle, "row", rowIdx);
elseif filtered.LastRun(rowIdx) == "Running"
addStyle(app.Table, runningStyle, "row", rowIdx);
elseif filtered.Scope(rowIdx) == "Standalone"
addStyle(app.Table, blueStyle, "row", rowIdx);
elseif filtered.Status(rowIdx) == "Implemented"
addStyle(app.Table, greenStyle, "row", rowIdx);
elseif filtered.Status(rowIdx) == "Placeholder"
addStyle(app.Table, amberStyle, "row", rowIdx);
else
addStyle(app.Table, redStyle, "row", rowIdx);
end
end
end
function tableSelectionChanged(src, event)
if isempty(event.Indices)
app.SelectedRow = [];
return
end
selectedRow = event.Indices(1);
visibleRows = src.Data;
selectedTestFile = string(visibleRows.TestFile(selectedRow));
matchIdx = find(app.Inventory.TestFile == selectedTestFile, 1);
if isempty(matchIdx)
selectedTarget = string(visibleRows.Target(selectedRow));
matchIdx = find(app.Inventory.Target == selectedTarget, 1);
end
app.SelectedRow = matchIdx;
end
function openSelectedTest(~, ~)
record = selectedRecord();
if isempty(record)
uialert(app.Figure, "Select a test row first.", "No Selection");
return
end
if strlength(record.TestFile) == 0 || ~isfile(record.TestFile)
uialert(app.Figure, "The selected class does not have a test file yet.", "No Test File");
return
end
edit(record.TestFile);
appendLog(sprintf("[%s] Opened %s", stamp(), record.TestFile));
end
function runSelectedTest(~, ~)
record = selectedRecord();
if isempty(record)
uialert(app.Figure, "Select a test row first.", "No Selection");
return
end
if strlength(record.TestFile) == 0 || ~isfile(record.TestFile)
uialert(app.Figure, "The selected class does not have a runnable test file yet.", "Missing Test");
return
end
titleText = sprintf("Running %s", record.TestFileName);
markRowAsRunning(record);
runWithProgress(titleText, @() runSingleTestFile(record.TestFile), record);
end
function runProfile(profile)
titleText = sprintf("Running profile '%s'", profile);
rowIndices = profileRowIndices(profile);
app.ActiveProfileRowIndices = rowIndices;
markRowsAsRunning(rowIndices);
runWithProgress(titleText, @() runProfileInternal(profile, rowIndices), table());
app.ActiveProfileRowIndices = [];
end
function generateMissingTests(~, ~)
setupRepositoryPath();
try
summary = generateTests("verbose", false);
refreshInventory();
appendLog(sprintf("[%s] Generated missing tests: %d new, %d existing skipped, %d classes tracked.", ...
stamp(), summary.generated, summary.skippedExisting, summary.classCount));
catch err
appendLog(sprintf("[%s] generateTests failed.", stamp()));
appendLog(getReport(err, "extended", "hyperlinks", "off"));
uialert(app.Figure, err.message, "Generate Tests Failed");
end
end
function runWithProgress(titleText, runner, record)
try
[output, results] = runner();
appendLog(sprintf("[%s] %s", stamp(), titleText));
appendLog(output);
updateRunStateFromResults(results, record);
writeRunArtifacts(titleText, results, record);
catch err
appendLog(sprintf("[%s] %s failed.", stamp(), titleText));
appendLog(getReport(err, "extended", "hyperlinks", "off"));
updateFailureState(record);
uialert(app.Figure, err.message, "Run Failed");
end
end
function [output, results] = runProfileInternal(profile, rowIndices)
setupRepositoryPath();
profile = string(profile);
results = matlab.unittest.TestResult.empty(0, 1);
logLines = strings(0, 1);
logLines(end + 1, 1) = sprintf("Running profile '%s' with %d test files.", profile, numel(rowIndices));
for idx = 1:numel(rowIndices)
record = app.Inventory(rowIndices(idx), :);
if strlength(record.TestFile) == 0 || ~isfile(record.TestFile)
continue
end
logLines(end + 1, 1) = sprintf("Running %s", record.TestFileName);
drawnow;
currentResults = runtests(char(record.TestFile));
currentResults = currentResults(:);
if isempty(results)
results = currentResults;
else
results = [results; currentResults]; %#ok<AGROW>
end
updateRunStateFromResults(currentResults, record);
[statusText, summaryText] = summarizeResults(currentResults);
logLines(end + 1, 1) = sprintf("Finished %s: %s (%s)", ...
record.TestFileName, statusText, summaryText);
drawnow;
end
logLines(end + 1, 1) = sprintf("Passed: %d | Failed: %d | Incomplete: %d", ...
nnz([results.Passed]), nnz([results.Failed]), nnz([results.Incomplete]));
output = strjoin(logLines, newline);
end
function [output, results] = runSingleTestFile(testFile)
setupRepositoryPath();
results = runtests(char(testFile));
output = evalc('disp(results);');
end
function record = selectedRecord()
record = table();
if isempty(app.SelectedRow)
return
end
if app.SelectedRow < 1 || app.SelectedRow > height(app.Inventory)
return
end
record = app.Inventory(app.SelectedRow, :);
end
function setupRepositoryPath()
addpath(repoRoot);
addpath(testsRoot);
folders = ["Classes", "Functions", "Datatypes", "Libs"];
for folder = folders
fullFolder = fullfile(repoRoot, folder);
if isfolder(fullFolder)
addpath(genpath(fullFolder));
end
end
end
function appendLog(textIn)
newLines = string(splitlines(string(textIn)));
current = string(app.LogArea.Value);
current = current(current ~= "");
newLines = newLines(newLines ~= "");
combined = [current; newLines];
if isempty(combined)
combined = " ";
end
app.LogArea.Value = cellstr(combined);
drawnow;
end
function markRowAsRunning(record)
if isempty(record) || strlength(record.TestFile) == 0
return
end
updateMappedState(char(record.TestFile), 'Running', '...');
applyStoredRunState();
pause(0.05);
end
function markRowsAsRunning(rowIndices)
for idx = 1:numel(rowIndices)
key = char(app.Inventory.TestFile(rowIndices(idx)));
if ~isempty(key)
updateMappedState(key, 'Running', '...');
end
end
applyStoredRunState();
pause(0.05);
end
function updateRunStateFromResults(results, record)
if ~isempty(record)
[statusText, summaryText] = summarizeResults(results);
updateMappedState(char(record.TestFile), statusText, summaryText);
else
resultNames = string({results.Name})';
uniqueNames = unique(resultNames);
for idx = 1:numel(uniqueNames)
currentResults = results(resultNames == uniqueNames(idx));
[statusText, summaryText] = summarizeResults(currentResults);
targetRows = find(app.Inventory.TestFileName == uniqueNames(idx));
for rowIdx = targetRows'
updateMappedState(char(app.Inventory.TestFile(rowIdx)), statusText, summaryText);
end
end
end
applyStoredRunState();
end
function updateFailureState(record)
if ~isempty(record) && strlength(record.TestFile) > 0
updateMappedState(char(record.TestFile), 'Failed', 'error');
applyStoredRunState();
end
end
function updateMappedState(key, statusText, summaryText)
if isempty(key)
return
end
app.LastRunStatusMap(key) = char(statusText);
app.LastRunSummaryMap(key) = char(summaryText);
end
function applyStoredRunState()
for idx = 1:height(app.Inventory)
key = char(app.Inventory.TestFile(idx));
if isempty(key)
continue
end
if isKey(app.LastRunStatusMap, key)
app.Inventory.LastRun(idx) = string(app.LastRunStatusMap(key));
app.Inventory.ResultSummary(idx) = string(app.LastRunSummaryMap(key));
if isKey(app.LastRunDateMap, key)
app.Inventory.LastRunDate(idx) = string(app.LastRunDateMap(key));
end
if isKey(app.LastRunCommitMap, key)
app.Inventory.LastRunCommit(idx) = string(app.LastRunCommitMap(key));
end
end
end
updateSummary(app.Inventory);
applyFilter();
end
function loadPersistedRunState()
app.LastRunStatusMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunSummaryMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunDateMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunCommitMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
if ~isfile(latestRunFile)
return
end
try
payload = jsondecode(fileread(latestRunFile));
fallbackDate = stringOrEmpty(payload, 'timestamp');
fallbackCommit = "";
if isfield(payload, 'git')
fallbackCommit = stringOrEmpty(payload.git, 'short_commit');
end
if isfield(payload, 'row_states')
rowStates = payload.row_states;
for idx = 1:numel(rowStates)
key = stringOrEmpty(rowStates(idx), 'test_file');
if strlength(key) == 0
continue
end
app.LastRunStatusMap(char(key)) = char(stringOrEmpty(rowStates(idx), 'last_run'));
app.LastRunSummaryMap(char(key)) = char(stringOrEmpty(rowStates(idx), 'result_summary'));
rowDate = stringOrEmpty(rowStates(idx), 'last_run_date');
rowCommit = stringOrEmpty(rowStates(idx), 'last_run_commit');
if strlength(rowDate) == 0
rowDate = fallbackDate;
end
if strlength(rowCommit) == 0
rowCommit = fallbackCommit;
end
app.LastRunDateMap(char(key)) = char(rowDate);
app.LastRunCommitMap(char(key)) = char(rowCommit);
end
end
applyStoredRunState();
catch
% Ignore malformed local cache files and rebuild from scratch.
end
end
function writeRunArtifacts(runLabel, results, record)
if ~isfolder(reportsRoot)
mkdir(reportsRoot);
end
gitInfo = collectGitMetadata(repoRoot);
runTimestamp = char(datetime("now", "TimeZone", "local", "Format", "yyyy-MM-dd'T'HH:mm:ssXXX"));
updateMetadataMaps(results, record, runTimestamp, gitInfo.short_commit);
report = struct();
report.timestamp = runTimestamp;
rowStates = inventoryRowStates(app.Inventory);
report.run_label = char(runLabel);
report.repo_root = repoRoot;
report.git = gitInfo;
report.totals = struct( ...
'passed', nnz([results.Passed]), ...
'failed', nnz([results.Failed]), ...
'incomplete', nnz([results.Incomplete]));
report.method_results = resultEntries(results);
report.row_states = rowStates;
jsonText = jsonencode(report, PrettyPrint=true);
writeTextFile(latestRunFile, jsonText);
reportFile = fullfile(reportsRoot, sprintf('%s_%s.json', ...
datestr(now, 'yyyy-mm-dd_HHMMSS'), sanitizeFileLabel(runLabel)));
writeTextFile(reportFile, jsonText);
appendLog(sprintf("[%s] Saved run report: %s", stamp(), reportFile));
end
function updateMetadataMaps(results, record, runDate, shortCommit)
if ~isempty(record) && strlength(record.TestFile) > 0
key = char(record.TestFile);
app.LastRunDateMap(key) = char(string(runDate));
app.LastRunCommitMap(key) = char(string(shortCommit));
applyStoredRunState();
return
end
if ~isempty(app.ActiveProfileRowIndices)
for rowIdx = app.ActiveProfileRowIndices(:)'
key = char(app.Inventory.TestFile(rowIdx));
if isempty(key)
continue
end
app.LastRunDateMap(key) = char(string(runDate));
app.LastRunCommitMap(key) = char(string(shortCommit));
end
end
applyStoredRunState();
end
function rowIndices = profileRowIndices(profile)
rowIndices = [];
normalizedTags = "," + erase(app.Inventory.Tags, " ") + ",";
switch string(profile)
case "fast"
rowIndices = find(app.Inventory.Status ~= "Missing" & ...
contains(normalizedTags, ",fast,") & ...
~contains(normalizedTags, ",placeholder,") & ...
~contains(normalizedTags, ",hardware,") & ...
~contains(normalizedTags, ",slow,"));
case "full"
rowIndices = find(app.Inventory.Status ~= "Missing" & ...
~contains(normalizedTags, ",placeholder,") & ...
~contains(normalizedTags, ",hardware,"));
case "integration"
rowIndices = find(app.Inventory.Status ~= "Missing" & ...
contains(normalizedTags, ",integration,") & ...
~contains(normalizedTags, ",placeholder,") & ...
~contains(normalizedTags, ",hardware,"));
case "placeholder"
rowIndices = find(contains(normalizedTags, ",placeholder,"));
end
end
function textOut = stamp()
textOut = char(datetime("now", "Format", "yyyy-MM-dd HH:mm:ss"));
end
end
function color = metricColor(hasRuns, activeColor)
if hasRuns
color = activeColor;
else
color = [0.48, 0.52, 0.58];
end
end
function inventory = buildInventory(repoRoot, testsRoot)
repoRoot = char(string(repoRoot));
testsRoot = char(string(testsRoot));
classesRoot = fullfile(repoRoot, 'Classes');
classFiles = collectMFiles(classesRoot);
records = struct( ...
"Status", {}, ...
"LastRun", {}, ...
"ResultSummary", {}, ...
"LastRunDate", {}, ...
"LastRunCommit", {}, ...
"Scope", {}, ...
"Target", {}, ...
"TargetFile", {}, ...
"TestFile", {}, ...
"TestFileName", {}, ...
"Tags", {});
matchedTestFiles = strings(0, 1);
for idx = 1:numel(classFiles)
classFile = fullfile(classFiles(idx).folder, classFiles(idx).name);
if ~isClassFile(classFile)
continue
end
relativePath = strrep(classFile, [classesRoot, filesep], "");
[relativeDir, className] = fileparts(relativePath);
relativeDirText = char(join(string(relativeDir), ""));
classNameText = char(string(className));
testFile = findMatchingTestFile(testsRoot, relativeDirText, classNameText);
if strlength(testFile) == 0
status = "Missing";
tags = "";
testFileName = "";
else
source = fileread(testFile);
if contains(source, "assumeFail(")
continue
else
matchedTestFiles(end + 1, 1) = testFile; %#ok<AGROW>
status = "Implemented";
tags = strjoin(extractTestTags(source), ", ");
[~, testFileName] = fileparts(testFile);
end
end
targetLabel = fullfile('Classes', relativeDirText, [classNameText, '.m']);
targetLabel = strrep(targetLabel, "\", "/");
records(end + 1) = struct( ... %#ok<AGROW>
"Status", status, ...
"LastRun", "", ...
"ResultSummary", "", ...
"LastRunDate", "", ...
"LastRunCommit", "", ...
"Scope", "Class", ...
"Target", string(targetLabel), ...
"TargetFile", string(classFile), ...
"TestFile", string(testFile), ...
"TestFileName", string(testFileName), ...
"Tags", string(tags));
end
allTestFiles = dir(fullfile(testsRoot, "**", "*_test.m"));
for idx = 1:numel(allTestFiles)
testFile = fullfile(allTestFiles(idx).folder, allTestFiles(idx).name);
if any(strcmpi(testFile, matchedTestFiles))
continue
end
source = fileread(testFile);
if contains(source, "assumeFail(")
continue
else
status = "Implemented";
end
[~, testFileName] = fileparts(testFile);
relativeTest = strrep(testFile, [testsRoot, filesep], "");
relativeTest = strrep(relativeTest, "\", "/");
tags = strjoin(extractTestTags(source), ", ");
records(end + 1) = struct( ... %#ok<AGROW>
"Status", status, ...
"LastRun", "", ...
"ResultSummary", "", ...
"LastRunDate", "", ...
"LastRunCommit", "", ...
"Scope", "Standalone", ...
"Target", string(relativeTest), ...
"TargetFile", "", ...
"TestFile", string(testFile), ...
"TestFileName", string(testFileName), ...
"Tags", string(tags));
end
inventory = struct2table(records);
if isempty(inventory)
inventory = table(strings(0, 1), strings(0, 1), strings(0, 1), ...
strings(0, 1), strings(0, 1), strings(0, 1), strings(0, 1), ...
strings(0, 1), strings(0, 1), strings(0, 1), strings(0, 1), ...
"VariableNames", ["Status", "LastRun", "ResultSummary", "LastRunDate", ...
"LastRunCommit", "Scope", "Target", "TargetFile", "TestFile", ...
"TestFileName", "Tags"]);
return
end
inventory = inventory(inventory.Status ~= "Placeholder", :);
statusOrder = containers.Map( ...
{'Implemented', 'Missing', 'Placeholder'}, ...
{1, 2, 3});
scopeOrder = containers.Map({'Class', 'Standalone'}, {1, 2});
statusRank = zeros(height(inventory), 1);
scopeRank = zeros(height(inventory), 1);
for idx = 1:height(inventory)
statusRank(idx) = statusOrder(char(inventory.Status(idx)));
scopeRank(idx) = scopeOrder(char(inventory.Scope(idx)));
end
inventory.statusRank = statusRank;
inventory.scopeRank = scopeRank;
inventory = sortrows(inventory, {'scopeRank', 'statusRank', 'Target'});
inventory = removevars(inventory, {'statusRank', 'scopeRank'});
end
function testFile = findMatchingTestFile(testsRoot, relativeDir, className)
testsRoot = char(string(testsRoot));
relativeDir = char(join(string(relativeDir), ""));
className = char(string(className));
testDir = fullfile(testsRoot, relativeDir);
testFile = "";
if ~isfolder(testDir)
return
end
exactFile = fullfile(testDir, [className, '_test.m']);
if isfile(exactFile)
testFile = string(exactFile);
return
end
candidates = dir(fullfile(testDir, '*_test.m'));
if isempty(candidates)
return
end
candidateNames = string({candidates.name});
suffix = ['_', className, '_test.m'];
matchMask = endsWith(candidateNames, suffix, "IgnoreCase", true);
if any(matchMask)
firstMatch = candidates(find(matchMask, 1));
testFile = string(fullfile(firstMatch.folder, firstMatch.name));
end
end
function tags = extractTestTags(source)
tokens = regexp(source, "TestTags\s*=\s*\{([^}]*)\}", "tokens", "once");
if isempty(tokens)
tags = strings(0, 1);
return
end
tags = regexp(tokens{1}, "'([^']*)'", "tokens");
tags = string(cellfun(@(entry) entry{1}, tags, "UniformOutput", false));
end
function tf = isClassFile(filePath)
source = fileread(filePath);
tf = contains(source, "classdef");
end
function files = collectMFiles(rootFolder)
pathString = genpath(rootFolder);
folders = regexp(pathString, pathsep, "split");
folders = folders(~cellfun("isempty", folders));
files = dir(fullfile(rootFolder, "*.m"));
files = files([]); %#ok<NASGU>
files = dir(fullfile(rootFolder, "*.m"));
files = files([]);
for idx = 1:numel(folders)
currentFiles = dir(fullfile(folders{idx}, "*.m"));
if isempty(currentFiles)
continue
end
currentFiles = currentFiles(~[currentFiles.isdir]);
files = [files; currentFiles]; %#ok<AGROW>
end
end
function [statusText, summaryText] = summarizeResults(results)
passedCount = nnz([results.Passed]);
failedCount = nnz([results.Failed]);
incompleteCount = nnz([results.Incomplete]);
if failedCount > 0
statusText = "Failed";
elseif incompleteCount > 0
statusText = "Incomplete";
else
statusText = "Passed";
end
summaryText = sprintf('%d/%d/%d', passedCount, failedCount, incompleteCount);
end
function out = stringOrEmpty(structValue, fieldName)
if isfield(structValue, fieldName)
out = string(structValue.(fieldName));
else
out = "";
end
end
function rows = inventoryRowStates(inventory)
rows = repmat(struct( ...
'test_file', '', ...
'target', '', ...
'scope', '', ...
'last_run', '', ...
'result_summary', '', ...
'last_run_date', '', ...
'last_run_commit', ''), height(inventory), 1);
for idx = 1:height(inventory)
rows(idx).test_file = char(inventory.TestFile(idx));
rows(idx).target = char(inventory.Target(idx));
rows(idx).scope = char(inventory.Scope(idx));
rows(idx).last_run = char(inventory.LastRun(idx));
rows(idx).result_summary = char(inventory.ResultSummary(idx));
rows(idx).last_run_date = char(inventory.LastRunDate(idx));
rows(idx).last_run_commit = char(inventory.LastRunCommit(idx));
end
end
function entries = resultEntries(results)
entries = repmat(struct( ...
'name', '', ...
'passed', false, ...
'failed', false, ...
'incomplete', false, ...
'duration_seconds', 0), numel(results), 1);
for idx = 1:numel(results)
entries(idx).name = char(string(results(idx).Name));
entries(idx).passed = results(idx).Passed;
entries(idx).failed = results(idx).Failed;
entries(idx).incomplete = results(idx).Incomplete;
entries(idx).duration_seconds = durationToSeconds(results(idx).Duration);
end
end
function gitInfo = collectGitMetadata(repoRoot)
gitInfo = struct();
gitInfo.commit = strtrim(runCommand(sprintf('git -C "%s" rev-parse HEAD', repoRoot)));
gitInfo.short_commit = strtrim(runCommand(sprintf('git -C "%s" rev-parse --short HEAD', repoRoot)));
gitInfo.branch = strtrim(runCommand(sprintf('git -C "%s" rev-parse --abbrev-ref HEAD', repoRoot)));
statusOutput = runCommand(sprintf('git -C "%s" status --porcelain', repoRoot));
gitInfo.dirty = ~isempty(strtrim(statusOutput));
end
function output = runCommand(commandText)
[status, output] = system(commandText);
if status ~= 0
output = '';
end
end
function writeTextFile(filePath, content)
fid = fopen(filePath, 'w');
assert(fid ~= -1, 'Could not write file: %s', filePath);
cleaner = onCleanup(@() fclose(fid)); %#ok<NASGU>
fprintf(fid, '%s', content);
end
function label = sanitizeFileLabel(labelText)
label = regexprep(char(string(labelText)), '[^A-Za-z0-9]+', '_');
label = regexprep(label, '^_+|_+$', '');
if isempty(label)
label = 'run';
end
end
function value = durationToSeconds(durationValue)
try
value = seconds(durationValue);
if isa(value, 'duration')
value = str2double(char(value));
end
catch
try
value = posixtime(datetime(durationValue) - datetime(0, 0, 0));
catch
durationText = char(string(durationValue));
tokens = regexp(durationText, '(\d+):(\d+):([\d\.]+)', 'tokens', 'once');
if isempty(tokens)
value = NaN;
else
value = str2double(tokens{1}) * 3600 + ...
str2double(tokens{2}) * 60 + ...
str2double(tokens{3});
end
end
end
if ~isnumeric(value) || ~isscalar(value)
value = NaN;
end
end

View File

@@ -1,11 +1,79 @@
classdef Electricalsignal_test < matlab.unittest.TestCase
% Test class for Electricalsignal
classdef Electricalsignal_test < IMDDTestCase
methods (Test, TestTags = {'unit', 'fast', 'signals', 'electricalsignal'})
function constructorStoresSignalFsAndLogbook(testCase)
seed = Signal(1, "fs", 2);
seed = seed.logbookentry("electrical seed", seed);
methods (Test)
function testExample(testCase)
% Example test case for Electricalsignal
% Add your test code here
testCase.verifyTrue(true);
elec = Electricalsignal([1; -2; 3], "fs", 80e9, "logbook", seed.logbook);
testCase.verifyClass(elec, 'Electricalsignal');
testCase.verifyEqual(elec.signal, [1; -2; 3]);
testCase.verifyEqual(elec.fs, 80e9);
testCase.verifyEqual(elec.logbook, seed.logbook);
end
function inheritedArithmeticKeepsElectricalsignalTypeAndMetadata(testCase)
seed = Signal(0, "fs", 1);
seed = seed.logbookentry("lhs metadata", seed);
lhs = Electricalsignal([1; 2; 3], "fs", 64e9, "logbook", seed.logbook);
rhs = Electricalsignal([4; 5; 6], "fs", 32e9, "logbook", table());
sumSig = lhs + rhs;
testCase.verifyClass(sumSig, 'Electricalsignal');
testCase.verifyEqual(sumSig.signal, [5; 7; 9]);
testCase.verifyEqual(sumSig.fs, lhs.fs);
testCase.verifyEqual(sumSig.logbook, lhs.logbook);
end
function informationCastPreservesSignalFsAndLogbook(testCase)
seed = Signal(3, "fs", 4);
seed = seed.logbookentry("electrical cast", seed);
elec = Electricalsignal([2; 0; -2], "fs", 96e9, "logbook", seed.logbook);
info = elec.Informationsignal("fs", elec.fs, "logbook", elec.logbook);
testCase.verifyClass(info, 'Informationsignal');
testCase.verifyEqual(info.signal, elec.signal);
testCase.verifyEqual(info.fs, elec.fs);
testCase.verifyEqual(info.logbook, elec.logbook);
end
function power50MatchesInheritedElectricalPowerDefinition(testCase)
elec = Electricalsignal([1; -1; 1; -1], "fs", 1, "logbook", table());
expectedPowerDbm = 10 * log10(mean(abs(elec.signal).^2) / 50) + 30;
testCase.verifyEqual(elec.power50(), expectedPowerDbm, "AbsTol", 1e-12);
testCase.verifyEqual(elec.power50(), elec.power(), "AbsTol", 1e-12);
end
function csprUsesCarrierToTotalPowerRatioInDb(testCase)
elec = Electricalsignal([1; 3], "fs", 1, "logbook", table());
carrierPowerDbm = pow2db(abs(mean(elec.signal)).^2) + 30;
expectedCsprDb = carrierPowerDbm - elec.power();
testCase.verifyEqual(elec.cspr(), expectedCsprDb, "AbsTol", 1e-12);
end
function normalizeMilliwattSetsPowerToZeroDbmInto50Ohm(testCase)
elec = Electricalsignal([2; -2; 2; -2], "fs", 1, "logbook", table());
elec = elec.normalize("mode", normalization_mode.milliwatt);
testCase.verifyEqual(elec.power50(), 0, "AbsTol", 1e-12);
testCase.verifyClass(elec, 'Electricalsignal');
end
function setPowerSupportsMilliwattAndDbwTargets(testCase)
elec = Electricalsignal([1; -1; 1; -1], "fs", 1, "logbook", table());
elecMilliwatt = elec.setPower(2, power_notation.mW);
elecDbw = elec.setPower(-20, power_notation.dBW);
testCase.verifyEqual(elecMilliwatt.power50(), 10 * log10(2), "AbsTol", 1e-12);
testCase.verifyEqual(elecDbw.power50(), 10, "AbsTol", 1e-12);
end
end
end

Some files were not shown because too many files have changed in this diff Show More