Frequency Selective Surface#

This example demonstrates building a PFS model using the Frequency Selective Surface (FSS) test case, which is included in the NeurEco installation package.

Problem Description#

This test case predicts the frequency response of a Frequency Selective Surface (FSS) based on 5 design parameters. FSS structures are used in electromagnetic applications such as antenna radomes, filters, and absorbers.

Inputs (6 features):

  • Frequency (first column)

  • Design parameter 1

  • Design parameter 2

  • Design parameter 3

  • Design parameter 4

  • Design parameter 5

Outputs (2 complex features):

  • Transverse Electric (TE) mode response

  • Transverse Magnetic (TM) mode response

Test Case Data#

The test case includes:

  • Training: 1500 samples

  • Validation: 420 samples

  • Test: 9080 samples

Files:

inputs_train.npy   targets_train.npy
inputs_valid.npy   targets_valid.npy
inputs_test.npy    targets_test.npy

Setup#

  1. Create a working directory (e.g., FSS)

  2. Extract the test case data into this directory

  3. Create a Python script (e.g., fss_example.py)

Complete Example#

"""
NeurEco PFS Example: Frequency Selective Surface (FSS)

This example uses the FSS test case included with the NeurEco installation.
"""

from NeurEco import NeurEcoFrequential as Frequential
import numpy as np
import matplotlib.pyplot as plt


# ===================
# Step 1: Load Data
# ===================

x_train = np.load('inputs_train.npy')
y_train = np.load('targets_train.npy')
x_valid = np.load('inputs_valid.npy')
y_valid = np.load('targets_valid.npy')
x_test = np.load('inputs_test.npy')
y_test = np.load('targets_test.npy')

print(f"Training samples: {x_train.shape[0]}")
print(f"Validation samples: {x_valid.shape[0]}")
print(f"Test samples: {x_test.shape[0]}")
print(f"Input features: {x_train.shape[1]} (frequency + 5 parameters)")
print(f"Output features: {y_train.shape[1]} (TE and TM modes)")
print(f"Output dtype: {y_train.dtype}")  # Should be complex


# ===================
# Step 2: Build Model
# ===================

builder = Frequential.PFS()

config = Frequential.PFSBuildConfig(
    # Training data
    input_data=x_train,
    output_data=y_train,
    # Validation data
    validation_input_data=x_valid,
    validation_output_data=y_valid,
    # Test data
    test_input_data=x_test,
    test_output_data=y_test,
    # Output paths
    write_model_to="./fssModel/fss.efnn",
    checkpoint_address="./fssModel/fss.checkpoint",
)

status = builder.build(config)

if status == 0:
    print("\nBuild completed successfully!")


# =============================
# Step 3: Explore Checkpoints
# =============================

model = Frequential.PFS()
n_models = model.get_number_of_networks_from_checkpoint("./fssModel/fss.checkpoint")

print(f"\nCheckpoint contains {n_models} models")
print("Evaluating each checkpoint to find accuracy/complexity trade-off:\n")

print(f"{'Model':<8} {'L2 Error':<15} {'Denominator Size':<20}")
print("-" * 45)

for i in range(n_models):
    model.load_model_from_checkpoint("./fssModel/fss.checkpoint", i)
    outputs = model.evaluate(x_test)
    error = model.compute_error(outputs, y_test)
    denom_size = model.get_denominator_size()
    print(f"{i:<8} {100*error:>10.4f}%     {denom_size:<20}")


# =============================
# Step 4: Evaluate Final Model
# =============================

evaluator = Frequential.PFS()
evaluator.load("./fssModel/fss.efnn")

print(f"\n=== Final Model ===")
print(f"Inputs: {evaluator.get_input_count()}")
print(f"Outputs: {evaluator.get_output_count()}")
print(f"Denominator size: {evaluator.get_denominator_size()}")

# Evaluate on test set
predictions = evaluator.evaluate(x_test)
error = evaluator.compute_error(predictions, y_test)
print(f"Test L2 relative error: {100 * error:.4f}%")


# =============================
# Step 5: Visualize Results
# =============================

# Select a subset of test samples with the same parameter values
# but varying frequency for visualization
unique_params = np.unique(x_test[:, 1:], axis=0)
param_set = unique_params[0]  # Pick first parameter combination

# Find all samples with these parameters
mask = np.all(x_test[:, 1:] == param_set, axis=1)
x_subset = x_test[mask]
y_subset = y_test[mask]
pred_subset = predictions[mask]

