classdef Signalgenerator %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 methods (Access=public) function obj = Signalgenerator(options) %NAME Construct an instance of this class % Detailed explanation goes here arguments options.form signalform = signalform.sine 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 % fn = fieldnames(options); for n = 1:numel(fn) try obj.(fn{n}) = options.(fn{n}); 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) % actual processing of the signal (steps 1. - 3.) signal = obj.build_signal(); signalclass_out = Informationsignal(signal,"fs",obj.fs); % append to logbook lbdesc = ['Signalgenerator: ',char(obj.form), '']; signalclass_out = signalclass_out.logbookentry(lbdesc); end function signal = build_signal(obj) %METHOD1 Summary of this method goes here % Detailed explanation goes here arguments(Input) obj end arguments(Output) signal double end switch obj.form case signalform.sine % Parameters T = 1/obj.fs; % Sampling period (seconds per sample) L = obj.length; % Length of signal (number of samples) t = (0:L-1)*T; % Time vector % Sine wave parameters f = obj.fsig; % Frequency of the sine wave (Hz) A = 1; % Amplitude of the sine wave % 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 end methods (Access=private) % 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