diff --git a/src/blender_maxwell/assets/internal/source/_source_gaussian_beam.blend b/src/blender_maxwell/assets/internal/source/_source_gaussian_beam.blend
new file mode 100644
index 0000000..f10bd2d
--- /dev/null
+++ b/src/blender_maxwell/assets/internal/source/_source_gaussian_beam.blend
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e10949ae762da334781224d3a48d19b3dc6bc1b44f31ab062da2930cb6312139
+size 1564739
diff --git a/src/blender_maxwell/assets/internal/source/_source_plane_wave.blend b/src/blender_maxwell/assets/internal/source/_source_plane_wave.blend
index 0214a29..3ae77dc 100644
--- a/src/blender_maxwell/assets/internal/source/_source_plane_wave.blend
+++ b/src/blender_maxwell/assets/internal/source/_source_plane_wave.blend
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cb4bb76ea45a8a072a7a7792af0911a9d11483163a0e1aa8486a27eb67bf05a9
-size 1939925
+oid sha256:faf56bee30a9832b850767d620e9a430eef04a80f879ae84239d71391d0b3f61
+size 1484900
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py
index 66649d7..89e6f35 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py
@@ -128,6 +128,7 @@ class ManagedBLMesh(base.ManagedObj):
"""
bl_object = bpy.data.objects.get(self.name)
if bl_object is None:
+ log.info('Created previewable ManagedBLMesh "%s"', bl_object.name)
bl_object = self.bl_object()
if bl_object.name not in preview_collection().objects:
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/__init__.py
index 9381487..6a83d80 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/__init__.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/__init__.py
@@ -16,7 +16,7 @@
from . import (
# astigmatic_gaussian_beam_source,
- # gaussian_beam_source,
+ gaussian_beam_source,
plane_wave_source,
point_dipole_source,
temporal_shapes,
@@ -24,19 +24,19 @@ from . import (
BL_REGISTER = [
*temporal_shapes.BL_REGISTER,
+ *plane_wave_source.BL_REGISTER,
*point_dipole_source.BL_REGISTER,
# *uniform_current_source.BL_REGISTER,
- *plane_wave_source.BL_REGISTER,
- # *gaussian_beam_source.BL_REGISTER,
+ *gaussian_beam_source.BL_REGISTER,
# *astigmatic_gaussian_beam_source.BL_REGISTER,
# *tfsf_source.BL_REGISTER,
]
BL_NODES = {
**temporal_shapes.BL_NODES,
+ **plane_wave_source.BL_NODES,
**point_dipole_source.BL_NODES,
# **uniform_current_source.BL_NODES,
- **plane_wave_source.BL_NODES,
- # **gaussian_beam_source.BL_NODES,
+ **gaussian_beam_source.BL_NODES,
# **astigmatic_gaussian_beam_source.BL_NODES,
# **tfsf_source.BL_NODES,
}
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/gaussian_beam_source.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/gaussian_beam_source.py
index 5b7d824..fc72c76 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/gaussian_beam_source.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/gaussian_beam_source.py
@@ -14,8 +14,239 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import typing as typ
+
+import bpy
+import sympy as sp
+import tidy3d as td
+
+from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
+from blender_maxwell.utils import bl_cache, logger
+from blender_maxwell.utils import extra_sympy_units as spux
+
+from ... import contracts as ct
+from ... import managed_objs, sockets
+from .. import base, events
+
+log = logger.get(__name__)
+
+
+class GaussianBeamSourceNode(base.MaxwellSimNode):
+ """A finite-extent angled source simulating a wave produced by lensed fibers, with focal properties similar to that of ideal lasers, and linear polarization.
+
+ The amplitude envelope is a gaussian function, and the complex electric field is a well-defined frequency-dependent phasor with very few shape parameters.
+
+ The only critical shape parameter is the **waist**: At a chosen "focus" distance, the width of the beam has a chosen radius.
+ These properties are called "waist distance" and "waist radius".
+ At all other points, the width of the beam has a well-defined hyperbolic relationship to the waist, when given the IOR-dependent Rayleigh length.
+
+ - Tidy3D Documentation:
+ - Mathematical Formalism:
+ """
+
+ node_type = ct.NodeType.GaussianBeamSource
+ bl_label = 'Gaussian Beam Source'
+ use_sim_node_name = True
+
+ ####################
+ # - Sockets
+ ####################
+ input_sockets: typ.ClassVar = {
+ 'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
+ 'Center': sockets.ExprSocketDef(
+ shape=(3,),
+ mathtype=spux.MathType.Real,
+ physical_type=spux.PhysicalType.Length,
+ default_value=sp.Matrix([0, 0, 0]),
+ ),
+ 'Size': sockets.ExprSocketDef(
+ shape=(2,),
+ mathtype=spux.MathType.Real,
+ physical_type=spux.PhysicalType.Length,
+ default_value=sp.Matrix([1, 1]),
+ ),
+ 'Waist Dist': sockets.ExprSocketDef(
+ mathtype=spux.MathType.Real,
+ physical_type=spux.PhysicalType.Length,
+ default_value=0.0,
+ ),
+ 'Waist Radius': sockets.ExprSocketDef(
+ mathtype=spux.MathType.Real,
+ physical_type=spux.PhysicalType.Length,
+ default_value=1.0,
+ abs_min=0.01,
+ ),
+ 'Spherical': sockets.ExprSocketDef(
+ shape=(2,),
+ mathtype=spux.MathType.Real,
+ physical_type=spux.PhysicalType.Angle,
+ default_value=sp.Matrix([0, 0]),
+ ),
+ 'Pol ∡': sockets.ExprSocketDef(
+ physical_type=spux.PhysicalType.Angle,
+ default_value=0,
+ ),
+ }
+ output_sockets: typ.ClassVar = {
+ 'Angled Source': sockets.MaxwellSourceSocketDef(),
+ }
+
+ managed_obj_types: typ.ClassVar = {
+ 'mesh': managed_objs.ManagedBLMesh,
+ 'modifier': managed_objs.ManagedBLModifier,
+ }
+
+ ####################
+ # - Properties
+ ####################
+ injection_axis: ct.SimSpaceAxis = bl_cache.BLField(ct.SimSpaceAxis.X, prop_ui=True)
+ injection_direction: ct.SimAxisDir = bl_cache.BLField(
+ ct.SimAxisDir.Plus, prop_ui=True
+ )
+ num_freqs: int = bl_cache.BLField(1, abs_min=1, soft_max=20, prop_ui=True)
+
+ ####################
+ # - UI
+ ####################
+ def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout):
+ layout.prop(self, self.blfields['injection_axis'], expand=True)
+ layout.prop(self, self.blfields['injection_direction'], expand=True)
+ # layout.prop(self, self.blfields['num_freqs'], text='f Points')
+ ## TODO: UI is a bit crowded already!
+
+ ####################
+ # - Outputs
+ ####################
+ @events.computes_output_socket(
+ 'Angled Source',
+ props={'sim_node_name', 'injection_axis', 'injection_direction', 'num_freqs'},
+ input_sockets={
+ 'Temporal Shape',
+ 'Center',
+ 'Size',
+ 'Waist Dist',
+ 'Waist Radius',
+ 'Spherical',
+ 'Pol ∡',
+ },
+ unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
+ scale_input_sockets={
+ 'Center': 'Tidy3DUnits',
+ 'Size': 'Tidy3DUnits',
+ 'Waist Dist': 'Tidy3DUnits',
+ 'Waist Radius': 'Tidy3DUnits',
+ 'Spherical': 'Tidy3DUnits',
+ 'Pol ∡': 'Tidy3DUnits',
+ },
+ )
+ def compute_source(self, props, input_sockets, unit_systems):
+ size_2d = input_sockets['Size']
+ size = {
+ ct.SimSpaceAxis.X: (0, *size_2d),
+ ct.SimSpaceAxis.Y: (size_2d[0], 0, size_2d[1]),
+ ct.SimSpaceAxis.Z: (*size_2d, 0),
+ }[props['injection_axis']]
+
+ # Display the results
+ return td.GaussianBeam(
+ name=props['sim_node_name'],
+ center=input_sockets['Center'],
+ size=size,
+ source_time=input_sockets['Temporal Shape'],
+ num_freqs=props['num_freqs'],
+ direction=props['injection_direction'].plus_or_minus,
+ angle_theta=input_sockets['Spherical'][0],
+ angle_phi=input_sockets['Spherical'][1],
+ pol_angle=input_sockets['Pol ∡'],
+ waist_radius=input_sockets['Waist Radius'],
+ waist_distance=input_sockets['Waist Dist'],
+ ## NOTE: Waist is place at this signed dist along neg. direction
+ )
+
+ ####################
+ # - Preview - Changes to Input Sockets
+ ####################
+ @events.on_value_changed(
+ # Trigger
+ prop_name='preview_active',
+ # Loaded
+ managed_objs={'mesh'},
+ props={'preview_active'},
+ )
+ def on_preview_changed(self, managed_objs, props):
+ """Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen."""
+ mesh = managed_objs['mesh']
+
+ # Push Preview State to Managed Mesh
+ if props['preview_active']:
+ mesh.show_preview()
+ else:
+ mesh.hide_preview()
+
+ @events.on_value_changed(
+ # Trigger
+ socket_name={
+ 'Center',
+ 'Size',
+ 'Waist Dist',
+ 'Waist Radius',
+ 'Spherical',
+ 'Pol ∡',
+ },
+ prop_name={'injection_axis', 'injection_direction'},
+ run_on_init=True,
+ # Loaded
+ managed_objs={'mesh', 'modifier'},
+ props={'injection_axis', 'injection_direction'},
+ input_sockets={
+ 'Temporal Shape',
+ 'Center',
+ 'Size',
+ 'Waist Dist',
+ 'Waist Radius',
+ 'Spherical',
+ 'Pol ∡',
+ },
+ unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
+ scale_input_sockets={
+ 'Center': 'BlenderUnits',
+ },
+ )
+ def on_inputs_changed(self, managed_objs, props, input_sockets, unit_systems):
+ size_2d = input_sockets['Size']
+ size = {
+ ct.SimSpaceAxis.X: sp.Matrix([0, *size_2d]),
+ ct.SimSpaceAxis.Y: sp.Matrix([size_2d[0], 0, size_2d[1]]),
+ ct.SimSpaceAxis.Z: sp.Matrix([*size_2d, 0]),
+ }[props['injection_axis']]
+
+ # Push Input Values to GeoNodes Modifier
+ managed_objs['modifier'].bl_modifier(
+ managed_objs['mesh'].bl_object(location=input_sockets['Center']),
+ 'NODES',
+ {
+ 'node_group': import_geonodes(GeoNodes.SourceGaussianBeam),
+ 'unit_system': unit_systems['BlenderUnits'],
+ 'inputs': {
+ # Orientation
+ 'Inj Axis': props['injection_axis'].axis,
+ 'Direction': props['injection_direction'].true_or_false,
+ 'theta': input_sockets['Spherical'][0],
+ 'phi': input_sockets['Spherical'][1],
+ 'Pol Angle': input_sockets['Pol ∡'],
+ # Gaussian Beam
+ 'Size': size,
+ 'Waist Dist': input_sockets['Waist Dist'],
+ 'Waist Radius': input_sockets['Waist Radius'],
+ },
+ },
+ )
+
+
####################
# - Blender Registration
####################
-BL_REGISTER = []
-BL_NODES = {}
+BL_REGISTER = [
+ GaussianBeamSourceNode,
+]
+BL_NODES = {ct.NodeType.GaussianBeamSource: (ct.NodeCategory.MAXWELLSIM_SOURCES)}
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/plane_wave_source.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/plane_wave_source.py
index 75b2dad..7927836 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/plane_wave_source.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/plane_wave_source.py
@@ -30,6 +30,18 @@ from .. import base, events
class PlaneWaveSourceNode(base.MaxwellSimNode):
+ """An infinite-extent angled source simulating an plane wave with linear polarization.
+
+ The amplitude envelope is a gaussian function, and the complex electric field is a well-defined frequency-dependent phasor with very few shape parameters.
+
+ The only critical shape parameter is the **waist**: At a chosen "focus" distance, the width of the beam has a chosen radius.
+ These properties are called "waist distance" and "waist radius".
+ At all other points, the width of the beam has a well-defined hyperbolic relationship to the waist, when given the IOR-dependent Rayleigh length.
+
+ - Tidy3D Documentation:
+ - Mathematical Formalism:
+ """
+
node_type = ct.NodeType.PlaneWaveSource
bl_label = 'Plane Wave Source'
use_sim_node_name = True