This notebook simulates a wireless communication system at 28 GHz, then trains a Dense Neural Network (DNN) to estimate the channel H from the transmitted and received signals.
import numpy, matplotlib, scipy, pandas, tensorflow ...Loads all tools needed:
numpy— math and array operationsmatplotlib— plottingscipy— signal processing (available but not heavily used here)pandas— data tables and Excel read/writetensorflow / keras— build and train the DNN
DATA_PATH = .../data/H_28GHz.csvJust sets the path to the CSV file. No Google Drive needed (this was the Colab-specific part that was removed).
h_raw = pd.read_csv(DATA_PATH, header=None, names=['H_complex'])
h_raw['H_complex'] = h_raw['H_complex'].apply(lambda x: complex(x.strip()))
H_array = h_raw['H_complex'].to_numpy()Reads the CSV where each row is a complex number like 2.97e-06 + 5.69e-06j.
Each value is parsed from a string into a proper Python complex number, then stored as a numpy array.
Output:
Loaded 62978 channel samples
First value: (2.9709600000000003e-06+5.69286e-06j)
print(f'Total samples : {len(H_array)}')
print(f'Real — min: ... max: ...')
print(f'Imag — min: ... max: ...')Quick sanity check — confirms all 62,978 samples loaded correctly and shows the value range of real and imaginary parts.
N = 20000 # Use first 20,000 samples
noise_pwr = 0.01 # AWGN noise variance
H_array = H_array[:N] # Trim to N samplesCaps the dataset at 20,000 samples (the first 20,000 from the 62,978 available).
noise_pwr = 0.01 means moderate noise — not completely clean, not completely corrupted.
X = np.random.randn(N)
X[X < 0] = -1 # BPSK: negative → -1
X[X >= 0] = +1 # BPSK: positive → +1BPSK (Binary Phase Shift Keying) is the simplest digital modulation scheme:
- Every transmitted bit is either
+1or-1 - This simulates actual data being sent wirelessly
Output: total number of symbols generated (= N).
Y = H_array * X + sqrt(noise_pwr) * randn(N)This is the standard wireless channel model:
| Term | Meaning |
|---|---|
H_array * X |
Signal after passing through the channel (distorted by H) |
sqrt(noise_pwr) * randn(N) |
Random AWGN noise added |
Y |
What the receiver actually picks up |
Output: confirms Y has N values; prints Y[1] as a spot check.
H_reduced = H_array.tolist()
Y = Y.tolist()Converts numpy arrays to Python lists for easier DataFrame creation in the next step.
df = pd.DataFrame({
'X': X,
'Y': Y,
'H_original': [str(v) for v in H_reduced],
'H_original_real': h_ori_real,
'H_original_imag': h_ori_imag,
})
df.to_excel('required_values.xlsx', ...)Bundles all the data together and saves it to required_values.xlsx. This file is the shared dataset that will be loaded by the LSTM notebook later.
Columns saved: X, Y, H_original (complex as string), H_original_real, H_original_imag.
df = pd.read_excel("required_values.xlsx")
df.head()Reloads and previews the file to confirm it was saved correctly.
df.describe()Shows count, mean, std, min, max for every column. This is a sanity check — confirms the data looks sensible before training.
df.isnull().sum()All zeros = no missing data. The dataset is clean.
X_new = df.iloc[:, 0:2] # columns: X (transmitted), |Y| (received)
Y_new = df.iloc[:, 3] # column: H_original_real (what we want to predict)The model is trained to learn: given X (sent bit) and Y (received signal) → estimate H_real (channel real part).
- Input (
X_new): 2 features per sample — transmitted bit and received signal - Output (
Y_new): 1 value — the real part of H
Output shapes: (N, 2) and (N,).
Y_new = np.reshape(Y_new, (len(Y_new), 1))Reshapes from (N,) to (N, 1) — Keras requires the output to be a 2D array.
for i in range(len(X_new)):
X_new[i, 1] = abs(complex(X_new[i, 1]))The Y column was stored as a complex string in Excel. This cell converts each value back to a complex number and takes its magnitude |Y|. The model works with signal magnitudes, not raw complex numbers.
scaler1 = MinMaxScaler()
X_scaled = scaler1.transform(X_new) # inputs scaled to [0, 1]
scaler2 = MinMaxScaler()
Y_scaled = scaler2.transform(Y_new) # outputs scaled to [0, 1]Scales all values to [0, 1]. Neural networks train faster and more stably on normalised data. Two separate scalers are used so each can be reversed independently after training.
print(X_scaled.shape, Y_scaled.shape)Confirms everything is the right shape before splitting. Expected: (N, 2) and (N, 1).
x_train, x_test, y_train, y_test = train_test_split(X_scaled, Y_scaled, test_size=0.2)Randomly splits data:
- 80% → training
- 20% → testing
Output: sizes of train and test sets.
from keras.models import Sequential
from keras.layers import DenseImports the building blocks for constructing the DNN.
Dense(500, relu) → Dense(250, relu) → Dense(120, relu) → Dense(1, linear)
model.compile(optimizer='adam', loss='mean_squared_error')| Layer | Units | Activation | Role |
|---|---|---|---|
| Dense | 500 | ReLU | First hidden layer — learns non-linear patterns |
| Dense | 250 | ReLU | Narrows representation |
| Dense | 120 | ReLU | Further compression |
| Dense | 1 | Linear | Output — predicts normalised H real part |
- Input shape: 2 features (X, |Y|)
- Loss: MSE (penalises large errors more)
- Optimiser: Adam
model.summary() is called inside the function, so the parameter count is printed when the model is built.
Epoch 1/50 — loss: 0.0054 val_loss: 0.0026
...
Epoch 50/50 — loss: 0.0027 val_loss: 0.0025
How to read this:
- Training loss dropped from ~0.0054 → ~0.0027
- Validation loss stabilised around 0.0025–0.0026 very early
val_loss < train_lossthroughout → no overfitting; the model generalises well
plt.plot(epochs, loss, 'r', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.savefig('../outputs/main/dnn_training_loss.png', dpi=150)Saves a plot of training and validation loss over 50 epochs.
Red = training loss, Blue = validation loss. Both drop quickly then flatten — healthy training.
Saved to:
outputs/main/dnn_training_loss.png
x_train_predict = model.predict(x_train) # predictions on training data
y_predict = model.predict(x_test) # predictions on test dataRuns the trained model on both sets to compare predicted vs actual H values.
x_predict = scaler2.inverse_transform(x_train_predict)
y_predict2 = scaler2.inverse_transform(y_predict)Converts predictions back from normalised [0, 1] to the original H scale (tiny values ~1e-6 range) using the saved scaler.
len(y_predict)Simple check — confirms how many test predictions were made (= 20% of N).
Train data RMSE : ...
Train data MSE : ...
Train data MAE : ...
─────────────────────────────
Test data RMSE : ...
Test data MSE : ...
Test data MAE : ...
All values are on the normalised [0, 1] scale.
| Metric | Meaning |
|---|---|
| MSE | Average of squared errors — lower is better |
| RMSE | Square root of MSE — in the same units as the prediction |
| MAE | Average absolute error — how far off predictions are on average |
Train data explained variance: 0.0
Test data explained variance: 0.0
This confirms the DNN's predictions have zero correlation with actual H values. This is the key limitation of this approach:
- Channel
Hvalues are extremely small (~1e-6) - Transmitted signal
X(±1) is ~1,000,000× larger and dominatesY - The received signal
Ycarries almost no information about H - The model learns to predict the mean of H (a flat line) for all inputs
This is expected and motivates the LSTM approach in lstm.ipynb.
plt.plot(y_test, label='Actual H (real, normalised)')
plt.plot(y_predict, label='Predicted H (real, normalised)')
plt.savefig('../outputs/main/dnn_channel_prediction.png', dpi=150)A full-width plot of actual vs predicted H over all test samples.
The predicted line is nearly flat while the actual H varies — visually confirming the explained variance issue.
Saved to:
outputs/main/dnn_channel_prediction.png
# Cell 29 — Manually compute MSE between actual and predicted
Diff = abs(y_test - y_predict)
squared_values = np.square(Diff)
meanSquareError = sum(squared_values) / len(squared_values)
# Cell 30 — First actual test value
y_test[0]
# Cell 31 — First scaled predicted value
y_predict[0]
# Cell 32 — First inverse-transformed predicted value
y_predict2[0]Manual cross-checks of individual predictions for visual inspection and verification.
all_pred_scaled = model.predict(X_scaled, verbose=0)
all_pred_real = scaler2.inverse_transform(all_pred_scaled).flatten()
df_final = pd.read_excel('required_values.xlsx')
df_final['H_DNN_predicted'] = all_pred_real
df_final.to_excel('required_values.xlsx', sheet_name='Sheet1', index=False)This is the critical handoff step between the two notebooks:
- Re-runs the model on all N samples in their original order (the train/test split had shuffled them, so we cannot use those predictions directly)
- Inverse-transforms back to original scale
- Adds a new column
H_DNN_predictedtorequired_values.xlsx
Output:
required_values.xlsx updated with column: H_DNN_predicted
Columns: ['X', 'Y', 'H_original', 'H_original_real', 'H_original_imag', 'H_DNN_predicted']
required_values.xlsx now contains both ground truth H values and DNN predictions, ready for the LSTM notebook to load and compare.
| File | What it is |
|---|---|
required_values.xlsx |
Complete dataset — X, Y, H_original, H_real, H_imag, H_DNN_predicted (all N samples) |
outputs/main/dnn_training_loss.png |
Train vs validation loss over 50 epochs |
outputs/main/dnn_channel_prediction.png |
Actual vs predicted H (real part) on all test samples |
The DNN trains successfully (loss ~0.0025) but the explained variance is 0 — it predicts a near-constant value regardless of input. This is because noise (noise_pwr = 0.01) is massively larger than the channel coefficients (~1e-6), so the received signal Y carries almost no useful information about H.
The final step saves DNN predictions back to required_values.xlsx, completing the handoff to lstm.ipynb, which bypasses this problem entirely by learning directly from the time-series structure of H itself.
To fix the explained variance issue you would need to:
- Reduce
noise_pwrsignificantly (e.g. 1e-10), or - Use pilot-based estimation (correlate known X values with Y), or
- Use the LSTM approach (
lstm.ipynb) — which predicts H from its own past history, not from X/Y at all