# Sort by frequency
sort_idx = np.argsort(x_subset[:, 0])
frequencies = x_subset[sort_idx, 0]
y_sorted = y_subset[sort_idx]
pred_sorted = pred_subset[sort_idx]

# Plot TE mode (output 0)
fig, axes = plt.subplots(2, 2, figsize=(12, 8))

# TE Magnitude
ax = axes[0, 0]
ax.plot(frequencies, np.abs(y_sorted[:, 0]), 'r-', label="Reference", linewidth=2)
ax.plot(frequencies, np.abs(pred_sorted[:, 0]), 'k--', label="Prediction", linewidth=1.5)
ax.set_xlabel("Frequency")
ax.set_ylabel("|TE|")
ax.set_title("TE Mode - Magnitude")
ax.legend()
ax.grid(True)

# TE Phase
ax = axes[0, 1]
ax.plot(frequencies, np.angle(y_sorted[:, 0], deg=True), 'r-', label="Reference", linewidth=2)
ax.plot(frequencies, np.angle(pred_sorted[:, 0], deg=True), 'k--', label="Prediction", linewidth=1.5)
ax.set_xlabel("Frequency")
ax.set_ylabel("Phase TE (degrees)")
ax.set_title("TE Mode - Phase")
ax.legend()
ax.grid(True)

# TM Magnitude
ax = axes[1, 0]
ax.plot(frequencies, np.abs(y_sorted[:, 1]), 'r-', label="Reference", linewidth=2)
ax.plot(frequencies, np.abs(pred_sorted[:, 1]), 'k--', label="Prediction", linewidth=1.5)
ax.set_xlabel("Frequency")
ax.set_ylabel("|TM|")
ax.set_title("TM Mode - Magnitude")
ax.legend()
ax.grid(True)

# TM Phase
ax = axes[1, 1]
ax.plot(frequencies, np.angle(y_sorted[:, 1], deg=True), 'r-', label="Reference", linewidth=2)
ax.plot(frequencies, np.angle(pred_sorted[:, 1], deg=True), 'k--', label="Prediction", linewidth=1.5)
ax.set_xlabel("Frequency")
ax.set_ylabel("Phase TM (degrees)")
ax.set_title("TM Mode - Phase")
ax.legend()
ax.grid(True)

plt.suptitle("FSS Frequency Response: Prediction vs Reference")
plt.tight_layout()
plt.show()


# =============================
# Cleanup
# =============================

builder.delete()
model.delete()
evaluator.delete()

Key Concepts Demonstrated#

Complex-Valued Outputs

PFS models naturally handle complex outputs (magnitude and phase):

predictions = model.evaluate(x_test)
print(f"dtype: {predictions.dtype}")  # complex128

# Extract magnitude and phase
magnitude = np.abs(predictions[:, 0])
phase_deg = np.angle(predictions[:, 0], deg=True)

Frequency as First Input (Required)

The first column of input data must always be the frequency — this is mandatory, not optional:

# Column 0: frequency (REQUIRED - must always be present)
# Columns 1-5: design parameters
print(f"Input shape: {x_train.shape}")  # (1500, 6)

# Even for single-frequency evaluation, frequency column must exist:
single_freq_input = np.array([[1.5e10, 0.5, 0.9, 0.8, 0.3, 0.6]])  # freq in Hz + 5 params in [0, 1]
output = model.evaluate(single_freq_input)

Checkpoint Exploration

Compare intermediate models to find the best trade-off between accuracy and complexity:

for i in range(n_models):
    model.load_model_from_checkpoint(checkpoint_path, i)
    error = model.compute_error(model.evaluate(x_test), y_test)
    denom_size = model.get_denominator_size()
    print(f"Model {i}: error={100*error:.2f}%, denom_size={denom_size}")

Denominator Size

The denominator size indicates model complexity. Larger denominators can capture more complex frequency behavior (more poles/zeros) but increase computation time:

denom_size = model.get_denominator_size()
print(f"Denominator size: {denom_size}")

Expected Results#

With the FSS test case, you should see:

  • Training completes with multiple checkpoints

  • Final model achieves approximately 1-2% L2 relative error

  • TE and TM predictions closely match reference data in both magnitude and phase

Next Steps#

  • Experiment with different reduced_space_dimension values

  • Try different max_enrichment_steps to control training duration

  • Export the model to FMU format for use in other simulation tools (requires license)