# Beamforming in Nullspace

**URL:** <https://nullspaceusers.discourse.group/t/beamforming-in-nullspace/92>\
**Category:** Prep  (CAD and Meshing)\
**Created:** [April 29, 2026, 1:15am UTC](https://nullspaceusers.discourse.group/t/beamforming-in-nullspace/92 "2026-04-29T01:15:06Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![MiguelNeves](https://avatars.discourse-cdn.com/v4/letter/m/ecd19e/32.png) [@MiguelNeves](https://nullspaceusers.discourse.group/u/MiguelNeves)\
**Post date:** [April 29, 2026, 1:15am UTC](https://nullspaceusers.discourse.group/t/beamforming-in-nullspace/92/1 "2026-04-29T01:15:06Z")

</div>

Dear Nullspace Team,

I am currently working on an Integrated Sensing and Communication (ISAC) model using an 8x8 planar antenna array with polarization diversity employing wideband half-wavelength dipoles (tilted ±45º). Each antenna element consists of two crossed dipoles, where only one is active at a time. Below I attached a portion of my `.jou` file showing the creation of a single antenna element:

```auto
create surface rectangle width {w} height {L} zplane
#{a1 = Id("surface")}
split surface {a1} across location position {-w} 0 0 location position {w} 0 0
#{a1a = Id("surface")-1}
#{a1b = Id("surface")}

create surface rectangle width {L} height {w} zplane
#{a2 = Id("surface")}
split surface {a2} across location position 0 {-w} 0 location position 0 {w} 0
#{a2a = Id("surface")-1}
#{a2b = Id("surface")}

rotate surface {a1a} {a1b} {a2a} {a2b} angle 45 about Z
move surface {a1a} {a1b} {a2a} {a2b} x {0} y {0}

block 1 surface {a1a} {a1b}
block 2 surface {a2a} {a2b}
nsem assign surface {a1a} {a1b} {a2a} {a2b} material "PEC"
nsem voltage source "p1" pos surface {a1a} neg surface {a1b} impedance 50
nsem lumped load "l2" surface {a2a} {a2b} impedance 50 0

```

For beamforming, I am aggregating these dipoles into eight 4x2 subsections (8 elements and 16 total ports per subsection) aiming to generate directive beams covering a 120° sector. Each section is intended to produce a beam pointing to unique (θ,ϕ) coordinates. The image below showcases the subsections inside the antenna.

 ![Antenna sections.drawio](https://canada1.discourse-cdn.com/flex010/uploads/nullspaceusers/original/1X/d283f25f2708d5f524964041cc38c253c00da79f.jpeg)

In order to steer the main lobe of each section to the specific (θ,ϕ) coordinates, I am applying progressive phase shifts (δx​,δy​) across the elements. Is it possible to define the progressive phase excitation directly within Nullspace Prep or is the recommended approach to apply them only during post-processing (e.g., in Python using complex weights)?

Thank you in advance!

Kind regards,

Miguel Neves

---

<div class="post-metadata">

**Author:** ![daniel.faircloth](https://yyz2.discourse-cdn.com/flex010/user_avatar/nullspaceusers.discourse.group/daniel.faircloth/32/37_2.png) [@daniel.faircloth](https://nullspaceusers.discourse.group/u/daniel.faircloth)\
**Post date:** [April 29, 2026, 6:16am UTC](https://nullspaceusers.discourse.group/t/beamforming-in-nullspace/92/2 "2026-04-29T06:16:05Z")

</div>

Hi Miguel,

Thanks for your question. This is a great use case for Nullspace. The short answer is to do the progressive phase shifts in post-processing, not in Nullspace Prep. The nsem voltage source command in Prep only takes pos/neg surfaces and a (real) impedance. There is no phase or complex-amplitude argument, and each voltage source enforces a fixed 1 V excitation at its feed.

The recommended workflow is:

1. Define each active dipole as its own voltage source in the .jou file (you already are — p1 per element). For an 8×8 array with one active crossed-dipole per element, that gives N\_VS = 64 voltage sources. Keep the inactive arm as a nsem lumped load exactly as you’re doing. That captures the loaded passive arm correctly during the solve.

2. Run a single simulation. Nullspace EM solves all 64 (or 128 is you add in the inactive elements) right-hand-sides in one go, so you get the full S-parameter matrix and the per-port far fields with no extra cost.

3. Form the complex weight vector(s) in Python for each of your beam directions:

```python
import numpy as np
k = 2*np.pi*f/c
# Element positions in the section (x_m, y_n) and target (theta0, phi0)
delta_x = -k*dx*np.sin(theta0)*np.cos(phi0)
delta_y = -k*dy*np.sin(theta0)*np.sin(phi0)
w = np.exp(1j*(m*delta_x + n*delta_y)) # length N_VS = 64, complex

```

The weights array passed to the API is length N\_VS (or [N\_VS, N\_f] for frequency-dependent weighting) and is generally complex-valued.

You might also want to look at the functions maximize\_pattern\_at\_scan and maximize\_pattern\_at\_scan\_for\_polarization in the post-processing module.

4. Apply the weights through the PostProcess API. All the relevant methods accept a weights= argument and do the weighted-sum across sources internally. When weights are supplied, the far-field-derived arrays collapse the per-source axis to length 1, i.e. shape [N\_phi, N\_theta, 1, N\_f, 1 or 2].

5. Sweep the 8 beams by simply re-calling those methods with each w\_k for k = 1…8. No re-simulation needed. This is purely a weighted superposition of the per-port solutions you already computed.

The closest worked example is the Circularly Polarized Dual-Fed Patch Antenna (examples/cp\_patch\_antenna, also in the user guide Examples → Circularly Polarized Dual-Fed Patch Antenna). It builds an RHCP weight vector from two feeds and feeds it to get\_active\_s\_parameters, get\_directivity, and get\_gain exactly as above. Same pattern, just at a different scale than your 8×8.

A couple of practical notes for your geometry specifically:

- The element ordering in the weights array must match the order in which voltage sources were created in Prep — post.get\_voltage\_source\_names() is the authoritative list. It’s worth printing it once and asserting your (m, n) indexing against it.

- Per the .jou snippet you shared, each element has one voltage source (the active crossed dipole, e.g. p1) and one lumped load (the passive crossed dipole, e.g. l2). That gives N\_VS = 64 for the full 8×8 array. The “16 ports per subsection” you mention then refers to the total physical dipole arms (8 elements × 2 arms), not the number of voltage sources Nullspace EM sees — only 8 of those 16 arms per subsection are excited.

- If you want to study polarization diversity (i.e. choose at post-processing time which arm of each crossed pair is active), the cleaner setup is to assign a voltage source to both arms of every element instead of a VS on one and a lumped load on the other. That makes N\_VS = 128, and you build a per-beam weight vector that zeros the inactive arm and applies the progressive phase to the active one. Cost: one solve covers every {beam direction × polarization} combination you’ll ever post-process.

Hope that helps. Please reach out with any further questions.

---

<div class="post-metadata">

**Author:** ![MiguelNeves](https://avatars.discourse-cdn.com/v4/letter/m/ecd19e/32.png) [@MiguelNeves](https://nullspaceusers.discourse.group/u/MiguelNeves)\
**Post date:** [May 4, 2026, 12:02am UTC](https://nullspaceusers.discourse.group/t/beamforming-in-nullspace/92/3 "2026-05-04T00:02:18Z")

</div>

Thank you for the guidance!

I wanted to follow up just to say that I’ve successfully implemented the post-processing workflow you suggested. By applying the complex weight vectors in Python, I managed to simulate and verify all 8 beams, each correctly pointing in their unique (θ,ϕ) directions.

I will reach out again if I have any further questions as the work progresses.

Best regards.

---

<div class="post-metadata">

**Author:** ![daniel.faircloth](https://yyz2.discourse-cdn.com/flex010/user_avatar/nullspaceusers.discourse.group/daniel.faircloth/32/37_2.png) [@daniel.faircloth](https://nullspaceusers.discourse.group/u/daniel.faircloth)\
**Post date:** [May 4, 2026, 12:09pm UTC](https://nullspaceusers.discourse.group/t/beamforming-in-nullspace/92/4 "2026-05-04T12:09:23Z")

</div>

That’s great. Please reach out with any other questions. Happy simulating.
