import optycal as opt

"""Example 4: Parabolic Reflector with Patch Feed

In this example we:
- Build a rotationally symmetric parabolic reflector from a parametric profile.
- Apply a physical-optics (PO) style surface current solution on the reflector.
- Illuminate the reflector with a patch feed in a rotated coordinate system.
- Compute 1D and 2D far-field patterns from the reflector surface.
- Visualize geometry and fields in 3D.

"""

# Center frequency [Hz]
f0 = 9e9

# Speed of light in vacuum [m/s]
c0 = 299792458

# Wavelength [m]
wl = c0 / f0

# Sampling / element spacing along the reflector profile
ds = wl * 0.3

# Paraboloid geometry parameters [m]
radius = 0.6          # Aperture radius
focal_distance = 0.4  # Focal length

# Coordinate system for the feed:
# Start from global CS, then rotate 180 deg around z-axis.
# This flips the feed orientation so it points toward the reflector.
CS = opt.GCS.displace().rotate_basis((0, 0, 1), 180)

"""Parabolic creation procedure (manual)

Optycal will contain simpler ways to make parabolas but now we will demonstrate how to make one manually.

The way to create parametric surfaces in Optycal is by defining a sweep line in space and then to translate
that through space with a sweep function. That traced out sheet will be meshed automatically.

"""
# First we define a parametric line. We define a line by providing functions for each coordinate so x(t), y(t), z(t). If none, provided all are 0
# So our line will go up from the origin in the Z-direction
# x(t) = 0
# y(t) = 0
# z(t) = radius*t
# By default the parametrization variable goes from 0 to 1.
parline = opt.ParametricLine(fz=lambda t: radius * t)

# If we now sweep this line by rotating it around the origin, we get a disc. We don't want that, thus we also have to project this disc onto the parabola.
# To do this we have to mix a function that projects coordinates onto other coordinate (Mapping) with the revolution.
# First we create a mapping that maps a flat plane onto a paraola regardless of the size. We can automate the generation of this mapping with the function
# parabolic reflector:
mapping = opt.Mapping.parabolic_reflector(
    (0, 0, 0), # Focal point: origin
    focal_distance, # Focal distance
    (1, 0, 0) # Point direction: X-axis
)

# This mapping again maps all points on the plane normal to direction onto the parabola. Then we will apply this mapping to a revolution around the X-axis
sweep_function = mapping.map(opt.SweepFunction.revolve((1, 0, 0)))

# This new sweep function combines the projection onto the parabola with the rotation around the X-axis.
# Finally we will now apply this sweep_function to our parametric line to mesh our surface.
# The arguments are (the parametric line, the discretization of the mesh, the coordinate system to put the geometry in and an alignment function)

# This opt.AlignX Alignment function deserves some explanation. Its a quirk in the current code.
# When creating a mesh, its very important to consistently point normals of the triangles in the same direction. This procedure is time consuming to
# do computationally in Python as it requires a slow iteration loop across all triangles.
# To speed this up, we can provide a function of (x,y,z) that returns a vector that is used to align the normals. At any point, Optycal will
# Point flip the normal vector such to have a positive dot produt with this alignment function vector. It doesn't matter if its positive or negative,
# We may also create an alignment point such that all normal vecors point away from this origin. 
mesh = sweep_function.mesh(parline, ds, opt.GCS, alignment_function=opt.AlignX)


# Finally we have to turn our Mesh into a Surface. A Mesh is just the geometrical mathematical construct.
# A Surface object in Optycal is a mesh surface with a material stack pointing along the normal direction. You can use a single material layer or
# something like PEC or AIR for full reflection or transparancy.
# Optycal computes the E and H fields on the two sides of the surface. The sides are numbered 1 and 2 in the direction of the normal vectors
# 1 ----> 2

# Define a PEC reflector surface on the mesh.
# opt.FRES_PEC selects a perfect electric conductor PO formulation.
surf = opt.Surface(mesh, opt.FRES_PEC)

# Define patch feed at origin in CS:
# - Patch size: 0.3λ x 0.3λ
# - Pattern: generated by generate_patch_pattern(width, height, k0)
#   where k0 = 2π f0 / c0
ant = opt.Antenna(
    0, 0, 0,
    f0,
    CS,
    *opt.generate_patch_pattern(0.3 * wl, 0.3 * wl, 2 * 3.1415 * f0 / c0)
)

# Couple the feed to the reflector:
# This exposes the reflector surface to the fields from the antenna.
# Optycal does not use Electric and magnetic currents, it directly computes E/H fields to E/H fields.
# Optycal also doesn't compute shadowing effects, if you instruct it to expose a surface with a source,
# it will just expose it without considering reflections itself or obstruction, just the field from point A to point B.
ant.expose_surface(surf)

# 1D far-field cuts from the reflector surface with 0.5° sampling:
azi, ele = opt.FF1D.aziele(0.5)

# Evaluate far-field from the PO surface on both cuts:
surf.expose_ff(azi)
surf.expose_ff(ele)

# Plot normalized azimuth and elevation cuts in one figure:
# - normE / Eiso for directivity-like normalization
# - dB scaling
opt.plot_ff(
    azi.phi,
    [azi.field.normE / opt.Eiso, ele.field.normE / opt.Eiso],
    dB=True,
    labels=['Azi', 'Ele'],
)

# 2D spherical far-field with 0.5° resolution:
# sector=0.5 restricts angular range to focus on main beam region.
ffsph = opt.FF2D.sphere(0.5, sector=0.5)

# Compute 2D far-field from reflector surface:
surf.expose_ff(ffsph)

# Set up 3D visualization:
disp = opt.OptycalDisplay()

# Show reflector surface with field distribution (e.g. |E| on surface).
disp.add_surface_object(surf, field='normE')

# Show antenna feed, color-coded by amplitude.
disp.add_antenna_object(ant, color='amp')

# Add 3D far-field pattern:
# - 'normE' magnitude
# - dB scaling with floor at -50 dB
# - rmax=0.5 for compact visualization
# - offset shifts the pattern slightly in x for better separation from the dish.
disp.add_surf(
    *ffsph.surfplot('normE', 'abs', dB=True, dBfloor=-80, rmax=0.5, offset=(0.2, 0, 0))
)

# Render combined view of reflector, feed, and far-field.
disp.show()
