From 1ebb57cff774c28264807e5d1e4a1133a5a5708a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofus=20Albert=20H=C3=B8gsbro=20Rose?= Date: Sun, 10 Mar 2024 11:56:37 +0100 Subject: [PATCH] refactor: Massive architectural changes. See README.md for new, semi-finalized TODO list. --- code/README.md | 588 +++++--- code/blender_maxwell/__init__.py | 22 + .../node_trees/maxwell_sim_nodes/__init__.py | 6 + .../maxwell_sim_nodes/categories.py | 14 +- .../node_trees/maxwell_sim_nodes/contracts.py | 922 ------------ .../maxwell_sim_nodes/contracts/__init__.py | 52 + .../maxwell_sim_nodes/contracts/bl.py | 26 + .../maxwell_sim_nodes/contracts/data_flows.py | 56 + .../maxwell_sim_nodes/contracts/icons.py | 4 + .../contracts/managed_obj_type.py | 9 + .../contracts/node_cat_labels.py | 46 + .../maxwell_sim_nodes/contracts/node_cats.py | 74 + .../maxwell_sim_nodes/contracts/node_types.py | 147 ++ .../contracts/schemas/__init__.py | 4 + .../contracts/schemas/managed_obj.py | 31 + .../contracts/schemas/managed_obj_def.py | 11 + .../contracts/schemas/node.py | 9 + .../contracts/schemas/preset_def.py | 10 + .../contracts/schemas/socket_def.py | 12 + .../contracts/socket_bl_maps.py | 119 ++ .../contracts/socket_colors.py | 73 + .../contracts/socket_shapes.py | 68 + .../contracts/socket_types.py | 89 ++ .../contracts/socket_units.py | 265 ++++ .../maxwell_sim_nodes/contracts/trees.py | 9 + .../managed_objs/__init__.py | 2 + .../managed_objs/managed_bl_image.py | 190 +++ .../managed_objs/managed_bl_object.py | 202 +++ .../node_trees/maxwell_sim_nodes/node_tree.py | 226 ++- .../maxwell_sim_nodes/nodes/__init__.py | 18 +- .../maxwell_sim_nodes/nodes/base.py | 1293 +++++++++++------ .../nodes/inputs/__init__.py | 25 +- .../nodes/inputs/constants/__init__.py | 30 +- .../nodes/inputs/constants/wave_constant.py | 75 +- .../nodes/inputs/importers/__init__.py | 8 + .../inputs/importers/tidy_3d_web_importer.py | 105 ++ .../maxwell_sim_nodes/nodes/kitchen_sink.py | 130 +- .../nodes/mediums/__init__.py | 72 +- .../nodes/mediums/library_medium.py | 169 +-- .../nodes/outputs/__init__.py | 5 +- .../nodes/outputs/exporters/__init__.py | 3 + .../outputs/exporters/json_file_exporter.py | 109 +- .../outputs/exporters/tidy3d_web_exporter.py | 393 +++++ .../nodes/outputs/plotters/__init__.py | 2 - .../maxwell_sim_nodes/nodes/outputs/viewer.py | 97 ++ .../nodes/outputs/viewers/__init__.py | 14 - .../nodes/outputs/viewers/console_viewer.py | 6 - .../nodes/outputs/viewers/value_viewer.py | 6 - .../nodes/outputs/viewers/viewer_3d.py | 50 - .../nodes/simulations/__init__.py | 16 +- .../nodes/simulations/fdtd_sim.py | 74 +- .../nodes/simulations/sim_domain.py | 60 + .../nodes/sources/__init__.py | 24 +- .../nodes/sources/plane_wave_source.py | 114 +- .../nodes/sources/point_dipole_source.py | 80 +- .../nodes/sources/temporal_shapes/__init__.py | 12 +- .../gaussian_pulse_temporal_shape.py | 124 +- .../nodes/structures/__init__.py | 18 +- .../nodes/structures/primitives/__init__.py | 12 +- .../structures/primitives/box_structure.py | 40 +- .../nodes/utilities/combine.py | 2 +- .../maxwell_sim_nodes/sockets/__init__.py | 11 +- .../maxwell_sim_nodes/sockets/base.py | 497 ++++--- .../sockets/basic/any_socket.py | 34 +- .../sockets/basic/bool_socket.py | 35 +- .../sockets/basic/file_path_socket.py | 44 +- .../sockets/basic/text_socket.py | 47 +- .../sockets/blender/__init__.py | 7 - .../sockets/blender/collection_socket.py | 25 +- .../sockets/blender/geonodes_socket.py | 34 +- .../sockets/blender/image_socket.py | 37 +- .../sockets/blender/object_socket.py | 23 +- .../sockets/blender/text_socket.py | 31 +- .../sockets/blender/volume_socket.py | 42 - .../sockets/maxwell/__init__.py | 6 + .../sockets/maxwell/bound_box_socket.py | 91 +- .../sockets/maxwell/bound_face_socket.py | 27 +- .../sockets/maxwell/fdtd_sim_socket.py | 26 +- .../maxwell/medium_non_linearity_socket.py | 28 + .../sockets/maxwell/medium_socket.py | 119 +- .../sockets/maxwell/monitor_socket.py | 51 +- .../sockets/maxwell/sim_domain_socket.py | 28 + .../sockets/maxwell/sim_grid_axis_socket.py | 24 +- .../sockets/maxwell/sim_grid_socket.py | 52 +- .../sockets/maxwell/source_socket.py | 24 +- .../sockets/maxwell/structure_socket.py | 25 +- .../sockets/maxwell/temporal_shape_socket.py | 20 +- .../sockets/number/complex_number_socket.py | 57 +- .../sockets/number/integer_number_socket.py | 30 +- .../sockets/number/rational_number_socket.py | 48 +- .../sockets/number/real_number_socket.py | 55 +- .../sockets/physical/__init__.py | 10 - .../sockets/physical/accel_scalar_socket.py | 31 +- .../sockets/physical/angle_socket.py | 31 +- .../sockets/physical/area_socket.py | 30 +- .../sockets/physical/force_scalar_socket.py | 31 +- .../sockets/physical/freq_socket.py | 37 +- .../sockets/physical/length_socket.py | 29 +- .../sockets/physical/mass_socket.py | 45 +- .../sockets/physical/point_3d_socket.py | 41 +- .../sockets/physical/pol_socket.py | 222 ++- .../sockets/physical/size_3d_socket.py | 30 +- .../physical/spec_power_dist_socket.py | 41 - .../physical/spec_rel_permit_dist_socket.py | 41 - .../sockets/physical/speed_socket.py | 47 +- .../sockets/physical/time_socket.py | 38 +- .../sockets/physical/unit_system_socket.py | 169 +-- .../sockets/physical/vac_wl_socket.py | 54 - .../sockets/physical/volume_socket.py | 62 +- .../sockets/tidy3d/__init__.py | 7 + .../sockets/tidy3d/cloud_task.py | 315 ++++ .../vector/complex_2d_vector_socket.py | 22 +- .../vector/complex_3d_vector_socket.py | 22 +- .../sockets/vector/real_2d_vector_socket.py | 37 +- .../sockets/vector/real_3d_vector_socket.py | 42 +- .../node_trees/maxwell_viz_nodes/__init__.py | 0 code/blender_maxwell/operators/__init__.py | 7 + .../operators/connect_viewer.py | 67 + .../blender_maxwell/operators/install_deps.py | 4 +- .../operators/refresh_td_auth.py | 30 + code/blender_maxwell/operators/types.py | 2 + .../operators/uninstall_deps.py | 1 + code/blender_maxwell/utils/auth_td_web.py | 57 + .../utils/extra_sympy_units.py | 43 + code/blender_maxwell/utils/pydantic_sympy.py | 161 ++ 125 files changed, 6282 insertions(+), 3574 deletions(-) delete mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flows.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/icons.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/managed_obj_type.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_cat_labels.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_cats.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/__init__.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj_def.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/node.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/preset_def.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/socket_def.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_bl_maps.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_colors.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_shapes.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_types.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_units.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/trees.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/__init__.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_object.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/importers/__init__.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/importers/tidy_3d_web_importer.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/tidy3d_web_exporter.py delete mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/plotters/__init__.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py delete mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/__init__.py delete mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/console_viewer.py delete mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/value_viewer.py delete mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/viewer_3d.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py delete mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/volume_socket.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium_non_linearity_socket.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_domain_socket.py delete mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/spec_power_dist_socket.py delete mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/spec_rel_permit_dist_socket.py delete mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/vac_wl_socket.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/__init__.py create mode 100644 code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py delete mode 100644 code/blender_maxwell/node_trees/maxwell_viz_nodes/__init__.py create mode 100644 code/blender_maxwell/operators/connect_viewer.py create mode 100644 code/blender_maxwell/operators/refresh_td_auth.py create mode 100644 code/blender_maxwell/utils/auth_td_web.py create mode 100644 code/blender_maxwell/utils/pydantic_sympy.py diff --git a/code/README.md b/code/README.md index 7b30347..73117da 100644 --- a/code/README.md +++ b/code/README.md @@ -1,239 +1,397 @@ -# Node Design -Now that we can do all the cool things ex. presets and such, it's time to think more design. +# Nodes +## Inputs +[x] Wave Constant +- [ ] Implement export of frequency / wavelength ranges. +[ ] Unit System +- [ ] Implement presets, including "Tidy3D" and "Blender", shown in the label row. -## Nodes -**NOTE**: Throughout, when an object can be selected (ex. for GeoNodes structure to affect), a button should be available to generate a new object for the occasion. +[ ] Constants / Blender Constant +[ ] Constants / Number Constant +[ ] Constants / Physical Constant +- [ ] Pol: Elliptical plot viz +- [ ] Pol: Poincare sphere viz +[ ] Constants / Scientific Constant -**NOTE**: Throughout, all nodes that output floats/vectors should have a sympy dimension. Any node that takes floats/vectors should either have a pre-defined unit (exposed as a string in the node UI), or a selectable unit (ex. for value inputs). +[ ] Web / Tidy3D Web Importer -- Inputs - - Scene - - Time - - Unit System - - - Parameters: Sympy variables. - - *type* Parameter - - Constants: Typed numbers. - - Scientific Constant - - - *type* Constant - - Lists - - *type* List Element - - - File Data: Data from a file. -- Outputs - - Viewers - - Value Viewer: Live-monitoring. - - Console Viewer: w/Button to Print Types - - Exporters - - JSON File Exporter: Compatible with any socket implementing `.as_json()`. - - Plotters - - *various kinds of plotting? To Blender datablocks primarily, maybe*. +[ ] File Import / JSON File Import +- [ ] Dropdown to choose various supported JSON-sourced objects incl. +[ ] File Import / Tidy3D File Import +- [ ] Implement HDF-based import of Tidy3D-exported object (which includes ex. mesh data and such) +[ ] File Import / Array File Import +- [ ] Standardize 1D and 2D array loading/saving on numpy's savetxt with gzip enabled. +- [ ] Implement datatype dropdown to guide format from disk, prefilled to detected. +- [ ] Implement unit system input to guide conversion from numpy data type. +- [ ] Implement a LazyValue to provide a data path that avoids having to load massive arrays every time always. -- Sources - - **ALL**: Accept a Temporal Shape +## Outputs +[ ] Viewer +- [ ] A setting that live-previews just a value. +- [ ] Pop-up multiline string print as alternative to console print. +- [ ] Toggleable auto-plot, auto-3D-preview, auto-value-view, (?)auto-text-view. + +[ ] File Export / JSON File Export +[ ] File Import / Tidy3D File Export +- [ ] Implement HDF-based export of Tidy3D-exported object (which includes ex. mesh data and such) +[ ] File Export / Array File Export +- [ ] Implement datatype dropdown to guide format on disk. +- [ ] Implement unit system input to guide conversion to numpy data type. +- [ ] Standardize 1D and 2D array loading/saving on numpy's savetxt with gzip enabled. + +## Viz +[ ] Monitor Data Viz +- [ ] Implement dropdown to choose which monitor in the SimulationData should be visualized (based on which are available in the SimulationData), and implement visualization based on every kind of monitor-adjascent output data type () + +## Sources +[ ] Temporal Shapes / Gaussian Pulse Temporal Shape +[ ] Temporal Shapes / Continuous Wave Temporal Shape +[ ] Temporal Shapes / Symbolic Temporal Shape +- [ ] Specify a Sympy function to generate appropriate array based on +[ ] Temporal Shapes / Array Temporal Shape + +[ ] Point Dipole Source +[ ] Plane Wave Source +- [ ] Implement an oriented vector input with 3D preview. +[ ] Uniform Current Source +[ ] TFSF Source + +[ ] Gaussian Beam Source +[ ] Astigmatic Gaussian Beam Source + +[ ] Mode Source + +[ ] Array Source / EH Array Source +[ ] Array Source / EH Equivilance Array Source + +## Mediums +[x] Library Medium +- [ ] Implement frequency range output +[ ] PEC Medium +[ ] Isotropic Medium +[ ] Anisotropic Medium + +[ ] Sellmeier Medium +[ ] Drude Medium +[ ] Drude-Lorentz Medium +[ ] Debye Medium +[ ] Pole-Residue Medium - - Temporal Shapes - - Gaussian Pulse Temporal Shape - - Continuous Wave Temporal Shape - - Array Temporal Shape - - - Point Dipole Source - - Uniform Current Source - - Plane Wave Source - - Mode Source - - Gaussian Beam Source - - Astigmatic Gaussian Beam Source - - TFSF Source - - - E/H Equivalence Array Source - - E/H Array Source +[ ] Non-Linearity / `chi_3` Susceptibility Non-Linearity +[ ] Non-Linearity / Two-Photon Absorption Non-Linearity +[ ] Non-Linearity / Kerr Non-Linearity + +[ ] Space/Time epsilon/mu Modulation + +## Structures +[ ] BLObject Structure +[ ] GeoNodes Structure +- [ ] Use the modifier itself as memory, via the ManagedObj +- [?] When GeoNodes themselves declare panels, implement a grid-like tab system to select which sockets should be exposed in the node at a given point in time. + +[ ] Primitive Structures / Plane +[ ] Primitive Structures / Box Structure +[ ] Primitive Structures / Sphere +[ ] Primitive Structures / Cylinder +[ ] Primitive Structures / Ring +[ ] Primitive Structures / Capsule +[ ] Primitive Structures / Cone + +## Monitors +- **ALL**: "Steady-State" / "Time Domain" (only if relevant). + +[ ] E/H Field Monitor +- [ ] Monitor Domain as dropdown with Frequency or Time +- [ ] Axis-aligned planar 2D (pixel) and coord-aligned box 3D (voxel). +[ ] Field Power Flux Monitor +- [ ] Monitor Domain as dropdown with Frequency or Time +- [ ] Axis-aligned planar 2D (pixel) and coord-aligned box 3D (voxel). +[ ] \epsilon Tensor Monitor +- [ ] Axis-aligned planar 2D (pixel) and coord-aligned box 3D (voxel). +[ ] Diffraction Monitor +- [ ] Axis-aligned planar 2D (pixel) + +[ ] Projected E/H Field Monitor / Cartesian Projected E/H Field Monitor +- [ ] Use to implement the metalens: +[ ] Projected E/H Field Monitor / Angle Projected E/H Field Monitor +[ ] Projected E/H Field Monitor / K-Space Projected E/H Field Monitor + +- **TODO**: "Modal" solver monitoring (seems to be some kind of spatial+frequency feature, which an EM field can be decomposed into using a specially configured solver, which can be used to look for very particular kinds of effects by constraining investigations of a solver result to filter out everything that isn't these particular modes aka. features. Kind of a fourier-based redimensionalization, almost). + +## Simulations +[-] FDTDSim + +[-] Sim Domain +- [ ] By-Medium batching of Structures when building the td.Simulation object, which can have significant performance implications. + +[-] Boundary Conds +- [ ] Rename from Bounds / BoundBox +[ ] Boundary Cond / PML Bound Face +- [ ] Implement dropdown for "Normal" and "Stable" +[ ] Boundary Cond / PEC Bound Face +[ ] Boundary Cond / PMC Bound Face +[ ] Boundary Cond / Bloch Bound Face +[ ] Boundary Cond / Periodic Bound Face +[ ] Boundary Cond / Absorbing Bound Face + +[ ] Sim Grid +[ ] Sim Grid Axes / Auto Sim Grid Axis +[ ] Sim Grid Axes / Manual Sim Grid Axis +[ ] Sim Grid Axes / Uniform Sim Grid Axis +[ ] Sim Grid Axes / Array Sim Grid Axis + +## Converters +[ ] Math +- [ ] Implement common operations w/secondary choice of socket type based on a custom internal data structure +- [ ] Implement angfreq/frequency/vacwl conversion. +[ ] Separate +[ ] Combine +- [ ] Implement concatenation of sim-critical socket types into their multi-type -- Mediums - - **ALL**: Accept spatial field. Else, spatial uniformity. - - **ALL**: Accept non-linearity. Else, linear. - - **ALL**: Accept space-time modulation. Else, static. - - - Library Medium - - **NOTE**: Should provide an EnumProperty of materials with its own categorizations. It should provide another EnumProperty to choose the experiment. It should also be filterable by wavelength range, maybe also model info. Finally, a reference should be generated on use as text. - - - PEC Medium - - Isotropic Medium - - Anisotropic Medium - - - 3-Sellmeier Medium - - Sellmeier Medium - - Pole-Residue Medium - - Drude Medium - - Drude-Lorentz Medium - - Debye Medium - - - Non-Linearities - - Add Non-Linearity - - \chi_3 Susceptibility Non-Linearity - - Two-Photon Absorption Non-Linearity - - Kerr Non-Linearity - - - Space/Time \epsilon/\mu Modulation +# GeoNodes +[ ] Tests / Monkey (suzanne deserves to be simulated, she may need manifolding up though :)) +[ ] Tests / Wood Pile -- Structures - - Object Structure - - GeoNodes Structure - - Scripted Structure - - - Primitives - - Box Structure - - Sphere Structure - - Cylinder Structure +[ ] Primitives / Plane +[ ] Primitives / Box +[ ] Primitives / Sphere +[ ] Primitives / Cylinder +[ ] Primitives / Ring +[ ] Primitives / Capsule +[ ] Primitives / Cone + +[ ] Array / Square Array **NOTE: Ring and cylinder** +[ ] Array / Hex Array **NOTE: Ring and cylinder** +[ ] Hole Array / Square Hole Array: Takes a primitive hole shape. +[ ] Hole Array / Hex Hole Array: Takes a primitive hole shape. +[ ] Cavity Array / Hex Array w/ L-Cavity +[ ] Cavity Array / Hex Array w/ H-Cavity + +[ ] Crystal Sphere Lattice / Sphere FCC Array +[ ] Crystal Sphere Lattice / Sphere BCC Array -- Bounds - - Bound Box - - - Bound Faces - - PML Bound Face: "Normal"/"Stable" - - PEC Bound Face - - PMC Bound Face - - - Bloch Bound Face - - Periodic Bound Face - - Absorbing Bound Face - - -- Monitors - - **ALL**: "Steady-State" / "Time Domain" (only if relevant). - - - E/H Field Monitor: "Steady-State" - - Field Power Flux Monitor - - \epsilon Tensor Monitor - - Diffraction Monitor - - - **TODO**: "Modal" solver monitoring (seems to be some kind of spatial+frequency feature, which an EM field can be decomposed into using a specially configured solver, which can be used to look for very particular kinds of effects by constraining investigations of a solver result to filter out everything that isn't these particular modes aka. features. Kind of a fourier-based redimensionalization, almost). - - **TODO**: Near-field projections like so: - - Cartesian Near-Field Projection Monitor - - Observation Angle Near-Field Projection Monitor - - K-Space Near-Field Projection Monitor +# Benchmark / Example Sims +- [ ] Tunable Chiral Metasurface -- Simulations - - Sim Grid - - Sim Grid Axis - - Automatic Sim Grid Axis - - Manual Sim Grid Axis - - Uniform Sim Grid Axis - - Array Sim Grid Axis - - - FDTD Sim +# Sockets +## Basic +[ ] Any +[ ] Bool +[ ] String +- [ ] Rename from "Text" +[ ] File Path + +## Blender +[ ] Object +[ ] Collection + +[ ] Image + +[ ] GeoNodes +[ ] Text + +## Maxwell +[ ] Bound Conds +[ ] Bound Cond + +[ ] Medium +[ ] Medium Non-Linearity + +[ ] Source +[ ] Temporal Shape + +[ ] Structure +[ ] Monitor + +[ ] FDTD Sim +[ ] Sim Domain +- [?] Toggleable option to sync the simulation time duration to the scene end time (how to handle FPS vs time-step? Should we adjust the FPS such that there is one time step per frame, while keeping the definition of "second" aligned to a unit system?) +[ ] Sim Grid +[ ] Sim Grid Axis + +[ ] Simulation Data + +## Tidy3D +[ ] Cloud Task + +## Number +[ ] Integer +[ ] Rational +[ ] Real +[ ] Complex + +## Physical +[ ] Unit System +- [ ] Implement more comprehensible UI; honestly, probably with the new panels () + +[ ] Time + +[ ] Angle +[ ] Solid Angle (steradian) + +[ ] Frequency (hertz) +[ ] Angular Frequency (`rad*hertz`) +### Cartesian +[ ] Length +[ ] Area +[ ] Volume + +[ ] Point 1D +[ ] Point 2D +[ ] Point 3D + +[ ] Size 2D +[ ] Size 3D +### Mechanical +[ ] Mass + +[ ] Speed +[ ] Velocity 3D +[ ] Acceleration Scalar +[ ] Acceleration 3D +[ ] Force Scalar +[ ] Force 3D +[ ] Pressure +### Statistical +[ ] Energy (joule) +[ ] Power (watt) +[ ] Temperature +### Electrodynamical +[ ] Current (ampere) +[ ] Current Density 3D + +[ ] Charge (coulomb) +[ ] Voltage (volts) +[ ] Capacitance (farad) +[ ] Resistance (ohm) +[ ] Electric Conductance (siemens) + +[ ] Magnetic Flux (weber) +[ ] Magnetic Flux Density (tesla) +[ ] Inductance (henry) + +[ ] Electric Field 3D (`volt*meter`) +[ ] Magnetic Field 3D (tesla) +### Luminal +[ ] Luminous Intensity (candela) +[ ] Luminous Flux (lumen) +[ ] Illuminance (lux) +### Optical +[ ] Jones Polarization +[ ] Polarization -- Utilities - - Math: Contains a dropdown for operation. - - *type* Math: **Be careful about units :)** - - Operations - - List Operation - -## Sockets -- basic - - Any - - FilePath - - Text -- number - - IntegerNumber - - RationalNumber - - - RealNumber - - ComplexNumber - - RealNumberField - - ComplexNumberField - -- vector - - Real2DVector - - Complex2DVector - - Real2DVectorField - - Complex2DVectorField - - - Real3DVector - - Complex3DVector - - Real3DVectorField - - Complex3DVectorField -- physics - - PhysicalTime - - - PhysicalAngle - - - PhysicalLength - - PhysicalArea - - PhysicalVolume - - - PhysicalMass - - PhysicalLengthDensity - - PhysicalAreaDensity - - PhysicalVolumeDensity - - - PhysicalSpeed - - PhysicalAcceleration - - PhysicalForce - - - PhysicalPolarization - - - PhysicalFrequency - - PhysicalSpectralDistribution -- blender - - BlenderObject - - BlenderCollection - - - BlenderGeoNodes - - BlenderImage -- maxwell - - MaxwellMedium - - MaxwellMediumNonLinearity - - - MaxwellStructure - - - MaxwellBoundBox - - MaxwellBoundFace - - - MaxwellMonitor - - - MaxwellSimGrid - - - FDTDSim +# Style +[ ] Rethink the meaning of color and shapes in node sockets, including whether dynamic functionality is needed when it comes to socket shape (ex. it might be nice to know whether a socket is array-like or uses units). +[ ] Rethink the meaning of color and shapes in node sockets, including whether dynamic functionality is needed when it comes to socket shape. -### GeoNode Trees -For ease of use, we can ship with premade node trees/groups for: -- Primitives - - Plane - - Box - - Sphere - - Cylinder - - Ring - - Capsule - - Cone -- Array - - Square Array: Takes a primitive shape. - - Hex Array: Takes a primitive shape. -- Hole Array - - Square Hole Array: Takes a primitive hole shape. - - Hex Hole Array: Takes a primitive hole shape. -- Cavities - - Hex Array w/ L-Cavity: Takes a primitive hole shape. - - Hex Array w/ H-Cavity: Takes a primitive hole shape. -- Crystal - - FCC Sphere Array: Takes a primitive spherical-like shape. - - BCC Sphere Array: Takes a primitive spherical-like shape. -- Wood Pile +# Architecture +## Registration and Contracts +[ ] Finish the contract code converting from Blender sockets to our sockets based on dimensionality and the property description. +[ ] Refactor the node category code; it's ugly as all fuck. +[?] Would be nice with some kind of indicator somewhere to help set good socket descriptions when using geonodes and wanting units. -When it comes to geometry, we do need to make sure +## Managed Objects +[ ] Implement modifier support on the managed BL object, with special attention paid to the needs of the GeoNodes socket. +- [ ] Implement preview toggling too, ex. using the relevant node tree collections +- Remember, the managed object is "dumb". It's the node's responsibility to react to any relevant `on_value_change`, and forward all state needed by the modifier to the managed obj. It's only the managed obj's responsibility to not update any modifier value that wouldn't change anything. +[ ] Implement loading the xarray-defined voxels into OpenVDB, saving it, and loading it as a managed BL object with the volume setting. +[ ] Implement basic jax-driven volume voxel processing, especially cube based slicing. +[ ] Implement jax-driven linear interpolation of volume voxels to an image texture, whose pixels are sized according to the dimensions of another managed plane object (perhaps a uniquely described Managed BL object itself). -### Notes -**NOTE**: When several geometries assigned to the same medium are assigned to the same `tidy3d.GeometryGroup`, there can apparently be "significant performance enhancement"s (). -- We can and should, in the Simulation builder (or just the structure concatenator), batch together structures with the same Medium. +## Node Base Class +[ ] Dedicated `draw_preview`-type draw functions for plot customizations. +- [ ] For now, previewing isn't something I think should be part of the node +[ ] Custom `@cache`/`@lru_cache`/`@cached_property` which caches by instance ID (possibly based on `beartype` or `pydantic`). +[ ] When presets are used, if a preset is selected and the user alters a preset setting, then dynamically switch the preset indicator back to "Custom" to indicate that there is no active preset +[ ] It seems that `node.inputs` and `node.outputs` allows the use of a `move` method, which may allow reordering sockets dynamically, which we should expose to the user as user-configurable ordering rules (maybe resolved with a constraint solver). +[?] Mechanism for dynamic names (ex. "Library Medium" becoming "Au Medium") +[ ] Mechanism for selecting a blender object managed by a particular node. +[ ] Mechanism for ex. specially coloring a node that is currently participating in the preview. -**NOTE**: Some symmetries can be greatly important for performance. +## Socket Base Class +[ ] A feature `use_array` which allows a socket to declare that it can be both a single value and array-like (possibly constrained to a given shape). This should also allow the SocketDef to request that the input socket be initialised as a multi-input socket, once Blender updates to support those. +- [ ] Implement a shape-selector, with a dropdown for dimensionality and an appropriate `IntegerVectorProperty` for each kind of shape (supporting also straight-up inf), which is declared to the node that supports array-likeness so it can decide how exactly to expose properties in the array-like context of things. +[ ] Make `to_socket`s no-consent to new links from `from_socket`s of differing type (we'll see if this controls the typing story enough for now, and how much we'll need capabilities in the long run) +- [?] Alternatively, reject non matching link types, and red-mark non matching capabilities? + +## Many Nodes +[ ] Implement LazyValue stuff, including LazyParamValue on a new class of constant-like input nodes that really just emit ex. sympy variables. +[?] Require a Unit System for nodes that construct Tidy3D objects +[ ] Medium Features +- [ ] Accept spatial field. Else, spatial uniformity. +- [ ] Accept non-linearity. Else, linear. +- [ ] Accept space-time modulation. Else, static. +[ ] Modal Features +- [ ] ModeSpec, for use by ModeSource, ModeMonitor, ModeSolverMonitor. Data includes ModeSolverData, ModeData, ScalarModeFieldDataArray, ModeAmpsDataArray, ModeIndexDataArray, ModeSolver. + +## Development Tooling +[ ] Implement `rye` support +[ ] Setup neovim to be an ideal editor + +## Version Churn +[ ] Implement real StrEnum sockets, since they appear in py3.11 +[ ] Think about implementing new panels where appropriate () +[ ] Think about using the new bl4.1 file handler API to enable drag and drop creation of appropriate nodes (for importing files without hassle). +[ ] Keep an eye on our manual `__annotations__` hacking; python 3.13 is apparently fucking with it. +[ ] Plan for multi-input sockets (for now, just use the "Combine" node and have seperate socket types for both). +[ ] Keep an eye out for volume geonodes in 4.2 (July 16, 2024), which will better allow for more complicated volume processing (we might still want/need the jax based stuff after, but let's keep it minimal just in case) + +## Packaging +[ ] Allow specifying custom dir for keeping pip dependencies, so we can unify prod and dev (currently we hard-code a dev dependency path). +[ ] Refactor top-level `__init__.py` to check dependencies first. If not everything is available, it should only register a minimal addon; specifically, a message telling the user that the addon requires additional dependencies (list which), and the button to install them. When the installation is done, re-check deps and register the rest of the addon. +[ ] Use a Modal and multiline-text-like construction to print `pip install` as we install dependencies, so that the user has an idea that something is happening. +[ ] Test on Windows + +## Node Tree Cache Semantics + +## Projects / Plugins +### Field Data +[ ] Directly dealing with field data, instead of having field manipulations be baked into viz node(s). +[ ] Yee Cell Data as Attributes on By-Cell Point Cloud w/GeoNodes Integrations +- In effect, when we have xarray data defined based on Yee Cells ex. Poynting vector coordinates, let's import this to Blender as a simple point cloud centered at each cell and grant each an attribute corresponding to the data. +- What we can then do is use vanilla GeoNodes to ex. read the vector attribute, and draw small arrow meshes (maybe resampled which auto-interpolates the field values) from each point, thus effectively visualizing . vector fields and many other fun things. +- Of course, this is no good for volume cell data - but we can just overlay the raw volume cell data as we please. We can also, if we're sneaky, deal with our volume data as points as far as we can, and then finally do a "points to volume" type deal to make it sufficiently "fluffy/cloudy". +- I wonder if we could use the Attribute node in the shader editor to project interpolated values from points, onto a ex. plane mesh, in a way that would also be visualizable in the viewport. + +### Tidy3D Features +[ ] Symmetry for Performance +- [ ] Implement +[ ] Dispersive Model Fitting +[ ] Scattering Matrix Calculator +[ ] Resonance Finder +[ ] Adjoint Optimization +[ ] Design Space Exploration / Parameterization + +### Preview Semantics +[ ] Custom gizmos attached to preview toggles! +- There is a WIP for GN-driven gizmos: +- Probably best to wait for that, then just add gizmos to existing driven GN trees, as opposed to unholy OGL spaghetti. +[ ] Node-ManagedObj Selection binding +- BL to Node: + - Trigger: The post-depsgraph handler seems appropriate. + - Input: Read the object location (origin), using a unit system. + - Output: Write the input socket value. + - Condition: Input socket is unlinked. (If it's linked, then lock the object's position. Use sync_link_added() for that) +- Node to BL: + - Trigger: "Report" action on an input socket that the managed object declares reliance on. + - Input: The input socket value (linked or unlinked) + - Output: The object location (origin), using a unit system. + +### Parametric Geometry UX +[ ] Consider allowing a mesh attribute (set in ex. geometry node) to specify the name of a medium. +- This allows assembling complex multi-medium structures in one geonodes tree. +- This should result in the spawning of several Medium input sockets in the GeoNodes structure node, named as the attributes are. +- The GeoNodes structure node should then output as array-like TriMeshes, for which mediums are correctly defined. + +### Alternative Engines +[ ] MEEP integration () +- The main boost would be if we could setup a MEEP simulation entirely from a td.Simulation object. diff --git a/code/blender_maxwell/__init__.py b/code/blender_maxwell/__init__.py index 7f4bb27..06fc523 100644 --- a/code/blender_maxwell/__init__.py +++ b/code/blender_maxwell/__init__.py @@ -46,17 +46,39 @@ BL_REGISTER = [ *operators.BL_REGISTER, *preferences.BL_REGISTER, ] +BL_KMI_REGISTER = [ + *operators.BL_KMI_REGISTER, +] BL_NODE_CATEGORIES = [ *node_trees.BL_NODE_CATEGORIES, ] +km = bpy.context.window_manager.keyconfigs.addon.keymaps.new( + name='Node Editor', + space_type="NODE_EDITOR", +) +REGISTERED_KEYMAPS = [] def register(): + global REGISTERED_KEYMAPS + for cls in BL_REGISTER: bpy.utils.register_class(cls) + + for kmi_def in BL_KMI_REGISTER: + kmi = km.keymap_items.new( + *kmi_def["_"], + ctrl=kmi_def["ctrl"], + shift=kmi_def["shift"], + alt=kmi_def["alt"], + ) + REGISTERED_KEYMAPS.append(kmi) def unregister(): for cls in reversed(BL_REGISTER): bpy.utils.unregister_class(cls) + + for kmi in REGISTERED_KEYMAPS: + km.keymap_items.remove(kmi) if __name__ == "__main__": register() diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py index 0c8d410..e268bfa 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py @@ -1,3 +1,9 @@ +import sympy as sp +sp.printing.str.StrPrinter._default_settings['abbrev'] = True +## In this tree, all Sympy unit printing must be abbreviated. +## By configuring this in __init__.py, we guarantee it for all subimports. +## (Unless, elsewhere, this setting is changed. Be careful!) + from . import sockets from . import node_tree from . import nodes diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/categories.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/categories.py index 221a3c9..3be0191 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/categories.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/categories.py @@ -2,7 +2,7 @@ import bpy import nodeitems_utils -from . import contracts +from . import contracts as ct from .nodes import BL_NODES DYNAMIC_SUBMENU_REGISTRATIONS = [] @@ -15,7 +15,7 @@ def mk_node_categories( items = [] # Add Node Items - base_category = contracts.NodeCategory["_".join(syllable_prefix)] + base_category = ct.NodeCategory["_".join(syllable_prefix)] for node_type, node_category in BL_NODES.items(): if node_category == base_category: items.append(nodeitems_utils.NodeItem(node_type.value)) @@ -23,7 +23,7 @@ def mk_node_categories( # Add Node Sub-Menus for syllable, sub_tree in tree.items(): current_syllable_path = syllable_prefix + [syllable] - current_category = contracts.NodeCategory[ + current_category = ct.NodeCategory[ "_".join(current_syllable_path) ] @@ -54,9 +54,9 @@ def mk_node_categories( self.layout.menu(submenu_id) return draw - menu_class = type(current_category.value, (bpy.types.Menu,), { + menu_class = type(str(current_category.value), (bpy.types.Menu,), { 'bl_idname': current_category.value, - 'bl_label': contracts.NodeCategory_to_category_label[current_category], + 'bl_label': ct.NODE_CAT_LABELS[current_category], 'draw': draw_factory(tuple(subitems)), }) @@ -72,7 +72,7 @@ def mk_node_categories( # - Blender Registration #################### BL_NODE_CATEGORIES = mk_node_categories( - contracts.NodeCategory.get_tree()["MAXWELLSIM"], + ct.NodeCategory.get_tree()["MAXWELLSIM"], syllable_prefix = ["MAXWELLSIM"], ) ## TODO: refactor, this has a big code smell @@ -82,7 +82,7 @@ BL_REGISTER = [ ## TEST - TODO this is a big code smell def menu_draw(self, context): - if context.space_data.tree_type == contracts.TreeType.MaxwellSim.value: + if context.space_data.tree_type == ct.TreeType.MaxwellSim.value: for nodeitem_or_submenu in BL_NODE_CATEGORIES: if isinstance(nodeitem_or_submenu, str): submenu_id = nodeitem_or_submenu diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts.py deleted file mode 100644 index 03323d5..0000000 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts.py +++ /dev/null @@ -1,922 +0,0 @@ -import typing as typ -import typing_extensions as pytypes_ext -import enum - -import sympy as sp - -sp.printing.str.StrPrinter._default_settings['abbrev'] = True -## When we str() a unit expression, use abbrevied units. - -import sympy.physics.units as spu -import pydantic as pyd -import bpy - -from ...utils.blender_type_enum import ( - BlenderTypeEnum, append_cls_name_to_values, wrap_values_in_MT -) -from ...utils import extra_sympy_units as spuex - -#################### -# - String Types -#################### -BlenderColorRGB = tuple[float, float, float, float] -BlenderID = pytypes_ext.Annotated[str, pyd.StringConstraints( - pattern=r'^[A-Z_]+$', -)] - -# Socket ID -SocketName = pytypes_ext.Annotated[str, pyd.StringConstraints( - pattern=r'^[a-zA-Z0-9_]+$', -)] -BLSocketName = pytypes_ext.Annotated[str, pyd.StringConstraints( - pattern=r'^[a-zA-Z0-9_]+$', -)] - -# Socket ID -PresetID = pytypes_ext.Annotated[str, pyd.StringConstraints( - pattern=r'^[A-Z_]+$', -)] - -#################### -# - Sympy Expression Typing -#################### -ALL_UNIT_SYMBOLS = { - unit - for unit in spu.__dict__.values() - if isinstance(unit, spu.Quantity) -} -def has_units(expr: sp.Expr): - return any( - symbol in ALL_UNIT_SYMBOLS - for symbol in expr.atoms(sp.Symbol) - ) -def is_exactly_expressed_as_unit(expr: sp.Expr, unit) -> bool: - #try: - converted_expr = expr / unit - - return ( - converted_expr.is_number - and not converted_expr.has(spu.Quantity) - ) - -#################### -# - Icon Types -#################### -class Icon(BlenderTypeEnum): - MaxwellSimTree = "MOD_SIMPLEDEFORM" - -#################### -# - Tree Types -#################### -@append_cls_name_to_values -class TreeType(BlenderTypeEnum): - MaxwellSim = enum.auto() - -#################### -# - Socket Types -#################### -@append_cls_name_to_values -class SocketType(BlenderTypeEnum): - # Base - Any = enum.auto() - Bool = enum.auto() - Text = enum.auto() - FilePath = enum.auto() - - # Number - IntegerNumber = enum.auto() - RationalNumber = enum.auto() - RealNumber = enum.auto() - ComplexNumber = enum.auto() - - # Vector - Real2DVector = enum.auto() - Complex2DVector = enum.auto() - - Real3DVector = enum.auto() - Complex3DVector = enum.auto() - - # Physical - PhysicalUnitSystem = enum.auto() - PhysicalTime = enum.auto() - - PhysicalAngle = enum.auto() - - PhysicalLength = enum.auto() - PhysicalArea = enum.auto() - PhysicalVolume = enum.auto() - - PhysicalPoint2D = enum.auto() - PhysicalPoint3D = enum.auto() - - PhysicalSize2D = enum.auto() - PhysicalSize3D = enum.auto() - - PhysicalMass = enum.auto() - - PhysicalSpeed = enum.auto() - PhysicalAccelScalar = enum.auto() - PhysicalForceScalar = enum.auto() - PhysicalAccel3DVector = enum.auto() - PhysicalForce3DVector = enum.auto() - - PhysicalPol = enum.auto() - - PhysicalFreq = enum.auto() - PhysicalVacWL = enum.auto() - PhysicalSpecPowerDist = enum.auto() - PhysicalSpecRelPermDist = enum.auto() - - # Blender - BlenderObject = enum.auto() - BlenderCollection = enum.auto() - - BlenderImage = enum.auto() - BlenderVolume = enum.auto() - - BlenderGeoNodes = enum.auto() - BlenderText = enum.auto() - - BlenderPreviewTarget = enum.auto() - - # Maxwell - MaxwellSource = enum.auto() - MaxwellTemporalShape = enum.auto() - - MaxwellMedium = enum.auto() - MaxwellMediumNonLinearity = enum.auto() - - MaxwellStructure = enum.auto() - - MaxwellBoundBox = enum.auto() - MaxwellBoundFace = enum.auto() - - MaxwellMonitor = enum.auto() - - MaxwellFDTDSim = enum.auto() - MaxwellSimGrid = enum.auto() - MaxwellSimGridAxis = enum.auto() - -SocketType_to_units = { - SocketType.PhysicalTime: { - "default": "PS", - "values": { - "PS": spu.picosecond, - "NS": spu.nanosecond, - "MS": spu.microsecond, - "MLSEC": spu.millisecond, - "SEC": spu.second, - "MIN": spu.minute, - "HOUR": spu.hour, - "DAY": spu.day, - }, - }, - - SocketType.PhysicalAngle: { - "default": "RADIAN", - "values": { - "RADIAN": spu.radian, - "DEGREE": spu.degree, - "STERAD": spu.steradian, - "ANGMIL": spu.angular_mil, - }, - }, - - SocketType.PhysicalLength: { - "default": "UM", - "values": { - "PM": spu.picometer, - "A": spu.angstrom, - "NM": spu.nanometer, - "UM": spu.micrometer, - "MM": spu.millimeter, - "CM": spu.centimeter, - "M": spu.meter, - "INCH": spu.inch, - "FOOT": spu.foot, - "YARD": spu.yard, - "MILE": spu.mile, - }, - }, - SocketType.PhysicalArea: { - "default": "UM_SQ", - "values": { - "PM_SQ": spu.picometer**2, - "A_SQ": spu.angstrom**2, - "NM_SQ": spu.nanometer**2, - "UM_SQ": spu.micrometer**2, - "MM_SQ": spu.millimeter**2, - "CM_SQ": spu.centimeter**2, - "M_SQ": spu.meter**2, - "INCH_SQ": spu.inch**2, - "FOOT_SQ": spu.foot**2, - "YARD_SQ": spu.yard**2, - "MILE_SQ": spu.mile**2, - }, - }, - SocketType.PhysicalVolume: { - "default": "UM_CB", - "values": { - "PM_CB": spu.picometer**3, - "A_CB": spu.angstrom**3, - "NM_CB": spu.nanometer**3, - "UM_CB": spu.micrometer**3, - "MM_CB": spu.millimeter**3, - "CM_CB": spu.centimeter**3, - "M_CB": spu.meter**3, - "ML": spu.milliliter, - "L": spu.liter, - "INCH_CB": spu.inch**3, - "FOOT_CB": spu.foot**3, - "YARD_CB": spu.yard**3, - "MILE_CB": spu.mile**3, - }, - }, - - SocketType.PhysicalPoint2D: { - "default": "UM", - "values": { - "PM": spu.picometer, - "A": spu.angstrom, - "NM": spu.nanometer, - "UM": spu.micrometer, - "MM": spu.millimeter, - "CM": spu.centimeter, - "M": spu.meter, - "INCH": spu.inch, - "FOOT": spu.foot, - "YARD": spu.yard, - "MILE": spu.mile, - }, - }, - SocketType.PhysicalPoint3D: { - "default": "UM", - "values": { - "PM": spu.picometer, - "A": spu.angstrom, - "NM": spu.nanometer, - "UM": spu.micrometer, - "MM": spu.millimeter, - "CM": spu.centimeter, - "M": spu.meter, - "INCH": spu.inch, - "FOOT": spu.foot, - "YARD": spu.yard, - "MILE": spu.mile, - }, - }, - - SocketType.PhysicalSize2D: { - "default": "UM", - "values": { - "PM": spu.picometer, - "A": spu.angstrom, - "NM": spu.nanometer, - "UM": spu.micrometer, - "MM": spu.millimeter, - "CM": spu.centimeter, - "M": spu.meter, - "INCH": spu.inch, - "FOOT": spu.foot, - "YARD": spu.yard, - "MILE": spu.mile, - }, - }, - SocketType.PhysicalSize3D: { - "default": "UM", - "values": { - "PM": spu.picometer, - "A": spu.angstrom, - "NM": spu.nanometer, - "UM": spu.micrometer, - "MM": spu.millimeter, - "CM": spu.centimeter, - "M": spu.meter, - "INCH": spu.inch, - "FOOT": spu.foot, - "YARD": spu.yard, - "MILE": spu.mile, - }, - }, - - SocketType.PhysicalMass: { - "default": "UG", - "values": { - "E_REST": spu.electron_rest_mass, - "DAL": spu.dalton, - "UG": spu.microgram, - "MG": spu.milligram, - "G": spu.gram, - "KG": spu.kilogram, - "TON": spu.metric_ton, - }, - }, - - SocketType.PhysicalSpeed: { - "default": "UM_S", - "values": { - "PM_S": spu.picometer / spu.second, - "NM_S": spu.nanometer / spu.second, - "UM_S": spu.micrometer / spu.second, - "MM_S": spu.millimeter / spu.second, - "M_S": spu.meter / spu.second, - "KM_S": spu.kilometer / spu.second, - "KM_H": spu.kilometer / spu.hour, - "FT_S": spu.feet / spu.second, - "MI_H": spu.mile / spu.hour, - }, - }, - SocketType.PhysicalAccelScalar: { - "default": "UM_S_SQ", - "values": { - "PM_S_SQ": spu.picometer / spu.second**2, - "NM_S_SQ": spu.nanometer / spu.second**2, - "UM_S_SQ": spu.micrometer / spu.second**2, - "MM_S_SQ": spu.millimeter / spu.second**2, - "M_S_SQ": spu.meter / spu.second**2, - "KM_S_SQ": spu.kilometer / spu.second**2, - "FT_S_SQ": spu.feet / spu.second**2, - }, - }, - SocketType.PhysicalForceScalar: { - "default": "UNEWT", - "values": { - "KG_M_S_SQ": spu.kg * spu.m/spu.second**2, - "NNEWT": spuex.nanonewton, - "UNEWT": spuex.micronewton, - "MNEWT": spuex.millinewton, - "NEWT": spu.newton, - }, - }, - SocketType.PhysicalAccel3DVector: { - "default": "UM_S_SQ", - "values": { - "PM_S_SQ": spu.picometer / spu.second**2, - "NM_S_SQ": spu.nanometer / spu.second**2, - "UM_S_SQ": spu.micrometer / spu.second**2, - "MM_S_SQ": spu.millimeter / spu.second**2, - "M_S_SQ": spu.meter / spu.second**2, - "KM_S_SQ": spu.kilometer / spu.second**2, - "FT_S_SQ": spu.feet / spu.second**2, - }, - }, - SocketType.PhysicalForce3DVector: { - "default": "UNEWT", - "values": { - "KG_M_S_SQ": spu.kg * spu.m/spu.second**2, - "NNEWT": spuex.nanonewton, - "UNEWT": spuex.micronewton, - "MNEWT": spuex.millinewton, - "NEWT": spu.newton, - }, - }, - - SocketType.PhysicalFreq: { - "default": "THZ", - "values": { - "HZ": spu.hertz, - "KHZ": spuex.kilohertz, - "MHZ": spuex.megahertz, - "GHZ": spuex.gigahertz, - "THZ": spuex.terahertz, - "PHZ": spuex.petahertz, - "EHZ": spuex.exahertz, - }, - }, - SocketType.PhysicalVacWL: { - "default": "NM", - "values": { - "PM": spu.picometer, ## c(vac) = wl*freq - "A": spu.angstrom, - "NM": spu.nanometer, - "UM": spu.micrometer, - "MM": spu.millimeter, - "CM": spu.centimeter, - "M": spu.meter, - }, - }, -} - -SocketType_to_color = { - # Basic - SocketType.Any: (0.8, 0.8, 0.8, 1.0), # Light Grey - SocketType.Bool: (0.7, 0.7, 0.7, 1.0), # Medium Light Grey - SocketType.Text: (0.7, 0.7, 0.7, 1.0), # Medium Light Grey - SocketType.FilePath: (0.6, 0.6, 0.6, 1.0), # Medium Grey - - # Number - SocketType.IntegerNumber: (0.5, 0.5, 1.0, 1.0), # Light Blue - SocketType.RationalNumber: (0.4, 0.4, 0.9, 1.0), # Medium Light Blue - SocketType.RealNumber: (0.3, 0.3, 0.8, 1.0), # Medium Blue - SocketType.ComplexNumber: (0.2, 0.2, 0.7, 1.0), # Dark Blue - - # Vector - SocketType.Real2DVector: (0.5, 1.0, 0.5, 1.0), # Light Green - SocketType.Complex2DVector: (0.4, 0.9, 0.4, 1.0), # Medium Light Green - SocketType.Real3DVector: (0.3, 0.8, 0.3, 1.0), # Medium Green - SocketType.Complex3DVector: (0.2, 0.7, 0.2, 1.0), # Dark Green - - # Physical - SocketType.PhysicalUnitSystem: (1.0, 0.5, 0.5, 1.0), # Light Red - SocketType.PhysicalTime: (1.0, 0.5, 0.5, 1.0), # Light Red - SocketType.PhysicalAngle: (0.9, 0.45, 0.45, 1.0), # Medium Light Red - SocketType.PhysicalLength: (0.8, 0.4, 0.4, 1.0), # Medium Red - SocketType.PhysicalArea: (0.7, 0.35, 0.35, 1.0), # Medium Dark Red - SocketType.PhysicalVolume: (0.6, 0.3, 0.3, 1.0), # Dark Red - SocketType.PhysicalPoint2D: (0.7, 0.35, 0.35, 1.0), # Medium Dark Red - SocketType.PhysicalPoint3D: (0.6, 0.3, 0.3, 1.0), # Dark Red - SocketType.PhysicalSize2D: (0.7, 0.35, 0.35, 1.0), # Medium Dark Red - SocketType.PhysicalSize3D: (0.6, 0.3, 0.3, 1.0), # Dark Red - SocketType.PhysicalMass: (0.9, 0.6, 0.4, 1.0), # Light Orange - SocketType.PhysicalSpeed: (0.8, 0.55, 0.35, 1.0), # Medium Light Orange - SocketType.PhysicalAccelScalar: (0.7, 0.5, 0.3, 1.0), # Medium Orange - SocketType.PhysicalForceScalar: (0.6, 0.45, 0.25, 1.0), # Medium Dark Orange - SocketType.PhysicalAccel3DVector: (0.7, 0.5, 0.3, 1.0), # Medium Orange - SocketType.PhysicalForce3DVector: (0.6, 0.45, 0.25, 1.0), # Medium Dark Orange - SocketType.PhysicalPol: (0.5, 0.4, 0.2, 1.0), # Dark Orange - SocketType.PhysicalFreq: (1.0, 0.7, 0.5, 1.0), # Light Peach - SocketType.PhysicalVacWL: (1.0, 0.7, 0.5, 1.0), # Light Peach - SocketType.PhysicalSpecPowerDist: (0.9, 0.65, 0.45, 1.0), # Medium Light Peach - SocketType.PhysicalSpecRelPermDist: (0.8, 0.6, 0.4, 1.0), # Medium Peach - - # Blender - SocketType.BlenderObject: (0.7, 0.5, 1.0, 1.0), # Light Purple - SocketType.BlenderCollection: (0.6, 0.45, 0.9, 1.0), # Medium Light Purple - SocketType.BlenderImage: (0.5, 0.4, 0.8, 1.0), # Medium Purple - SocketType.BlenderVolume: (0.4, 0.35, 0.7, 1.0), # Medium Dark Purple - SocketType.BlenderGeoNodes: (0.3, 0.3, 0.6, 1.0), # Dark Purple - SocketType.BlenderText: (0.5, 0.5, 0.75, 1.0), # Light Lavender - SocketType.BlenderPreviewTarget: (0.5, 0.5, 0.75, 1.0), # Light Lavender - - # Maxwell - SocketType.MaxwellSource: (1.0, 1.0, 0.5, 1.0), # Light Yellow - SocketType.MaxwellTemporalShape: (0.9, 0.9, 0.45, 1.0), # Medium Light Yellow - SocketType.MaxwellMedium: (0.8, 0.8, 0.4, 1.0), # Medium Yellow - SocketType.MaxwellMediumNonLinearity: (0.7, 0.7, 0.35, 1.0), # Medium Dark Yellow - SocketType.MaxwellStructure: (0.6, 0.6, 0.3, 1.0), # Dark Yellow - SocketType.MaxwellBoundBox: (0.9, 0.8, 0.5, 1.0), # Light Gold - SocketType.MaxwellBoundFace: (0.8, 0.7, 0.45, 1.0), # Medium Light Gold - SocketType.MaxwellMonitor: (0.7, 0.6, 0.4, 1.0), # Medium Gold - SocketType.MaxwellFDTDSim: (0.6, 0.5, 0.35, 1.0), # Medium Dark Gold - SocketType.MaxwellSimGrid: (0.5, 0.4, 0.3, 1.0), # Dark Gold - SocketType.MaxwellSimGridAxis: (0.4, 0.3, 0.25, 1.0), # Darkest Gold -} - -BLNodeSocket_to_SocketType = { - 1: { - "NodeSocketStandard": SocketType.Any, - "NodeSocketVirtual": SocketType.Any, - "NodeSocketGeometry": SocketType.Any, - "NodeSocketTexture": SocketType.Any, - "NodeSocketShader": SocketType.Any, - "NodeSocketMaterial": SocketType.Any, - - "NodeSocketString": SocketType.Text, - "NodeSocketBool": SocketType.Bool, - "NodeSocketCollection": SocketType.BlenderCollection, - "NodeSocketImage": SocketType.BlenderImage, - "NodeSocketObject": SocketType.BlenderObject, - - "NodeSocketFloat": SocketType.RealNumber, - "NodeSocketFloatAngle": SocketType.PhysicalAngle, - "NodeSocketFloatDistance": SocketType.PhysicalLength, - "NodeSocketFloatFactor": SocketType.RealNumber, - "NodeSocketFloatPercentage": SocketType.RealNumber, - "NodeSocketFloatTime": SocketType.PhysicalTime, - "NodeSocketFloatTimeAbsolute": SocketType.RealNumber, - "NodeSocketFloatUnsigned": SocketType.RealNumber, - - "NodeSocketInt": SocketType.IntegerNumber, - "NodeSocketIntFactor": SocketType.IntegerNumber, - "NodeSocketIntPercentage": SocketType.IntegerNumber, - "NodeSocketIntUnsigned": SocketType.IntegerNumber, - }, - 2: { - "NodeSocketVector": SocketType.Real3DVector, - "NodeSocketVectorAcceleration": SocketType.Real3DVector, - "NodeSocketVectorDirection": SocketType.Real3DVector, - "NodeSocketVectorEuler": SocketType.Real3DVector, - "NodeSocketVectorTranslation": SocketType.Real3DVector, - "NodeSocketVectorVelocity": SocketType.Real3DVector, - "NodeSocketVectorXYZ": SocketType.Real3DVector, - #"NodeSocketVector": SocketType.Real2DVector, - #"NodeSocketVectorAcceleration": SocketType.PhysicalAccel2D, - #"NodeSocketVectorDirection": SocketType.PhysicalDir2D, - #"NodeSocketVectorEuler": SocketType.PhysicalEuler2D, - #"NodeSocketVectorTranslation": SocketType.PhysicalDispl2D, - #"NodeSocketVectorVelocity": SocketType.PhysicalVel2D, - #"NodeSocketVectorXYZ": SocketType.Real2DPoint, - }, - 3: { - "NodeSocketRotation": SocketType.Real3DVector, - - "NodeSocketColor": SocketType.Any, - - "NodeSocketVector": SocketType.Real3DVector, - #"NodeSocketVectorAcceleration": SocketType.PhysicalAccel3D, - #"NodeSocketVectorDirection": SocketType.PhysicalDir3D, - #"NodeSocketVectorEuler": SocketType.PhysicalEuler3D, - #"NodeSocketVectorTranslation": SocketType.PhysicalDispl3D, - "NodeSocketVectorTranslation": SocketType.PhysicalPoint3D, - #"NodeSocketVectorVelocity": SocketType.PhysicalVel3D, - "NodeSocketVectorXYZ": SocketType.PhysicalPoint3D, - }, -} - -BLNodeSocket_to_SocketType_by_desc = { - 1: { - "Angle": SocketType.PhysicalAngle, - - "Length": SocketType.PhysicalLength, - "Area": SocketType.PhysicalArea, - "Volume": SocketType.PhysicalVolume, - - "Mass": SocketType.PhysicalMass, - - "Speed": SocketType.PhysicalSpeed, - "Accel": SocketType.PhysicalAccelScalar, - "Force": SocketType.PhysicalForceScalar, - - "Freq": SocketType.PhysicalFreq, - }, - 2: { - #"2DCount": SocketType.Int2DVector, - - #"2DPoint": SocketType.PhysicalPoint2D, - #"2DSize": SocketType.PhysicalSize2D, - #"2DPol": SocketType.PhysicalPol, - "2DPoint": SocketType.PhysicalPoint3D, - "2DSize": SocketType.PhysicalSize3D, - }, - 3: { - #"Count": SocketType.Int3DVector, - - "Point": SocketType.PhysicalPoint3D, - "Size": SocketType.PhysicalSize3D, - - #"Force": SocketType.PhysicalForce3D, - - "Freq": SocketType.PhysicalSize3D, - }, -} - - -#################### -# - Node Types -#################### -@append_cls_name_to_values -class NodeType(BlenderTypeEnum): - KitchenSink = enum.auto() - - # Inputs - UnitSystem = enum.auto() - - ## Inputs / Scene - Time = enum.auto() - - ## Inputs / Parameters - NumberParameter = enum.auto() - PhysicalParameter = enum.auto() - - ## Inputs / Constants - WaveConstant = enum.auto() - ScientificConstant = enum.auto() - NumberConstant = enum.auto() - PhysicalConstant = enum.auto() - BlenderConstant = enum.auto() - - ## Inputs / Lists - RealList = enum.auto() - ComplexList = enum.auto() - - ## Inputs / - InputFile = enum.auto() - - - # Outputs - ## Outputs / Viewers - Viewer3D = enum.auto() - ValueViewer = enum.auto() - ConsoleViewer = enum.auto() - - ## Outputs / Exporters - JSONFileExporter = enum.auto() - - - # Sources - ## Sources / Temporal Shapes - GaussianPulseTemporalShape = enum.auto() - ContinuousWaveTemporalShape = enum.auto() - ListTemporalShape = enum.auto() - - ## Sources / - PointDipoleSource = enum.auto() - UniformCurrentSource = enum.auto() - PlaneWaveSource = enum.auto() - ModeSource = enum.auto() - GaussianBeamSource = enum.auto() - AstigmaticGaussianBeamSource = enum.auto() - TFSFSource = enum.auto() - - EHEquivalenceSource = enum.auto() - EHSource = enum.auto() - - # Mediums - LibraryMedium = enum.auto() - - PECMedium = enum.auto() - IsotropicMedium = enum.auto() - AnisotropicMedium = enum.auto() - - TripleSellmeierMedium = enum.auto() - SellmeierMedium = enum.auto() - PoleResidueMedium = enum.auto() - DrudeMedium = enum.auto() - DrudeLorentzMedium = enum.auto() - DebyeMedium = enum.auto() - - ## Mediums / Non-Linearities - AddNonLinearity = enum.auto() - ChiThreeSusceptibilityNonLinearity = enum.auto() - TwoPhotonAbsorptionNonLinearity = enum.auto() - KerrNonLinearity = enum.auto() - - # Structures - ObjectStructure = enum.auto() - GeoNodesStructure = enum.auto() - ScriptedStructure = enum.auto() - - ## Structures / Primitives - BoxStructure = enum.auto() - SphereStructure = enum.auto() - CylinderStructure = enum.auto() - - - # Bounds - BoundBox = enum.auto() - - ## Bounds / Bound Faces - PMLBoundFace = enum.auto() - PECBoundFace = enum.auto() - PMCBoundFace = enum.auto() - - BlochBoundFace = enum.auto() - PeriodicBoundFace = enum.auto() - AbsorbingBoundFace = enum.auto() - - - # Monitors - EHFieldMonitor = enum.auto() - FieldPowerFluxMonitor = enum.auto() - EpsilonTensorMonitor = enum.auto() - DiffractionMonitor = enum.auto() - - ## Monitors / Near-Field Projections - CartesianNearFieldProjectionMonitor = enum.auto() - ObservationAngleNearFieldProjectionMonitor = enum.auto() - KSpaceNearFieldProjectionMonitor = enum.auto() - - - # Sims - SimGrid = enum.auto() - - ## Sims / Sim Grid Axis - AutomaticSimGridAxis = enum.auto() - ManualSimGridAxis = enum.auto() - UniformSimGridAxis = enum.auto() - ArraySimGridAxis = enum.auto() - - ## Sim / - FDTDSim = enum.auto() - - - # Utilities - Combine = enum.auto() - Separate = enum.auto() - Math = enum.auto() - - ## Utilities / Converters - WaveConverter = enum.auto() - - ## Utilities / Operations - ArrayOperation = enum.auto() - -#################### -# - Node Category Types -#################### -@wrap_values_in_MT -class NodeCategory(BlenderTypeEnum): - MAXWELLSIM = enum.auto() - - # Inputs/ - MAXWELLSIM_INPUTS = enum.auto() - MAXWELLSIM_INPUTS_SCENE = enum.auto() - MAXWELLSIM_INPUTS_PARAMETERS = enum.auto() - MAXWELLSIM_INPUTS_CONSTANTS = enum.auto() - MAXWELLSIM_INPUTS_LISTS = enum.auto() - - # Outputs/ - MAXWELLSIM_OUTPUTS = enum.auto() - MAXWELLSIM_OUTPUTS_VIEWERS = enum.auto() - MAXWELLSIM_OUTPUTS_EXPORTERS = enum.auto() - MAXWELLSIM_OUTPUTS_PLOTTERS = enum.auto() - - # Sources/ - MAXWELLSIM_SOURCES = enum.auto() - MAXWELLSIM_SOURCES_TEMPORALSHAPES = enum.auto() - - # Mediums/ - MAXWELLSIM_MEDIUMS = enum.auto() - MAXWELLSIM_MEDIUMS_NONLINEARITIES = enum.auto() - - # Structures/ - MAXWELLSIM_STRUCTURES = enum.auto() - MAXWELLSIM_STRUCTURES_PRIMITIVES = enum.auto() - - # Bounds/ - MAXWELLSIM_BOUNDS = enum.auto() - MAXWELLSIM_BOUNDS_BOUNDFACES = enum.auto() - - # Monitors/ - MAXWELLSIM_MONITORS = enum.auto() - MAXWELLSIM_MONITORS_NEARFIELDPROJECTIONS = enum.auto() - - # Simulations/ - MAXWELLSIM_SIMS = enum.auto() - MAXWELLSIM_SIMGRIDAXES = enum.auto() - - # Utilities/ - MAXWELLSIM_UTILITIES = enum.auto() - MAXWELLSIM_UTILITIES_CONVERTERS = enum.auto() - MAXWELLSIM_UTILITIES_OPERATIONS = enum.auto() - - @classmethod - def get_tree(cls): - ## TODO: Refactor - syllable_categories = [ - node_category.value.split("_") - for node_category in cls - if node_category.value != "MAXWELLSIM" - ] - - category_tree = {} - for syllable_category in syllable_categories: - # Set Current Subtree to Root - current_category_subtree = category_tree - - for i, syllable in enumerate(syllable_category): - # Create New Category Subtree and/or Step to Subtree - if syllable not in current_category_subtree: - current_category_subtree[syllable] = {} - current_category_subtree = current_category_subtree[syllable] - - return category_tree - -NodeCategory_to_category_label = { - # Inputs/ - NodeCategory.MAXWELLSIM_INPUTS: "Inputs", - NodeCategory.MAXWELLSIM_INPUTS_SCENE: "Scene", - NodeCategory.MAXWELLSIM_INPUTS_PARAMETERS: "Parameters", - NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS: "Constants", - NodeCategory.MAXWELLSIM_INPUTS_LISTS: "Lists", - - # Outputs/ - NodeCategory.MAXWELLSIM_OUTPUTS: "Outputs", - NodeCategory.MAXWELLSIM_OUTPUTS_VIEWERS: "Viewers", - NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS: "Exporters", - NodeCategory.MAXWELLSIM_OUTPUTS_PLOTTERS: "Plotters", - - # Sources/ - NodeCategory.MAXWELLSIM_SOURCES: "Sources", - NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES: "Temporal Shapes", - - # Mediums/ - NodeCategory.MAXWELLSIM_MEDIUMS: "Mediums", - NodeCategory.MAXWELLSIM_MEDIUMS_NONLINEARITIES: "Non-Linearities", - - # Structures/ - NodeCategory.MAXWELLSIM_STRUCTURES: "Structures", - NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES: "Primitives", - - # Bounds/ - NodeCategory.MAXWELLSIM_BOUNDS: "Bounds", - NodeCategory.MAXWELLSIM_BOUNDS_BOUNDFACES: "Bound Faces", - - # Monitors/ - NodeCategory.MAXWELLSIM_MONITORS: "Monitors", - NodeCategory.MAXWELLSIM_MONITORS_NEARFIELDPROJECTIONS: "Near-Field Projections", - - # Simulations/ - NodeCategory.MAXWELLSIM_SIMS: "Simulations", - NodeCategory.MAXWELLSIM_SIMGRIDAXES: "Sim Grid Axes", - - # Utilities/ - NodeCategory.MAXWELLSIM_UTILITIES: "Utilities", - NodeCategory.MAXWELLSIM_UTILITIES_CONVERTERS: "Converters", - NodeCategory.MAXWELLSIM_UTILITIES_OPERATIONS: "Operations", -} - - - -#################### -# - Protocols -#################### -class SocketDefProtocol(typ.Protocol): - socket_type: SocketType - label: str - - def init(self, bl_socket: bpy.types.NodeSocket) -> None: - ... - -class PresetDef(pyd.BaseModel): - label: str - description: str - values: dict[SocketName, typ.Any] - -SocketReturnType = typ.TypeVar('SocketReturnType', covariant=True) -## - Covariance: If B subtypes A, then Container[B] subtypes Container[A]. -## - This is absolutely what we want here. - -#@typ.runtime_checkable -#class BLSocketProtocol(typ.Protocol): -# socket_type: SocketType -# socket_color: BlenderColorRGB -# -# bl_label: str -# -# compatible_types: dict[typ.Type, set[typ.Callable[[typ.Any], bool]]] -# -# def draw( -# self, -# context: bpy.types.Context, -# layout: bpy.types.UILayout, -# node: bpy.types.Node, -# text: str, -# ) -> None: -# ... -# -# @property -# def default_value(self) -> typ.Any: -# ... -# @default_value.setter -# def default_value(self, value: typ.Any) -> typ.Any: -# ... -# - -@typ.runtime_checkable -class NodeTypeProtocol(typ.Protocol): - node_type: NodeType - - bl_label: str - - input_sockets: dict[SocketName, SocketDefProtocol] - output_sockets: dict[SocketName, SocketDefProtocol] - presets: dict[PresetID, PresetDef] | None - - # Built-In Blender Methods - def init(self, context: bpy.types.Context) -> None: - ... - - def draw_buttons( - self, - context: bpy.types.Context, - layout: bpy.types.UILayout, - ) -> None: - ... - - @classmethod - def poll(cls, ntree: bpy.types.NodeTree) -> None: - ... - - # Socket Getters - def g_input_bl_socket( - self, - input_socket_name: SocketName, - ) -> bpy.types.NodeSocket: - ... - - def g_output_bl_socket( - self, - output_socket_name: SocketName, - ) -> bpy.types.NodeSocket: - ... - - # Socket Methods - def s_input_value( - self, - input_socket_name: SocketName, - value: typ.Any - ) -> typ.Any: - ... - - # Data-Flow Methods - def compute_input( - self, - input_socket_name: SocketName, - ) -> typ.Any: - ... - def compute_output( - self, - output_socket_name: SocketName, - ) -> typ.Any: - ... diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py new file mode 100644 index 0000000..71d2824 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py @@ -0,0 +1,52 @@ +#################### +# - String Types +#################### +from .bl import SocketName +from .bl import PresetName +from .bl import ManagedObjName + +from .bl import BLEnumID +from .bl import BLColorRGBA + +#################### +# - Icon Types +#################### +from .icons import Icon + +#################### +# - Tree Types +#################### +from .trees import TreeType + +#################### +# - Socket Types +#################### +from .socket_types import SocketType + +from .socket_units import SOCKET_UNITS +from .socket_colors import SOCKET_COLORS +from .socket_shapes import SOCKET_SHAPES +from .socket_bl_maps import BLSocketToSocket + +#################### +# - Node Types +#################### +from .node_types import NodeType + +from .node_cats import NodeCategory +from .node_cat_labels import NODE_CAT_LABELS + +#################### +# - Managed Obj Type +#################### +from .managed_obj_type import ManagedObjType + +#################### +# - Data Flows +#################### +from .data_flows import DataFlowKind + +#################### +# - Schemas +#################### +from . import schemas diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl.py new file mode 100644 index 0000000..fd75323 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl.py @@ -0,0 +1,26 @@ +import typing as typ +import pydantic as pyd +import typing_extensions as pytypes_ext + +import bpy + +#################### +# - Pure BL Types +#################### +BLEnumID = pytypes_ext.Annotated[str, pyd.StringConstraints( + pattern=r'^[A-Z_]+$', +)] +SocketName = pytypes_ext.Annotated[str, pyd.StringConstraints( + pattern=r'^[a-zA-Z0-9_]+$', +)] +PresetName = pytypes_ext.Annotated[str, pyd.StringConstraints( + pattern=r'^[a-zA-Z0-9_]+$', +)] +BLColorRGBA = tuple[float, float, float, float] + +#################### +# - Shared-With-BL Types +#################### +ManagedObjName = pytypes_ext.Annotated[str, pyd.StringConstraints( + pattern=r'^[a-z_]+$', +)] diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flows.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flows.py new file mode 100644 index 0000000..98ad666 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flows.py @@ -0,0 +1,56 @@ +import enum + +from ....utils.blender_type_enum import BlenderTypeEnum + +class DataFlowKind(BlenderTypeEnum): + """Defines a shape/kind of data that may flow through a node tree. + + Since a node socket may define one of each, we can support several related kinds of data flow through the same node-graph infrastructure. + + Attributes: + Value: A value usable without new data. + - Basic types aka. float, int, list, string, etc. . + - Exotic (immutable-ish) types aka. numpy array, KDTree, etc. . + - A usable constructed object, ex. a `tidy3d.Box`. + - Expressions (`sp.Expr`) that don't have unknown variables. + - Lazy sequences aka. generators, with all data bound. + + LazyValue: An object which, when given new data, can make many values. + - An `sp.Expr`, which might need `simplify`ing, `jax` JIT'ing, unit cancellations, variable substitutions, etc. before use. + - Lazy objects, for which all parameters aren't yet known. + - A computational graph aka. `aesara`, which may even need to be handled before + + Capabilities: A `ValueCapability` object providing compatibility. + + # Value Data Flow + Simply passing values is the simplest and easiest use case. + + This doesn't mean it's "dumb" - ex. a `sp.Expr` might, before use, have `simplify`, rewriting, unit cancellation, etc. run. + All of this is okay, as long as there is no *introduction of new data* ex. variable substitutions. + + + # Lazy Value Data Flow + By passing (essentially) functions, one supports: + - **Lightness**: While lazy values can be made expensive to construct, they will generally not be nearly as heavy to handle when trying to work with ex. operations on voxel arrays. + - **Performance**: Parameterizing ex. `sp.Expr` with variables allows one to build very optimized functions, which can make ex. node graph updates very fast if the only operation run is the `jax` JIT'ed function (aka. GPU accelerated) generated from the final full expression. + - **Numerical Stability**: Libraries like `aesara` build a computational graph, which can be automatically rewritten to avoid many obvious conditioning / cancellation errors. + - **Lazy Output**: The goal of a node-graph may not be the definition of a single value, but rather, a parameterized expression for generating *many values* with known properties. This is especially interesting for use cases where one wishes to build an optimization step using nodes. + + + # Capability Passing + By being able to pass "capabilities" next to other kinds of values, nodes can quickly determine whether a given link is valid without having to actually compute it. + + + # Lazy Parameter Value + When using parameterized LazyValues, one may wish to independently pass parameter values through the graph, so they can be inserted into the final (cached) high-performance expression without. + + The advantage of using a different data flow would be changing this kind of value would ONLY invalidate lazy parameter value caches, which would allow an incredibly fast path of getting the value into the lazy expression for high-performance computation. + + Implementation TBD - though, ostensibly, one would have a "parameter" node which both would only provide a LazyValue (aka. a symbolic variable), but would also be able to provide a LazyParamValue, which would be a particular value of some kind (probably via the `value` of some other node socket). + """ + + Value = enum.auto() + LazyValue = enum.auto() + Capabilities = enum.auto() + + LazyParamValue = enum.auto() diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/icons.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/icons.py new file mode 100644 index 0000000..04fdb16 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/icons.py @@ -0,0 +1,4 @@ +from ....utils.blender_type_enum import BlenderTypeEnum + +class Icon(BlenderTypeEnum): + SimNodeEditor = "MOD_SIMPLEDEFORM" diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/managed_obj_type.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/managed_obj_type.py new file mode 100644 index 0000000..9dad6f4 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/managed_obj_type.py @@ -0,0 +1,9 @@ +import enum + +from ....utils.blender_type_enum import ( + BlenderTypeEnum +) + +class ManagedObjType(BlenderTypeEnum): + ManagedBLObject = enum.auto() + ManagedBLImage = enum.auto() diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_cat_labels.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_cat_labels.py new file mode 100644 index 0000000..6f34484 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_cat_labels.py @@ -0,0 +1,46 @@ +from .node_cats import NodeCategory as NC + +NODE_CAT_LABELS = { + # Inputs/ + NC.MAXWELLSIM_INPUTS: "Inputs", + NC.MAXWELLSIM_INPUTS_IMPORTERS: "Importers", + NC.MAXWELLSIM_INPUTS_SCENE: "Scene", + NC.MAXWELLSIM_INPUTS_PARAMETERS: "Parameters", + NC.MAXWELLSIM_INPUTS_CONSTANTS: "Constants", + NC.MAXWELLSIM_INPUTS_LISTS: "Lists", + + # Outputs/ + NC.MAXWELLSIM_OUTPUTS: "Outputs", + NC.MAXWELLSIM_OUTPUTS_VIEWERS: "Viewers", + NC.MAXWELLSIM_OUTPUTS_EXPORTERS: "Exporters", + NC.MAXWELLSIM_OUTPUTS_PLOTTERS: "Plotters", + + # Sources/ + NC.MAXWELLSIM_SOURCES: "Sources", + NC.MAXWELLSIM_SOURCES_TEMPORALSHAPES: "Temporal Shapes", + + # Mediums/ + NC.MAXWELLSIM_MEDIUMS: "Mediums", + NC.MAXWELLSIM_MEDIUMS_NONLINEARITIES: "Non-Linearities", + + # Structures/ + NC.MAXWELLSIM_STRUCTURES: "Structures", + NC.MAXWELLSIM_STRUCTURES_PRIMITIVES: "Primitives", + + # Bounds/ + NC.MAXWELLSIM_BOUNDS: "Bounds", + NC.MAXWELLSIM_BOUNDS_BOUNDFACES: "Bound Faces", + + # Monitors/ + NC.MAXWELLSIM_MONITORS: "Monitors", + NC.MAXWELLSIM_MONITORS_NEARFIELDPROJECTIONS: "Near-Field Projections", + + # Simulations/ + NC.MAXWELLSIM_SIMS: "Simulations", + NC.MAXWELLSIM_SIMGRIDAXES: "Sim Grid Axes", + + # Utilities/ + NC.MAXWELLSIM_UTILITIES: "Utilities", + NC.MAXWELLSIM_UTILITIES_CONVERTERS: "Converters", + NC.MAXWELLSIM_UTILITIES_OPERATIONS: "Operations", +} diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_cats.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_cats.py new file mode 100644 index 0000000..4bff4d1 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_cats.py @@ -0,0 +1,74 @@ +import enum + +from ....utils.blender_type_enum import ( + BlenderTypeEnum, wrap_values_in_MT +) + +@wrap_values_in_MT +class NodeCategory(BlenderTypeEnum): + MAXWELLSIM = enum.auto() + + # Inputs/ + MAXWELLSIM_INPUTS = enum.auto() + MAXWELLSIM_INPUTS_IMPORTERS = enum.auto() + MAXWELLSIM_INPUTS_SCENE = enum.auto() + MAXWELLSIM_INPUTS_PARAMETERS = enum.auto() + MAXWELLSIM_INPUTS_CONSTANTS = enum.auto() + MAXWELLSIM_INPUTS_LISTS = enum.auto() + + # Outputs/ + MAXWELLSIM_OUTPUTS = enum.auto() + MAXWELLSIM_OUTPUTS_VIEWERS = enum.auto() + MAXWELLSIM_OUTPUTS_EXPORTERS = enum.auto() + MAXWELLSIM_OUTPUTS_PLOTTERS = enum.auto() + + # Sources/ + MAXWELLSIM_SOURCES = enum.auto() + MAXWELLSIM_SOURCES_TEMPORALSHAPES = enum.auto() + + # Mediums/ + MAXWELLSIM_MEDIUMS = enum.auto() + MAXWELLSIM_MEDIUMS_NONLINEARITIES = enum.auto() + + # Structures/ + MAXWELLSIM_STRUCTURES = enum.auto() + MAXWELLSIM_STRUCTURES_PRIMITIVES = enum.auto() + + # Bounds/ + MAXWELLSIM_BOUNDS = enum.auto() + MAXWELLSIM_BOUNDS_BOUNDFACES = enum.auto() + + # Monitors/ + MAXWELLSIM_MONITORS = enum.auto() + MAXWELLSIM_MONITORS_NEARFIELDPROJECTIONS = enum.auto() + + # Simulations/ + MAXWELLSIM_SIMS = enum.auto() + MAXWELLSIM_SIMGRIDAXES = enum.auto() + + # Utilities/ + MAXWELLSIM_UTILITIES = enum.auto() + MAXWELLSIM_UTILITIES_CONVERTERS = enum.auto() + MAXWELLSIM_UTILITIES_OPERATIONS = enum.auto() + + @classmethod + def get_tree(cls): + ## TODO: Refactor + syllable_categories = [ + str(node_category.value).split("_") + for node_category in cls + if node_category.value != "MAXWELLSIM" + ] + + category_tree = {} + for syllable_category in syllable_categories: + # Set Current Subtree to Root + current_category_subtree = category_tree + + for i, syllable in enumerate(syllable_category): + # Create New Category Subtree and/or Step to Subtree + if syllable not in current_category_subtree: + current_category_subtree[syllable] = {} + current_category_subtree = current_category_subtree[syllable] + + return category_tree diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py new file mode 100644 index 0000000..5549843 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py @@ -0,0 +1,147 @@ +import enum + +from ....utils.blender_type_enum import ( + BlenderTypeEnum, append_cls_name_to_values +) + +@append_cls_name_to_values +class NodeType(BlenderTypeEnum): + KitchenSink = enum.auto() + + # Inputs + UnitSystem = enum.auto() + + ## Inputs / Scene + Time = enum.auto() + + ## Inputs / Importers + Tidy3DWebImporter = enum.auto() + + ## Inputs / Parameters + NumberParameter = enum.auto() + PhysicalParameter = enum.auto() + + ## Inputs / Constants + WaveConstant = enum.auto() + ScientificConstant = enum.auto() + NumberConstant = enum.auto() + PhysicalConstant = enum.auto() + BlenderConstant = enum.auto() + + ## Inputs / Lists + RealList = enum.auto() + ComplexList = enum.auto() + + ## Inputs / + InputFile = enum.auto() + + + # Outputs + ## Outputs / Viewers + Viewer = enum.auto() + ValueViewer = enum.auto() + ConsoleViewer = enum.auto() + + ## Outputs / Exporters + JSONFileExporter = enum.auto() + Tidy3DWebExporter = enum.auto() + + + # Sources + ## Sources / Temporal Shapes + GaussianPulseTemporalShape = enum.auto() + ContinuousWaveTemporalShape = enum.auto() + ListTemporalShape = enum.auto() + + ## Sources / + PointDipoleSource = enum.auto() + UniformCurrentSource = enum.auto() + PlaneWaveSource = enum.auto() + ModeSource = enum.auto() + GaussianBeamSource = enum.auto() + AstigmaticGaussianBeamSource = enum.auto() + TFSFSource = enum.auto() + + EHEquivalenceSource = enum.auto() + EHSource = enum.auto() + + # Mediums + LibraryMedium = enum.auto() + + PECMedium = enum.auto() + IsotropicMedium = enum.auto() + AnisotropicMedium = enum.auto() + + TripleSellmeierMedium = enum.auto() + SellmeierMedium = enum.auto() + PoleResidueMedium = enum.auto() + DrudeMedium = enum.auto() + DrudeLorentzMedium = enum.auto() + DebyeMedium = enum.auto() + + ## Mediums / Non-Linearities + AddNonLinearity = enum.auto() + ChiThreeSusceptibilityNonLinearity = enum.auto() + TwoPhotonAbsorptionNonLinearity = enum.auto() + KerrNonLinearity = enum.auto() + + # Structures + ObjectStructure = enum.auto() + GeoNodesStructure = enum.auto() + ScriptedStructure = enum.auto() + + ## Structures / Primitives + BoxStructure = enum.auto() + SphereStructure = enum.auto() + CylinderStructure = enum.auto() + + + # Bounds + BoundBox = enum.auto() + + ## Bounds / Bound Faces + PMLBoundFace = enum.auto() + PECBoundFace = enum.auto() + PMCBoundFace = enum.auto() + + BlochBoundFace = enum.auto() + PeriodicBoundFace = enum.auto() + AbsorbingBoundFace = enum.auto() + + + # Monitors + EHFieldMonitor = enum.auto() + FieldPowerFluxMonitor = enum.auto() + EpsilonTensorMonitor = enum.auto() + DiffractionMonitor = enum.auto() + + ## Monitors / Near-Field Projections + CartesianNearFieldProjectionMonitor = enum.auto() + ObservationAngleNearFieldProjectionMonitor = enum.auto() + KSpaceNearFieldProjectionMonitor = enum.auto() + + + # Sims + SimDomain = enum.auto() + SimGrid = enum.auto() + + ## Sims / Sim Grid Axis + AutomaticSimGridAxis = enum.auto() + ManualSimGridAxis = enum.auto() + UniformSimGridAxis = enum.auto() + ArraySimGridAxis = enum.auto() + + ## Sim / + FDTDSim = enum.auto() + + + # Utilities + Combine = enum.auto() + Separate = enum.auto() + Math = enum.auto() + + ## Utilities / Converters + WaveConverter = enum.auto() + + ## Utilities / Operations + ArrayOperation = enum.auto() diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/__init__.py new file mode 100644 index 0000000..62c5f4f --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/__init__.py @@ -0,0 +1,4 @@ +from .preset_def import PresetDef +from .socket_def import SocketDef +from .managed_obj import ManagedObj +from .managed_obj_def import ManagedObjDef diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj.py new file mode 100644 index 0000000..dd036a0 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj.py @@ -0,0 +1,31 @@ +import typing as typ +import typing as typx + +import pydantic as pyd + +from ..bl import ManagedObjName, SocketName +from ..managed_obj_type import ManagedObjType + +class ManagedObj(typ.Protocol): + managed_obj_type: ManagedObjType + + def __init__( + self, + name: ManagedObjName, + ): + ... + + @property + def name(self) -> str: ... + @name.setter + def name(self, value: str): ... + + def free(self): + ... + + def bl_select(self): + """If this is a managed Blender object, and the operation "select this in Blender" makes sense, then do so. + + Else, do nothing. + """ + pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj_def.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj_def.py new file mode 100644 index 0000000..1854c58 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj_def.py @@ -0,0 +1,11 @@ +import typing as typ +from dataclasses import dataclass + +import pydantic as pyd + +from ..bl import PresetName, SocketName, BLEnumID +from .managed_obj import ManagedObj + +class ManagedObjDef(pyd.BaseModel): + mk: typ.Callable[[str], ManagedObj] + name_prefix: str = "" diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/node.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/node.py new file mode 100644 index 0000000..44c22d3 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/node.py @@ -0,0 +1,9 @@ +import typing as typ + +import pydantic as pyd + +from ..bl import ManagedObjName, SocketName +from ..managed_obj_type import ManagedObjType + +class MaxwellSimNode(typ.Protocol): + diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/preset_def.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/preset_def.py new file mode 100644 index 0000000..695d5e8 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/preset_def.py @@ -0,0 +1,10 @@ +import typing as typ + +import pydantic as pyd + +from ..bl import PresetName, SocketName, BLEnumID + +class PresetDef(pyd.BaseModel): + label: PresetName + description: str + values: dict[SocketName, typ.Any] diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/socket_def.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/socket_def.py new file mode 100644 index 0000000..3010eb8 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/socket_def.py @@ -0,0 +1,12 @@ +import typing as typ + +import bpy + +from ..socket_types import SocketType + +@typ.runtime_checkable +class SocketDef(typ.Protocol): + socket_type: SocketType + + def init(self, bl_socket: bpy.types.NodeSocket) -> None: + ... diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_bl_maps.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_bl_maps.py new file mode 100644 index 0000000..7e69cc9 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_bl_maps.py @@ -0,0 +1,119 @@ +import sympy.physics.units as spu +from ....utils import extra_sympy_units as spuex + +from .socket_types import SocketType as ST + +Dimensions = int ## Num. Elements in the Socket +BLSocketType = str ## A Blender-Defined Socket Type + +class BLSocketToSocket: + """Encodes ways of converting blender sockets of known dimensionality + to a corresponding SocketType. + + "Dimensionality" is simply how many elements the blender socket has. + The user must explicitly specify this, as blender allows a variable + number of elements for some sockets, and we do not. + """ + #################### + # - Direct BLSocketType -> SocketType + #################### + by_bl_socket_type: dict[Dimensions, dict[BLSocketType, ST]] = { + 1: { + "NodeSocketStandard": ST.Any, + "NodeSocketVirtual": ST.Any, + "NodeSocketGeometry": ST.Any, + "NodeSocketTexture": ST.Any, + "NodeSocketShader": ST.Any, + "NodeSocketMaterial": ST.Any, + + "NodeSocketString": ST.Text, + "NodeSocketBool": ST.Bool, + "NodeSocketCollection": ST.BlenderCollection, + "NodeSocketImage": ST.BlenderImage, + "NodeSocketObject": ST.BlenderObject, + + "NodeSocketFloat": ST.RealNumber, + "NodeSocketFloatAngle": ST.PhysicalAngle, + "NodeSocketFloatDistance": ST.PhysicalLength, + "NodeSocketFloatFactor": ST.RealNumber, + "NodeSocketFloatPercentage": ST.RealNumber, + "NodeSocketFloatTime": ST.PhysicalTime, + "NodeSocketFloatTimeAbsolute": ST.RealNumber, + "NodeSocketFloatUnsigned": ST.RealNumber, + + "NodeSocketInt": ST.IntegerNumber, + "NodeSocketIntFactor": ST.IntegerNumber, + "NodeSocketIntPercentage": ST.IntegerNumber, + "NodeSocketIntUnsigned": ST.IntegerNumber, + }, + 2: { + "NodeSocketVector": ST.Real3DVector, + "NodeSocketVectorAcceleration": ST.Real3DVector, + "NodeSocketVectorDirection": ST.Real3DVector, + "NodeSocketVectorEuler": ST.Real3DVector, + "NodeSocketVectorTranslation": ST.Real3DVector, + "NodeSocketVectorVelocity": ST.Real3DVector, + "NodeSocketVectorXYZ": ST.Real3DVector, + #"NodeSocketVector": ST.Real2DVector, + #"NodeSocketVectorAcceleration": ST.PhysicalAccel2D, + #"NodeSocketVectorDirection": ST.PhysicalDir2D, + #"NodeSocketVectorEuler": ST.PhysicalEuler2D, + #"NodeSocketVectorTranslation": ST.PhysicalDispl2D, + #"NodeSocketVectorVelocity": ST.PhysicalVel2D, + #"NodeSocketVectorXYZ": ST.Real2DPoint, + }, + 3: { + "NodeSocketRotation": ST.Real3DVector, + + "NodeSocketColor": ST.Any, + + "NodeSocketVector": ST.Real3DVector, + #"NodeSocketVectorAcceleration": ST.PhysicalAccel3D, + #"NodeSocketVectorDirection": ST.PhysicalDir3D, + #"NodeSocketVectorEuler": ST.PhysicalEuler3D, + #"NodeSocketVectorTranslation": ST.PhysicalDispl3D, + "NodeSocketVectorTranslation": ST.PhysicalPoint3D, + #"NodeSocketVectorVelocity": ST.PhysicalVel3D, + "NodeSocketVectorXYZ": ST.PhysicalPoint3D, + }, + } + + #################### + # - BLSocket Description-Driven SocketType Choice + #################### + by_description = { + 1: { + "Angle": ST.PhysicalAngle, + + "Length": ST.PhysicalLength, + "Area": ST.PhysicalArea, + "Volume": ST.PhysicalVolume, + + "Mass": ST.PhysicalMass, + + "Speed": ST.PhysicalSpeed, + "Accel": ST.PhysicalAccelScalar, + "Force": ST.PhysicalForceScalar, + + "Freq": ST.PhysicalFreq, + }, + 2: { + #"2DCount": ST.Int2DVector, + + #"2DPoint": ST.PhysicalPoint2D, + #"2DSize": ST.PhysicalSize2D, + #"2DPol": ST.PhysicalPol, + "2DPoint": ST.PhysicalPoint3D, + "2DSize": ST.PhysicalSize3D, + }, + 3: { + #"Count": ST.Int3DVector, + + "Point": ST.PhysicalPoint3D, + "Size": ST.PhysicalSize3D, + + #"Force": ST.PhysicalForce3D, + + "Freq": ST.PhysicalSize3D, + }, + } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_colors.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_colors.py new file mode 100644 index 0000000..3cdaed8 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_colors.py @@ -0,0 +1,73 @@ +import sympy.physics.units as spu +from ....utils import extra_sympy_units as spuex + +from .socket_types import SocketType as ST + +## TODO: Don't just presume sRGB. +SOCKET_COLORS = { + # Basic + ST.Any: (0.8, 0.8, 0.8, 1.0), # Light Grey + ST.Bool: (0.7, 0.7, 0.7, 1.0), # Medium Light Grey + ST.Text: (0.7, 0.7, 0.7, 1.0), # Medium Light Grey + ST.FilePath: (0.6, 0.6, 0.6, 1.0), # Medium Grey + ST.Secret: (0.0, 0.0, 0.0, 1.0), # Black + + # Number + ST.IntegerNumber: (0.5, 0.5, 1.0, 1.0), # Light Blue + ST.RationalNumber: (0.4, 0.4, 0.9, 1.0), # Medium Light Blue + ST.RealNumber: (0.3, 0.3, 0.8, 1.0), # Medium Blue + ST.ComplexNumber: (0.2, 0.2, 0.7, 1.0), # Dark Blue + + # Vector + ST.Real2DVector: (0.5, 1.0, 0.5, 1.0), # Light Green + ST.Complex2DVector: (0.4, 0.9, 0.4, 1.0), # Medium Light Green + ST.Real3DVector: (0.3, 0.8, 0.3, 1.0), # Medium Green + ST.Complex3DVector: (0.2, 0.7, 0.2, 1.0), # Dark Green + + # Physical + ST.PhysicalUnitSystem: (1.0, 0.5, 0.5, 1.0), # Light Red + ST.PhysicalTime: (1.0, 0.5, 0.5, 1.0), # Light Red + ST.PhysicalAngle: (0.9, 0.45, 0.45, 1.0), # Medium Light Red + ST.PhysicalLength: (0.8, 0.4, 0.4, 1.0), # Medium Red + ST.PhysicalArea: (0.7, 0.35, 0.35, 1.0), # Medium Dark Red + ST.PhysicalVolume: (0.6, 0.3, 0.3, 1.0), # Dark Red + ST.PhysicalPoint2D: (0.7, 0.35, 0.35, 1.0), # Medium Dark Red + ST.PhysicalPoint3D: (0.6, 0.3, 0.3, 1.0), # Dark Red + ST.PhysicalSize2D: (0.7, 0.35, 0.35, 1.0), # Medium Dark Red + ST.PhysicalSize3D: (0.6, 0.3, 0.3, 1.0), # Dark Red + ST.PhysicalMass: (0.9, 0.6, 0.4, 1.0), # Light Orange + ST.PhysicalSpeed: (0.8, 0.55, 0.35, 1.0), # Medium Light Orange + ST.PhysicalAccelScalar: (0.7, 0.5, 0.3, 1.0), # Medium Orange + ST.PhysicalForceScalar: (0.6, 0.45, 0.25, 1.0), # Medium Dark Orange + ST.PhysicalAccel3DVector: (0.7, 0.5, 0.3, 1.0), # Medium Orange + ST.PhysicalForce3DVector: (0.6, 0.45, 0.25, 1.0), # Medium Dark Orange + ST.PhysicalPol: (0.5, 0.4, 0.2, 1.0), # Dark Orange + ST.PhysicalFreq: (1.0, 0.7, 0.5, 1.0), # Light Peach + + # Blender + ST.BlenderObject: (0.7, 0.5, 1.0, 1.0), # Light Purple + ST.BlenderCollection: (0.6, 0.45, 0.9, 1.0), # Medium Light Purple + ST.BlenderImage: (0.5, 0.4, 0.8, 1.0), # Medium Purple + ST.BlenderVolume: (0.4, 0.35, 0.7, 1.0), # Medium Dark Purple + ST.BlenderGeoNodes: (0.3, 0.3, 0.6, 1.0), # Dark Purple + ST.BlenderText: (0.5, 0.5, 0.75, 1.0), # Light Lavender + ST.BlenderPreviewTarget: (0.5, 0.5, 0.75, 1.0), # Light Lavender + + # Maxwell + ST.MaxwellSource: (1.0, 1.0, 0.5, 1.0), # Light Yellow + ST.MaxwellTemporalShape: (0.9, 0.9, 0.45, 1.0), # Medium Light Yellow + ST.MaxwellMedium: (0.8, 0.8, 0.4, 1.0), # Medium Yellow + ST.MaxwellMediumNonLinearity: (0.7, 0.7, 0.35, 1.0), # Medium Dark Yellow + ST.MaxwellStructure: (0.6, 0.6, 0.3, 1.0), # Dark Yellow + ST.MaxwellBoundBox: (0.9, 0.8, 0.5, 1.0), # Light Gold + ST.MaxwellBoundFace: (0.8, 0.7, 0.45, 1.0), # Medium Light Gold + ST.MaxwellMonitor: (0.7, 0.6, 0.4, 1.0), # Medium Gold + ST.MaxwellFDTDSim: (0.6, 0.5, 0.35, 1.0), # Medium Dark Gold + ST.MaxwellSimGrid: (0.5, 0.4, 0.3, 1.0), # Dark Gold + ST.MaxwellSimGridAxis: (0.4, 0.3, 0.25, 1.0), # Darkest Gold + ST.MaxwellSimDomain: (0.4, 0.3, 0.25, 1.0), # Darkest Gold + + # Tidy3D + ST.Tidy3DCloudTask: (0.4, 0.3, 0.25, 1.0), # Darkest Gold +} + diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_shapes.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_shapes.py new file mode 100644 index 0000000..b3fbdfc --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_shapes.py @@ -0,0 +1,68 @@ +from .socket_types import SocketType as ST + +SOCKET_SHAPES = { + # Basic + ST.Any: "CIRCLE", + ST.Bool: "CIRCLE", + ST.Text: "SQUARE", + ST.FilePath: "SQUARE", + ST.Secret: "SQUARE", + + # Number + ST.IntegerNumber: "CIRCLE", + ST.RationalNumber: "CIRCLE", + ST.RealNumber: "CIRCLE", + ST.ComplexNumber: "CIRCLE_DOT", + + # Vector + ST.Real2DVector: "SQUARE_DOT", + ST.Complex2DVector: "DIAMOND_DOT", + ST.Real3DVector: "SQUARE_DOT", + ST.Complex3DVector: "DIAMOND_DOT", + + # Physical + ST.PhysicalUnitSystem: "CIRCLE", + ST.PhysicalTime: "CIRCLE", + ST.PhysicalAngle: "DIAMOND", + ST.PhysicalLength: "SQUARE", + ST.PhysicalArea: "SQUARE", + ST.PhysicalVolume: "SQUARE", + ST.PhysicalPoint2D: "DIAMOND", + ST.PhysicalPoint3D: "DIAMOND", + ST.PhysicalSize2D: "SQUARE", + ST.PhysicalSize3D: "SQUARE", + ST.PhysicalMass: "CIRCLE", + ST.PhysicalSpeed: "CIRCLE", + ST.PhysicalAccelScalar: "CIRCLE", + ST.PhysicalForceScalar: "CIRCLE", + ST.PhysicalAccel3DVector: "SQUARE_DOT", + ST.PhysicalForce3DVector: "SQUARE_DOT", + ST.PhysicalPol: "DIAMOND", + ST.PhysicalFreq: "CIRCLE", + + # Blender + ST.BlenderObject: "SQUARE", + ST.BlenderCollection: "SQUARE", + ST.BlenderImage: "DIAMOND", + ST.BlenderVolume: "DIAMOND", + ST.BlenderGeoNodes: "DIAMOND", + ST.BlenderText: "SQUARE", + ST.BlenderPreviewTarget: "SQUARE", + + # Maxwell + ST.MaxwellSource: "CIRCLE", + ST.MaxwellTemporalShape: "CIRCLE", + ST.MaxwellMedium: "CIRCLE", + ST.MaxwellMediumNonLinearity: "CIRCLE", + ST.MaxwellStructure: "SQUARE", + ST.MaxwellBoundBox: "SQUARE", + ST.MaxwellBoundFace: "DIAMOND", + ST.MaxwellMonitor: "CIRCLE", + ST.MaxwellFDTDSim: "SQUARE", + ST.MaxwellSimGrid: "SQUARE", + ST.MaxwellSimGridAxis: "DIAMOND", + ST.MaxwellSimDomain: "SQUARE", + + # Tidy3D + ST.Tidy3DCloudTask: "CIRCLE", +} diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_types.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_types.py new file mode 100644 index 0000000..a88c3f2 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_types.py @@ -0,0 +1,89 @@ +import enum + +from ....utils.blender_type_enum import ( + BlenderTypeEnum, append_cls_name_to_values, wrap_values_in_MT +) + +@append_cls_name_to_values +class SocketType(BlenderTypeEnum): + # Base + Any = enum.auto() + Bool = enum.auto() + Text = enum.auto() + FilePath = enum.auto() + Secret = enum.auto() + + # Number + IntegerNumber = enum.auto() + RationalNumber = enum.auto() + RealNumber = enum.auto() + ComplexNumber = enum.auto() + + # Vector + Real2DVector = enum.auto() + Complex2DVector = enum.auto() + + Real3DVector = enum.auto() + Complex3DVector = enum.auto() + + # Physical + PhysicalUnitSystem = enum.auto() + PhysicalTime = enum.auto() + + PhysicalAngle = enum.auto() + + PhysicalLength = enum.auto() + PhysicalArea = enum.auto() + PhysicalVolume = enum.auto() + + PhysicalPoint2D = enum.auto() + PhysicalPoint3D = enum.auto() + + PhysicalSize2D = enum.auto() + PhysicalSize3D = enum.auto() + + PhysicalMass = enum.auto() + + PhysicalSpeed = enum.auto() + PhysicalAccelScalar = enum.auto() + PhysicalForceScalar = enum.auto() + PhysicalAccel3DVector = enum.auto() + PhysicalForce3DVector = enum.auto() + + PhysicalPol = enum.auto() + + PhysicalFreq = enum.auto() + + # Blender + BlenderObject = enum.auto() + BlenderCollection = enum.auto() + + BlenderImage = enum.auto() + BlenderVolume = enum.auto() + + BlenderGeoNodes = enum.auto() + BlenderText = enum.auto() + + BlenderPreviewTarget = enum.auto() + + # Maxwell + MaxwellSource = enum.auto() + MaxwellTemporalShape = enum.auto() + + MaxwellMedium = enum.auto() + MaxwellMediumNonLinearity = enum.auto() + + MaxwellStructure = enum.auto() + + MaxwellBoundBox = enum.auto() + MaxwellBoundFace = enum.auto() + + MaxwellMonitor = enum.auto() + + MaxwellFDTDSim = enum.auto() + MaxwellSimGrid = enum.auto() + MaxwellSimGridAxis = enum.auto() + MaxwellSimDomain = enum.auto() + + # Tidy3D + Tidy3DCloudTask = enum.auto() diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_units.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_units.py new file mode 100644 index 0000000..8236d66 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/socket_units.py @@ -0,0 +1,265 @@ +import sympy.physics.units as spu +from ....utils import extra_sympy_units as spuex + +from .socket_types import SocketType as ST + +SOCKET_UNITS = { + ST.PhysicalTime: { + "default": "PS", + "values": { + "PS": spu.picosecond, + "NS": spu.nanosecond, + "MS": spu.microsecond, + "MLSEC": spu.millisecond, + "SEC": spu.second, + "MIN": spu.minute, + "HOUR": spu.hour, + "DAY": spu.day, + }, + }, + + ST.PhysicalAngle: { + "default": "RADIAN", + "values": { + "RADIAN": spu.radian, + "DEGREE": spu.degree, + "STERAD": spu.steradian, + "ANGMIL": spu.angular_mil, + }, + }, + + ST.PhysicalLength: { + "default": "UM", + "values": { + "PM": spu.picometer, + "A": spu.angstrom, + "NM": spu.nanometer, + "UM": spu.micrometer, + "MM": spu.millimeter, + "CM": spu.centimeter, + "M": spu.meter, + "INCH": spu.inch, + "FOOT": spu.foot, + "YARD": spu.yard, + "MILE": spu.mile, + }, + }, + ST.PhysicalArea: { + "default": "UM_SQ", + "values": { + "PM_SQ": spu.picometer**2, + "A_SQ": spu.angstrom**2, + "NM_SQ": spu.nanometer**2, + "UM_SQ": spu.micrometer**2, + "MM_SQ": spu.millimeter**2, + "CM_SQ": spu.centimeter**2, + "M_SQ": spu.meter**2, + "INCH_SQ": spu.inch**2, + "FOOT_SQ": spu.foot**2, + "YARD_SQ": spu.yard**2, + "MILE_SQ": spu.mile**2, + }, + }, + ST.PhysicalVolume: { + "default": "UM_CB", + "values": { + "PM_CB": spu.picometer**3, + "A_CB": spu.angstrom**3, + "NM_CB": spu.nanometer**3, + "UM_CB": spu.micrometer**3, + "MM_CB": spu.millimeter**3, + "CM_CB": spu.centimeter**3, + "M_CB": spu.meter**3, + "ML": spu.milliliter, + "L": spu.liter, + "INCH_CB": spu.inch**3, + "FOOT_CB": spu.foot**3, + "YARD_CB": spu.yard**3, + "MILE_CB": spu.mile**3, + }, + }, + + ST.PhysicalPoint2D: { + "default": "UM", + "values": { + "PM": spu.picometer, + "A": spu.angstrom, + "NM": spu.nanometer, + "UM": spu.micrometer, + "MM": spu.millimeter, + "CM": spu.centimeter, + "M": spu.meter, + "INCH": spu.inch, + "FOOT": spu.foot, + "YARD": spu.yard, + "MILE": spu.mile, + }, + }, + ST.PhysicalPoint3D: { + "default": "UM", + "values": { + "PM": spu.picometer, + "A": spu.angstrom, + "NM": spu.nanometer, + "UM": spu.micrometer, + "MM": spu.millimeter, + "CM": spu.centimeter, + "M": spu.meter, + "INCH": spu.inch, + "FOOT": spu.foot, + "YARD": spu.yard, + "MILE": spu.mile, + }, + }, + + ST.PhysicalSize2D: { + "default": "UM", + "values": { + "PM": spu.picometer, + "A": spu.angstrom, + "NM": spu.nanometer, + "UM": spu.micrometer, + "MM": spu.millimeter, + "CM": spu.centimeter, + "M": spu.meter, + "INCH": spu.inch, + "FOOT": spu.foot, + "YARD": spu.yard, + "MILE": spu.mile, + }, + }, + ST.PhysicalSize3D: { + "default": "UM", + "values": { + "PM": spu.picometer, + "A": spu.angstrom, + "NM": spu.nanometer, + "UM": spu.micrometer, + "MM": spu.millimeter, + "CM": spu.centimeter, + "M": spu.meter, + "INCH": spu.inch, + "FOOT": spu.foot, + "YARD": spu.yard, + "MILE": spu.mile, + }, + }, + + ST.PhysicalMass: { + "default": "UG", + "values": { + "E_REST": spu.electron_rest_mass, + "DAL": spu.dalton, + "UG": spu.microgram, + "MG": spu.milligram, + "G": spu.gram, + "KG": spu.kilogram, + "TON": spu.metric_ton, + }, + }, + + ST.PhysicalSpeed: { + "default": "UM_S", + "values": { + "PM_S": spu.picometer / spu.second, + "NM_S": spu.nanometer / spu.second, + "UM_S": spu.micrometer / spu.second, + "MM_S": spu.millimeter / spu.second, + "M_S": spu.meter / spu.second, + "KM_S": spu.kilometer / spu.second, + "KM_H": spu.kilometer / spu.hour, + "FT_S": spu.feet / spu.second, + "MI_H": spu.mile / spu.hour, + }, + }, + ST.PhysicalAccelScalar: { + "default": "UM_S_SQ", + "values": { + "PM_S_SQ": spu.picometer / spu.second**2, + "NM_S_SQ": spu.nanometer / spu.second**2, + "UM_S_SQ": spu.micrometer / spu.second**2, + "MM_S_SQ": spu.millimeter / spu.second**2, + "M_S_SQ": spu.meter / spu.second**2, + "KM_S_SQ": spu.kilometer / spu.second**2, + "FT_S_SQ": spu.feet / spu.second**2, + }, + }, + ST.PhysicalForceScalar: { + "default": "UNEWT", + "values": { + "KG_M_S_SQ": spu.kg * spu.m/spu.second**2, + "NNEWT": spuex.nanonewton, + "UNEWT": spuex.micronewton, + "MNEWT": spuex.millinewton, + "NEWT": spu.newton, + }, + }, + ST.PhysicalAccel3DVector: { + "default": "UM_S_SQ", + "values": { + "PM_S_SQ": spu.picometer / spu.second**2, + "NM_S_SQ": spu.nanometer / spu.second**2, + "UM_S_SQ": spu.micrometer / spu.second**2, + "MM_S_SQ": spu.millimeter / spu.second**2, + "M_S_SQ": spu.meter / spu.second**2, + "KM_S_SQ": spu.kilometer / spu.second**2, + "FT_S_SQ": spu.feet / spu.second**2, + }, + }, + ST.PhysicalForce3DVector: { + "default": "UNEWT", + "values": { + "KG_M_S_SQ": spu.kg * spu.m/spu.second**2, + "NNEWT": spuex.nanonewton, + "UNEWT": spuex.micronewton, + "MNEWT": spuex.millinewton, + "NEWT": spu.newton, + }, + }, + + ST.PhysicalFreq: { + "default": "THZ", + "values": { + "HZ": spu.hertz, + "KHZ": spuex.kilohertz, + "MHZ": spuex.megahertz, + "GHZ": spuex.gigahertz, + "THZ": spuex.terahertz, + "PHZ": spuex.petahertz, + "EHZ": spuex.exahertz, + }, + }, + ST.PhysicalPol: { + "default": "RADIAN", + "values": { + "RADIAN": spu.radian, + "DEGREE": spu.degree, + "STERAD": spu.steradian, + "ANGMIL": spu.angular_mil, + }, + }, + ST.MaxwellMedium: { + "default": "NM", + "values": { + "PM": spu.picometer, ## c(vac) = wl*freq + "A": spu.angstrom, + "NM": spu.nanometer, + "UM": spu.micrometer, + "MM": spu.millimeter, + "CM": spu.centimeter, + "M": spu.meter, + }, + }, + ST.MaxwellMonitor: { + "default": "NM", + "values": { + "PM": spu.picometer, ## c(vac) = wl*freq + "A": spu.angstrom, + "NM": spu.nanometer, + "UM": spu.micrometer, + "MM": spu.millimeter, + "CM": spu.centimeter, + "M": spu.meter, + }, + }, +} diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/trees.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/trees.py new file mode 100644 index 0000000..3ec9833 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/trees.py @@ -0,0 +1,9 @@ +import enum + +from ....utils.blender_type_enum import ( + BlenderTypeEnum, append_cls_name_to_values +) + +@append_cls_name_to_values +class TreeType(BlenderTypeEnum): + MaxwellSim = enum.auto() diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/__init__.py new file mode 100644 index 0000000..d06f918 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/__init__.py @@ -0,0 +1,2 @@ +from .managed_bl_image import ManagedBLImage +from .managed_bl_object import ManagedBLObject diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py new file mode 100644 index 0000000..72b4f48 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py @@ -0,0 +1,190 @@ +import typing as typ +import typing_extensions as typx +import io + +import numpy as np +import pydantic as pyd +import matplotlib.axis as mpl_ax + +import bpy + +from .. import contracts as ct + +AREA_TYPE = "IMAGE_EDITOR" +SPACE_TYPE = "IMAGE_EDITOR" + +class ManagedBLImage(ct.schemas.ManagedObj): + managed_obj_type = ct.ManagedObjType.ManagedBLImage + _bl_image_name: str + + def __init__(self, name: str): + ## TODO: Check that blender doesn't have any other images by the same name. + self._bl_image_name = name + + @property + def name(self): + return self._bl_image_name + + @name.setter + def name(self, value: str): + ## TODO: Check that blender doesn't have any other images by the same name. + if (bl_image := bpy.data.images.get(self.name)): + bl_image.name = value + + self._bl_image_name = value + + def free(self): + if (bl_image := bpy.data.images.get(self.name)): + bpy.data.images.remove(bl_image) + + #################### + # - Managed Object Management + #################### + def bl_image( + self, + width_px: int, + height_px: int, + color_model: typx.Literal["RGB", "RGBA"], + dtype: typx.Literal["uint8", "float32"], + ): + """Returns the managed blender image. + + If the requested image properties are different from the image's, then delete the old image make a new image with correct geometry. + """ + channels = 4 if color_model == "RGBA" else 3 + + # Remove Image (if mismatch) + if ( + (bl_image := bpy.data.images.get(self.name)) + and ( + bl_image.size[0] != width_px + or bl_image.size[1] != height_px + or bl_image.channels != channels + or bl_image.is_float ^ (dtype == "float32") + ) + ): + self.free() + + # Create Image w/Geometry (if none exists) + if not (bl_image := bpy.data.images.get(self.name)): + bl_image = bpy.data.images.new( + self.name, + width=width_px, + height=height_px, + ) + + return bl_image + + #################### + # - Editor UX Manipulation + #################### + @property + def preview_area(self) -> bpy.types.Area: + """Returns the visible preview area in the Blender UI. + If none are valid, return None. + """ + valid_areas = [ + area + for area in bpy.context.screen.areas + if area.type == AREA_TYPE + ] + if valid_areas: + return valid_areas[0] + + @property + def preview_space(self) -> bpy.types.SpaceProperties: + """Returns the visible preview space in the visible preview area of + the Blender UI + """ + if (preview_area := self.preview_area): + return next( + space + for space in preview_area.spaces + if space.type == SPACE_TYPE + ) + + #################### + # - Actions + #################### + def bl_select(self) -> None: + """Synchronizes the managed object to the preview, by manipulating + relevant editors. + """ + if (bl_image := bpy.data.images.get(self.name)): + self.preview_space.image = bl_image + + #################### + # - Special Methods + #################### + def mpl_plot_to_image( + self, + func_plotter: typ.Callable[[mpl_ax.Axis], None], + width_inches: float | None = None, + height_inches: float | None = None, + dpi: int | None = None, + bl_select: bool = False, + ): + import matplotlib.pyplot as plt + + # Compute Image Geometry + if (preview_area := self.preview_area): + # Retrieve DPI from Blender Preferences + _dpi = bpy.context.preferences.system.dpi + + # Retrieve Image Geometry from Area + width_px = preview_area.width + height_px = preview_area.height + + # Compute Inches + _width_inches = width_px / _dpi + _height_inches = height_px / _dpi + + elif width_inches and height_inches and dpi: + # Copy Parameters + _dpi = dpi + _width_inches = height_inches + _height_inches = height_inches + + # Compute Pixel Geometry + width_px = int(_width_inches * _dpi) + height_px = int(_height_inches * _dpi) + + else: + msg = f"There must either be a preview area, or defined `width_inches`, `height_inches`, and `dpi`" + raise ValueError(msg) + + # Compute Plot Dimensions + aspect_ratio = _width_inches / _height_inches + + # Create MPL Figure, Axes, and Compute Figure Geometry + fig, ax = plt.subplots( + figsize=[_width_inches, _height_inches], + dpi=_dpi, + ) + ax.set_aspect(aspect_ratio) + cmp_width_px, cmp_height_px = fig.canvas.get_width_height() + ## Use computed pixel w/h to preempt off-by-one size errors. + + # Plot w/User Parameter + func_plotter(ax) + + # Save Figure to BytesIO + with io.BytesIO() as buff: + fig.savefig(buff, format='raw', dpi=dpi) + buff.seek(0) + image_data = np.frombuffer( + buff.getvalue(), + dtype=np.uint8, + ).reshape([cmp_height_px, cmp_width_px, -1]) + + image_data = np.flipud(image_data).astype(np.float32) / 255 + plt.close(fig) + + # Optimized Write to Blender Image + bl_image = self.bl_image(cmp_width_px, cmp_height_px, "RGBA", "uint8") + bl_image.pixels.foreach_set(image_data.ravel()) + bl_image.update() + + if bl_select: + self.bl_select() + diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_object.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_object.py new file mode 100644 index 0000000..be27086 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_object.py @@ -0,0 +1,202 @@ +import typing as typ +import typing_extensions as typx +import functools +import contextlib +import io + +import numpy as np +import pydantic as pyd +import matplotlib.axis as mpl_ax + +import bpy +import bmesh + +from .. import contracts as ct + +class ManagedBLObject(ct.schemas.ManagedObj): + managed_obj_type = ct.ManagedObjType.ManagedBLObject + _bl_object_name: str + + def __init__(self, name: str): + ## TODO: Check that blender doesn't have any other objects by the same name. + self._bl_object_name = name + + # Object Name + @property + def bl_object_name(self): + return self._bl_object_name + + @bl_object_name.setter + def set_bl_object_name(self, value: str): + ## TODO: Check that blender doesn't have any other objects by the same name. + if (bl_object := bpy.data.objects.get(self.bl_object_name)): + bl_object.name = value + + self._bl_object_name = value + + # Object Datablock Name + @property + def bl_mesh_name(self): + return self.bl_object_name + "Mesh" + + @property + def bl_volume_name(self): + return self.bl_object_name + "Volume" + + # Deallocation + def free(self): + if (bl_object := bpy.data.objects.get(self.bl_object_name)): + # Delete the Underlying Datablock + if bl_object.type == "MESH": + bpy.data.meshes.remove(bl_object.data) + elif bl_object.type == "VOLUME": + bpy.data.volumes.remove(bl_object.data) + else: + msg = f"Type of to-delete `bl_object`, {bl_object.type}, is not valid" + raise ValueError(msg) + + #################### + # - Actions + #################### + def trigger_action( + self, + action: typx.Literal["report", "enable_previews"], + ): + if action == "report": + pass ## TODO: Cache invalidation. + + if action == "enable_previews": + + pass ## Image "previews" don't need enabling. + + def bl_select(self) -> None: + """Selects the managed Blender object globally, causing it to be ex. + outlined in the 3D viewport. + """ + bpy.ops.object.select_all(action='DESELECT') + bpy.data.objects['Suzanne'].select_set(True) + + #################### + # - Managed Object Management + #################### + def bl_object( + self, + kind: typx.Literal["MESH", "VOLUME"], + ): + """Returns the managed blender object. + + If the requested object data type is different, then delete the old + object and recreate. + """ + # Remove Object (if mismatch) + if ( + (bl_object := bpy.data.images.get(self.bl_object_name)) + and bl_object.type != kind + ): + self.free() + + # Create Object w/Appropriate Data Block + if not (bl_object := bpy.data.images.get(self.bl_object_name)): + if bl_object.type == "MESH": + bl_data = bpy.data.meshes.new(self.bl_mesh_name) + elif bl_object.type == "VOLUME": + raise NotImplementedError + else: + msg = f"Requested `bl_object` type {bl_object.type} is not valid" + raise ValueError(msg) + + bl_object = bpy.data.objects.new(self.bl_object_name, bl_data) + + return bl_object + + #################### + # - Data Properties + #################### + @property + def raw_mesh(self) -> bpy.types.Mesh: + """Returns the object's raw mesh data. + + Raises an error if the object has no mesh data. + """ + if ( + (bl_object := bpy.data.objects.get(self.bl_object_name)) + and bl_object.type == "MESH" + ): + return bl_object.data + + msg = f"Requested MESH data from `bl_object` of type {bl_object.type}" + raise ValueError(msg) + + @contextlib.contextmanager + def as_bmesh( + self, + evaluate: bool = True, + triangulate: bool = False, + ) -> bpy.types.Mesh: + if ( + (bl_object := bpy.data.objects.get(self.bl_object_name)) + and bl_object.type == "MESH" + ): + bmesh_mesh = None + try: + bmesh_mesh = bmesh.new() + if evaluate: + bmesh_mesh.from_object( + bl_object, + bpy.context.evaluated_depsgraph_get(), + ) + else: + bmesh_mesh.from_object(bl_object) + + if triangulate: + bmesh.ops.triangulate(bmesh_mesh, faces=bmesh_mesh.faces) + + yield bmesh_mesh + + finally: + if bmesh_mesh: bmesh_mesh.free() + + msg = f"Requested BMesh from `bl_object` of type {bl_object.type}" + raise ValueError(msg) + + @functools.cached_property + def as_arrays(self) -> dict: + # Compute Evaluted + Triangulated Mesh + _mesh = bpy.data.meshes.new(name="TemporaryMesh") + with self.as_bmesh(evaluate=True, triangulate=True) as bmesh_mesh: + bmesh_mesh.to_mesh(_mesh) + + # Optimized Vertex Copy + ## See + verts = np.zeros(3 * len(_mesh.vertices), dtype=np.float64) + _mesh.vertices.foreach_get('co', verts) + verts.shape = (-1, 3) + + # Optimized Triangle Copy + ## To understand, read it, **carefully**. + faces = np.zeros(3 * len(_mesh.polygons), dtype=np.uint64) + _mesh.polygons.foreach_get('vertices', faces) + faces.shape = (-1, 3) + + # Remove Temporary Mesh + bpy.data.meshes.remove(_mesh) + + return { + "verts": verts, + "faces": faces, + } + + #@property + #def volume(self) -> bpy.types.Volume: + # """Returns the object's volume data. + # + # Raises an error if the object has no volume data. + # """ + # if ( + # (bl_object := bpy.data.objects.get(self.bl_object_name)) + # and bl_object.type == "VOLUME" + # ): + # return bl_object.data + # + # msg = f"Requested VOLUME data from `bl_object` of type {bl_object.type}" + # raise ValueError(msg) diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py index 1b7e335..7527d50 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py @@ -1,92 +1,184 @@ +import typing as typ + import bpy -from . import contracts +from . import contracts as ct -ICON_SIM_TREE = 'MOD_SIMPLEDEFORM' +#################### +# - Cache Management +#################### +MemAddr = int +class DeltaNodeLinkCache(typ.TypedDict): + added: set[MemAddr] + removed: set[MemAddr] -class BLENDER_MAXWELL_PT_MaxwellSimTreePanel(bpy.types.Panel): - bl_label = "Node Tree Custom Prop" - bl_idname = "NODE_PT_custom_prop" - bl_space_type = 'NODE_EDITOR' - bl_region_type = 'UI' - bl_category = 'Item' - - @classmethod - def poll(cls, context): - return context.space_data.tree_type == contracts.TreeType.MaxwellSim.value - - def draw(self, context): - layout = self.layout - node_tree = context.space_data.node_tree - - layout.prop(node_tree, "preview_collection") - layout.prop(node_tree, "non_preview_collection") +class NodeLinkCache: + def __init__(self, node_tree: bpy.types.NodeTree): + # Initialize Parameters + self._node_tree = node_tree + self.link_ptrs_to_links = {} + self.link_ptrs = set() + self.link_ptrs_from_sockets = {} + self.link_ptrs_to_sockets = {} + + # Fill Cache + self.regenerate() + + def remove(self, link_ptrs: set[MemAddr]) -> None: + for link_ptr in link_ptrs: + self.link_ptrs.remove(link_ptr) + self.link_ptrs_to_links.pop(link_ptr, None) + + def regenerate(self) -> DeltaNodeLinkCache: + current_link_ptrs_to_links = { + link.as_pointer(): link for link in self._node_tree.links + } + current_link_ptrs = set(current_link_ptrs_to_links.keys()) + + # Compute Delta + added_link_ptrs = current_link_ptrs - self.link_ptrs + removed_link_ptrs = self.link_ptrs - current_link_ptrs + + # Update Caches Incrementally + self.remove(removed_link_ptrs) + + self.link_ptrs |= added_link_ptrs + for link_ptr in added_link_ptrs: + link = current_link_ptrs_to_links[link_ptr] + + self.link_ptrs_to_links[link_ptr] = link + self.link_ptrs_from_sockets[link_ptr] = link.from_socket + self.link_ptrs_to_sockets[link_ptr] = link.to_socket + + return {"added": added_link_ptrs, "removed": removed_link_ptrs} #################### # - Node Tree Definition #################### class MaxwellSimTree(bpy.types.NodeTree): - bl_idname = contracts.TreeType.MaxwellSim + bl_idname = ct.TreeType.MaxwellSim.value bl_label = "Maxwell Sim Editor" - bl_icon = contracts.Icon.MaxwellSimTree + bl_icon = ct.Icon.SimNodeEditor.value + managed_collection: bpy.props.PointerProperty( + name="Managed Collection", + description="Collection of Blender objects managed by this tree", + type=bpy.types.Collection, + ) preview_collection: bpy.props.PointerProperty( name="Preview Collection", description="Collection of Blender objects that will be previewed", type=bpy.types.Collection, - update=(lambda self, context: self.trigger_updates()) - ) - non_preview_collection: bpy.props.PointerProperty( - name="Non-Preview Collection", - description="Collection of Blender objects that will NOT be previewed", - type=bpy.types.Collection, - update=(lambda self, context: self.trigger_updates()) ) - def trigger_updates(self): - pass + #################### + # - Lock Methods + #################### + def unlock_all(self): + for node in self.nodes: + node.locked = False + for bl_socket in [*node.inputs, *node.outputs]: + bl_socket.locked = False + + #################### + # - Update Methods + #################### + def sync_node_removed(self, node: bpy.types.Node): + """Run by `Node.free()` when a node is being removed. + + Removes node input links from the internal cache (so we don't attempt to update non-existant sockets). + """ + for bl_socket in node.inputs.values(): + # Retrieve Socket Links (if any) + self._node_link_cache.remove({ + link.as_pointer() + for link in bl_socket.links + }) + ## ONLY Input Socket Links are Removed from the NodeLink Cache + ## - update() handles link-removal from still-existing node just fine. + ## - update() does NOT handle link-removal of non-existant nodes. + + def update(self): + """Run by Blender when 'something changes' in the node tree. + + Updates an internal node link cache, then updates sockets that just lost/gained an input link. + """ + if not hasattr(self, "_node_link_cache"): + self._node_link_cache = NodeLinkCache(self) + ## We presume update() is run before the first link is altered. + ## - Else, the first link of the session will not update caches. + ## - We remain slightly unsure of the semantics. + ## - More testing needed to prevent this 'first-link bug'. + return + + # Compute Changes to NodeLink Cache + delta_links = self._node_link_cache.regenerate() + + link_alterations = { + "to_remove": [], + "to_add": [], + } + for link_ptr in delta_links["removed"]: + from_socket = self._node_link_cache.link_ptrs_from_sockets[link_ptr] + to_socket = self._node_link_cache.link_ptrs_to_sockets[link_ptr] + + # Update Socket Caches + self._node_link_cache.link_ptrs_from_sockets.pop(link_ptr, None) + self._node_link_cache.link_ptrs_to_sockets.pop(link_ptr, None) + + # Trigger Report Chain on Socket that Just Lost a Link + ## Aka. Forward-Refresh Caches Relying on Linkage + if not ( + consent_removal := to_socket.sync_link_removed(from_socket) + ): + # Did Not Consent to Removal: Queue Add Link + link_alterations["to_add"].append((from_socket, to_socket)) + + for link_ptr in delta_links["added"]: + link = self._node_link_cache.link_ptrs_to_links.get(link_ptr) + if link is None: continue + + # Trigger Report Chain on Socket that Just Gained a Link + ## Aka. Forward-Refresh Caches Relying on Linkage + + if not ( + consent_added := link.to_socket.sync_link_added(link) + ): + # Did Not Consent to Addition: Queue Remove Link + link_alterations["to_remove"].append(link) + + # Execute Queued Operations + ## - Especially undoing undesirable link changes. + ## - This is important for locked graphs, whose links must not change. + for link in link_alterations["to_remove"]: + self.links.remove(link) + for from_socket, to_socket in link_alterations["to_add"]: + self.links.new(from_socket, to_socket) + + # If Queued Operations: Regenerate Cache + ## - This prevents the next update() from picking up on alterations. + if link_alterations["to_remove"] or link_alterations["to_add"]: + self._node_link_cache.regenerate() + +#################### +# - Post-Load Handler +#################### +def initialize_sim_tree_node_link_cache(scene: bpy.types.Scene): + """Whenever a file is loaded, create/regenerate the NodeLinkCache in all trees. + """ + for node_tree in bpy.data.node_groups: + if node_tree.bl_idname == "MaxwellSimTree": + if not hasattr(node_tree, "_node_link_cache"): + node_tree._node_link_cache = NodeLinkCache(node_tree) + else: + node_tree._node_link_cache.regenerate() #################### # - Blender Registration #################### +bpy.app.handlers.load_post.append(initialize_sim_tree_node_link_cache) + BL_REGISTER = [ MaxwellSimTree, ] - - - -#################### -# - Red Edges on Error -#################### -## TODO: Refactor -#def link_callback_new(context): -# print("A THING HAPPENED") -# node_tree_type = contracts.TreeType.MaxwellSim.value -# link = context.link -# -# if not ( -# link.from_node.node_tree.bl_idname == node_tree_type -# and link.to_node.node_tree.bl_idname == node_tree_type -# ): -# return -# -# source_node = link.from_node -# -# source_socket_name = source_node.g_output_socket_name( -# link.from_socket.name -# ) -# link_data = source_node.compute_output(source_socket_name) -# -# destination_socket = link.to_socket -# link.is_valid = destination_socket.is_compatible(link_data) -# -# print(source_node, destination_socket, link.is_valid) -# -#bpy.msgbus.subscribe_rna( -# key=("active", "node_tree"), -# owner=MaxwellSimTree, -# args=(bpy.context,), -# notify=link_callback_new, -# options={'PERSISTENT'} -#) diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py index 8262f4c..40fd4ed 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py @@ -5,10 +5,10 @@ from . import outputs from . import sources from . import mediums from . import structures -from . import bounds -from . import monitors +#from . import bounds +#from . import monitors from . import simulations -from . import utilities +#from . import utilities BL_REGISTER = [ *kitchen_sink.BL_REGISTER, @@ -17,10 +17,10 @@ BL_REGISTER = [ *sources.BL_REGISTER, *mediums.BL_REGISTER, *structures.BL_REGISTER, - *bounds.BL_REGISTER, - *monitors.BL_REGISTER, +# *bounds.BL_REGISTER, +# *monitors.BL_REGISTER, *simulations.BL_REGISTER, - *utilities.BL_REGISTER, +# *utilities.BL_REGISTER, ] BL_NODES = { **kitchen_sink.BL_NODES, @@ -29,8 +29,8 @@ BL_NODES = { **sources.BL_NODES, **mediums.BL_NODES, **structures.BL_NODES, - **bounds.BL_NODES, - **monitors.BL_NODES, +# **bounds.BL_NODES, +# **monitors.BL_NODES, **simulations.BL_NODES, - **utilities.BL_NODES, +# **utilities.BL_NODES, } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py index d5f494b..d29bcb4 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py @@ -1,204 +1,167 @@ +import uuid + import typing as typ -import typing_extensions as pytypes_ext +import typing_extensions as typx +import json +import inspect import bpy import pydantic as pyd -from .. import contracts +from .. import contracts as ct from .. import sockets -#################### -# - Decorator: Output Socket Computation -#################### -@typ.runtime_checkable -class ComputeOutputSocketFunc(typ.Protocol[contracts.SocketReturnType]): - """Protocol describing a function that computes the value of an - output socket. - """ - - def __call__( - _self, - self: contracts.NodeTypeProtocol, - ) -> contracts.SocketReturnType: - """Describes the function signature of all functions that compute - the value of an output socket. - - Args: - node: A node in the tree, passed via the 'self' attribute of the - node. - - Returns: - The value of the output socket, as the relevant type. - """ - ... +CACHE: dict[str, typ.Any] = {} ## By Instance UUID +## NOTE: CACHE does not persist between file loads. -class PydanticProtocolMeta(type(pyd.BaseModel), type(typ.Protocol)): pass - -class FuncOutputSocket( - pyd.BaseModel, - typ.Generic[contracts.SocketReturnType], - ComputeOutputSocketFunc[contracts.SocketReturnType], - metaclass=PydanticProtocolMeta, -): - """Defines a function (-like object) that defines an attachment from - an output socket name, to the original method that computes the value of - an output socket. +class MaxwellSimNode(bpy.types.Node): + # Fundamentals + node_type: ct.NodeType + bl_idname: str + use_sim_node_name: bool = False + bl_label: str + #draw_label(self) -> str: pass - Conforms to the protocol `ComputeOutputSocketFunc`. - Validation is provided by subtyping `pydantic.BaseModel`. + # Style + bl_description: str = "" - Attributes: - output_socket_func: The original method computing the value of an - output socket. - output_socket_name: The SocketName of the output socket for which - this function should be called to compute. - """ + #bl_width_default: float = 0.0 + #bl_width_min: float = 0.0 + #bl_width_max: float = 0.0 - output_socket_func: typ.Callable[ - [contracts.NodeTypeProtocol], - contracts.SocketReturnType, - ] - output_socket_name: contracts.SocketName + # Sockets + _output_socket_methods: dict - def __call__( - self, - node: contracts.NodeTypeProtocol - ) -> contracts.SocketReturnType: - """Computes the value of an output socket. - - Args: - node: A node in the tree, passed via the 'self' attribute of the - node. - - Returns: - The value of the output socket, as the relevant type. - """ - - return self.output_socket_func(node) - -# Define Factory Function & Decorator -def computes_output_socket( - output_socket_name: contracts.SocketName, -) -> typ.Callable[ - [ComputeOutputSocketFunc[contracts.SocketReturnType]], - FuncOutputSocket[contracts.SocketReturnType], -]: - """Given a socket name, defines a function-that-makes-a-function (aka. - decorator) which has the name of the socket attached. + input_sockets: dict[str, ct.schemas.SocketDef] = {} + output_sockets: dict[str, ct.schemas.SocketDef] = {} + input_socket_sets: dict[str, dict[str, ct.schemas.SocketDef]] = {} + output_socket_sets: dict[str, dict[str, ct.schemas.SocketDef]] = {} - Must be used as a decorator, ex. `@compute_output_socket("name")`. + # Presets + presets = {} - Args: - output_socket_name: The name of the output socket to attach the - decorated method to. + # Managed Objects + managed_obj_defs: dict[ct.ManagedObjName, ct.schemas.ManagedObjDef] = {} - Returns: - The decorator, which takes the output-socket-computing method - and returns a new output-socket-computing method, now annotated - and discoverable by the `MaxwellSimTreeNode`. - """ - - def decorator( - output_socket_func: ComputeOutputSocketFunc[contracts.SocketReturnType] - ) -> FuncOutputSocket[contracts.SocketReturnType]: - return FuncOutputSocket( - output_socket_func=output_socket_func, - output_socket_name=output_socket_name, - ) - - return decorator - - - -#################### -# - Node Callbacks -#################### -def sync_selected_preset(node) -> None: - """Whenever a preset is set in a NodeTypeProtocol, this function - should be called to overwrite the `default_value`s of the input sockets - with the actual preset values. - - Args: - node: The node for which input socket `default_value`s should be - set to the values defined within the currently selected preset. - """ - if hasattr(node, "preset") and hasattr(node, "presets"): - if node.preset is None: - msg = f"Node {node} has no preset EnumProperty" - raise ValueError(msg) - - if node.presets is None: - msg = f"Node {node} has preset EnumProperty, but no defined presets." - raise ValueError(msg) - - # Set Input Sockets to Preset Values - preset_def = node.presets[node.preset] - for input_socket_name, value in preset_def.values.items(): - node.s_input_value(input_socket_name, value) - - - -#################### -# - Node Superclass Definition -#################### -class MaxwellSimTreeNode(bpy.types.Node): - """A base type for nodes that greatly simplifies the implementation of - reliable, powerful nodes. - - Should be used together with `contracts.NodeTypeProtocol`. - """ + #################### + # - Initialization + #################### def __init_subclass__(cls, **kwargs: typ.Any): - super().__init_subclass__(**kwargs) ## Yucky superclass setup. + super().__init_subclass__(**kwargs) - # Set bl_idname - cls.bl_idname = cls.node_type.value + # Setup Blender ID for Node + if not hasattr(cls, "node_type"): + msg = f"Node class {cls} does not define 'node_type', or it is does not have the type {ct.NodeType}" + raise ValueError(msg) + cls.bl_idname = str(cls.node_type.value) - # Declare Node Property: 'preset' EnumProperty - if hasattr(cls, "input_socket_sets") or hasattr(cls, "output_socket_sets"): - socket_set_keys = [ - input_socket_set_key - for input_socket_set_key in cls.input_socket_sets.keys() - ] - socket_set_keys += [ - output_socket_set_key - for output_socket_set_key in cls.output_socket_sets.keys() - if output_socket_set_key not in socket_set_keys + # Setup Instance ID for Node + cls.__annotations__["instance_id"] = bpy.props.StringProperty( + name="Instance ID", + description="The instance ID of a particular MaxwellSimNode instance, used to index caches", + default="", + ) + + # Setup Name Property for Node + cls.__annotations__["sim_node_name"] = bpy.props.StringProperty( + name="Sim Node Name", + description="The name of a particular MaxwellSimNode node, which can be used to help identify data managed by the node", + default="", + update=(lambda self, context: self._sync_sim_node_name(context)) + ) + + # Setup Locked Property for Node + cls.__annotations__["locked"] = bpy.props.BoolProperty( + name="Locked State", + description="The lock-state of a particular MaxwellSimNode instance, which determines the node's user editability", + default=False, + ) + + # Setup Blender Label for Node + if not hasattr(cls, "bl_label"): + msg = f"Node class {cls} does not define 'bl_label'" + raise ValueError(msg) + + # Setup Callback Methods + cls._output_socket_methods = { + method._index_by: method + for attr_name in dir(cls) + if hasattr( + method := getattr(cls, attr_name), + "_callback_type" + ) and method._callback_type == "computes_output_socket" + } + cls._on_value_changed_methods = { + method + for attr_name in dir(cls) + if hasattr( + method := getattr(cls, attr_name), + "_callback_type" + ) and method._callback_type == "on_value_changed" + } + cls._on_show_preview = { + method + for attr_name in dir(cls) + if hasattr( + method := getattr(cls, attr_name), + "_callback_type" + ) and method._callback_type == "on_show_preview" + } + cls._on_show_plot = { + method + for attr_name in dir(cls) + if hasattr( + method := getattr(cls, attr_name), + "_callback_type" + ) and method._callback_type == "on_show_plot" + } + + # Setup Socket Set Dropdown + if not len(cls.input_socket_sets) + len(cls.output_socket_sets) > 0: + cls.active_socket_set = None + else: + ## Add Active Socket Set Enum + socket_set_names = ( + (_input_socket_set_names := list(cls.input_socket_sets.keys())) + + [ + output_socket_set_name + for output_socket_set_name in cls.output_socket_sets.keys() + if output_socket_set_name not in _input_socket_set_names + ] + ) + socket_set_ids = [ + socket_set_name.replace(" ", "_").upper() + for socket_set_name in socket_set_names ] + ## TODO: Better deriv. of sock.set. ID, ex. ( is currently invalid. - def labeller(socket_set_key): - return " ".join( - word.capitalize() - for word in socket_set_key.split("_") - ) - cls.__annotations__["socket_set"] = bpy.props.EnumProperty( - name="", - description="Select a node socket configuration", + ## Add Active Socket Set Enum + cls.__annotations__["active_socket_set"] = bpy.props.EnumProperty( + name="Active Socket Set", + description="The active socket set", items=[ ( - socket_set_key, - labeller(socket_set_key), - labeller(socket_set_key), + socket_set_name, + socket_set_name, + socket_set_name, + ) + for socket_set_id, socket_set_name in zip( + socket_set_ids, + socket_set_names, ) - for socket_set_key in socket_set_keys ], - default=socket_set_keys[0], - update=(lambda self, context: self._update_socket()), - ) - cls.__annotations__["socket_set_previous"] = bpy.props.StringProperty( - default=socket_set_keys[0] + default=socket_set_names[0], + update=(lambda self, _: self._sync_sockets()), ) - if not hasattr(cls, "input_socket_sets"): - cls.input_socket_sets = {} - if not hasattr(cls, "output_socket_sets"): - cls.output_socket_sets = {} - - # Declare Node Property: 'preset' EnumProperty - if hasattr(cls, "presets"): - first_preset = list(cls.presets.keys())[0] - cls.__annotations__["preset"] = bpy.props.EnumProperty( - name="Presets", - description="Select a preset", + # Setup Preset Dropdown + if not cls.presets: + cls.active_preset = None + else: + ## TODO: Check that presets are represented in a socket that is guaranteed to be always available, specifically either a static socket or ALL static socket sets. + cls.__annotations__["active_preset"] = bpy.props.EnumProperty( + name="Active Preset", + description="The active preset", items=[ ( preset_name, @@ -207,147 +170,269 @@ class MaxwellSimTreeNode(bpy.types.Node): ) for preset_name, preset_def in cls.presets.items() ], - default=first_preset, ## 1st is Default - update=(lambda self, context: sync_selected_preset(self)), + default=list(cls.presets.keys())[0], + update=lambda self, context: ( + self._sync_active_preset()() + ), ) - else: - cls.preset = None - cls.presets = None #################### - # - Blender Init / Constraints + # - Generic Properties #################### - def init(self, context: bpy.types.Context): - """Declares input and output sockets as described by the - `NodeTypeProtocol` specification, and initializes each as described - by user-provided `SocketDefProtocol`s. - """ - # Create Input Sockets - for socket_name, socket_def in self.input_sockets.items(): - self.inputs.new( - socket_def.socket_type.value, - socket_def.label, - ) - - # Create Output Sockets - for socket_name, socket_def in self.output_sockets.items(): - self.outputs.new( - socket_def.socket_type.value, - socket_def.label, - ) + def _sync_sim_node_name(self, context): + for managed_obj in self.managed_objs.values(): + managed_obj.name = self.sim_node_name - # Initialize Sockets - for socket_name, socket_def in self.input_sockets.items(): - bl_socket = self.inputs[ - self.input_sockets[socket_name].label - ] - socket_def.init(bl_socket) + # Recurse Until Equal + if managed_obj.name != self.sim_node_name: + self.sim_node_name = managed_obj.name + ## ManagedObj is allowed to alter the name when setting it. + ## - This will happen whenever the name is taken. + ## - If altered, set the 'sim_node_name' to the altered name. + ## - This will cause recursion, but only once. + + #################### + # - Managed Object Properties + #################### + @property + def managed_objs(self): + global CACHE + if not CACHE.get(self.instance_id): + CACHE[self.instance_id] = {} + + # If No Managed Objects in CACHE: Initialize Managed Objects + ## - This happens on every ex. file load, init(), etc. . + ## - ManagedObjects MUST the same object by name. + ## - We sync our 'sim_node_name' with all managed objects. + ## - (There is also a class-defined 'name_prefix' to differentiate) + ## - See the 'sim_node_name' w/its _sync function. + if CACHE[self.instance_id].get("managed_objs") is None: + # Initialize the Managed Object Instance Cache + CACHE[self.instance_id]["managed_objs"] = {} - for socket_name, socket_def in self.output_sockets.items(): - bl_socket = self.outputs[ - self.output_sockets[socket_name].label - ] - socket_def.init(bl_socket) - - # Initialize Dynamic Sockets - if hasattr(self, "socket_set"): - if self.socket_set in self.input_socket_sets: - for socket_name, socket_def in self.input_socket_sets[self.socket_set].items(): - self.inputs.new( - socket_def.socket_type.value, - socket_def.label, - ) - - bl_socket = self.inputs[socket_def.label] - socket_def.init(bl_socket) - - if self.socket_set in self.output_socket_sets: - for socket_name, socket_def in self.output_socket_sets[self.socket_set].items(): - self.outputs.new( - socket_def.socket_type.value, - socket_def.label, - ) - - bl_socket = self.outputs[socket_def.label] - socket_def.init(bl_socket) - - # Sync Default Preset to Input Socket Values - if self.preset is not None: - sync_selected_preset(self) - - if hasattr(self, "init_cb"): - self.init_cb() - - @classmethod - def poll(cls, ntree: bpy.types.NodeTree) -> bool: - """This class method controls whether a node can be instantiated - in a given node tree. - - In our case, we restrict node instantiation to within a - MaxwellSimTree. - - Args: - ntree: The node tree within which the user is currently working. - - Returns: - Whether or not the user should be able to instantiate the node. - - """ - - return ntree.bl_idname == contracts.TreeType.MaxwellSim.value - - def update(self) -> None: - """Called when some node properties (ex. links) change, - and/or by custom code.""" - if hasattr(self, "update_cb"): - self.update_cb() - - for bl_socket in self.outputs: - if bl_socket.is_linked: - for node_link in bl_socket.links: - linked_node = node_link.to_node - linked_node.update() - - def _update_socket(self): - if not hasattr(self, "socket_set"): - raise ValueError("no socket") - - if self.socket_set == self.socket_set_previous: return - - # Delete Old Sockets - if self.socket_set_previous in self.input_socket_sets: - for socket_name, socket_def in self.input_socket_sets[self.socket_set_previous].items(): - bl_socket = self.inputs[socket_def.label] - self.inputs.remove(bl_socket) - - if self.socket_set_previous in self.output_socket_sets: - for socket_name, socket_def in self.output_socket_sets[self.socket_set_previous].items(): - bl_socket = self.outputs[socket_def.label] - self.outputs.remove(bl_socket) - - # Add New Sockets - if self.socket_set in self.input_socket_sets: - for socket_name, socket_def in self.input_socket_sets[self.socket_set].items(): - self.inputs.new( - socket_def.socket_type.value, - socket_def.label, + # Fill w/Managed Objects by Name Socket + for mobj_id, mobj_def in self.managed_obj_defs.items(): + name = mobj_def.name_prefix + self.sim_node_name + CACHE[self.instance_id]["managed_objs"][mobj_id] = ( + mobj_def.mk(name) ) - - bl_socket = self.inputs[socket_def.label] - socket_def.init(bl_socket) + + return CACHE[self.instance_id]["managed_objs"] - if self.socket_set in self.output_socket_sets: - for socket_name, socket_def in self.output_socket_sets[self.socket_set].items(): - self.outputs.new( - socket_def.socket_type.value, - socket_def.label, + return CACHE[self.instance_id]["managed_objs"] + + #################### + # - Socket Properties + #################### + def active_bl_sockets(self, direc: typx.Literal["input", "output"]): + return self.inputs if direc == "input" else self.outputs + + def active_socket_set_sockets( + self, + direc: typx.Literal["input", "output"], + ) -> dict: + # No Active Socket Set: Return Nothing + if not self.active_socket_set: return {} + + # Retrieve Active Socket Set Sockets + socket_sets = ( + self.input_socket_sets + if direc == "input" else self.output_socket_sets + ) + active_socket_set_sockets = socket_sets.get( + self.active_socket_set + ) + + # Return Active Socket Set Sockets (if any) + if not active_socket_set_sockets: return {} + return active_socket_set_sockets + + def active_sockets(self, direc: typx.Literal["input", "output"]): + static_sockets = ( + self.input_sockets + if direc == "input" + else self.output_sockets + ) + socket_sets = ( + self.input_socket_sets + if direc == "input" + else self.output_socket_sets + ) + loose_sockets = ( + self.loose_input_sockets + if direc == "input" + else self.loose_output_sockets + ) + + return ( + static_sockets + | self.active_socket_set_sockets(direc=direc) + | loose_sockets + ) + + #################### + # - Loose Sockets + #################### + _DEFAULT_LOOSE_SOCKET_SER = json.dumps({ + "socket_names": [], + "socket_def_names": [], + "models": [], + }) + # Loose Sockets + ## Only Blender props persist as instance data + ser_loose_input_sockets: bpy.props.StringProperty( + name="Serialized Loose Input Sockets", + description="JSON-serialized representation of loose input sockets.", + default=_DEFAULT_LOOSE_SOCKET_SER, + ) + ser_loose_output_sockets: bpy.props.StringProperty( + name="Serialized Loose Input Sockets", + description="JSON-serialized representation of loose input sockets.", + default=_DEFAULT_LOOSE_SOCKET_SER, + ) + + ## Internal Serialization/Deserialization Methods (yuck) + def _ser_loose_sockets(self, deser: dict[str, ct.schemas.SocketDef]) -> str: + if not all(isinstance(model, pyd.BaseModel) for model in deser.values()): + msg = "Trying to deserialize loose sockets with invalid SocketDefs (they must be `pydantic` BaseModels)." + raise ValueError(msg) + + return json.dumps({ + "socket_names": list(deser.keys()), + "socket_def_names": [ + model.__class__.__name__ + for model in deser.values() + ], + "models": [ + dict(model) + for model in deser.values() + if isinstance(model, pyd.BaseModel) + ], + }) ## Big reliance on order-preservation of dicts here.) + def _deser_loose_sockets(self, ser: str) -> dict[str, ct.schemas.SocketDef]: + semi_deser = json.loads(ser) + return { + socket_name: getattr(sockets, socket_def_name)(**model_kwargs) + for socket_name, socket_def_name, model_kwargs in zip( + semi_deser["socket_names"], + semi_deser["socket_def_names"], + semi_deser["models"], + ) + if hasattr(sockets, socket_def_name) + } + + @property + def loose_input_sockets(self) -> dict[str, ct.schemas.SocketDef]: + return self._deser_loose_sockets(self.ser_loose_input_sockets) + @property + def loose_output_sockets(self) -> dict[str, ct.schemas.SocketDef]: + return self._deser_loose_sockets(self.ser_loose_output_sockets) + ## TODO: Some caching may play a role if this is all too slow. + + @loose_input_sockets.setter + def loose_input_sockets( + self, value: dict[str, ct.schemas.SocketDef], + ) -> None: + self.ser_loose_input_sockets = self._ser_loose_sockets(value) + + # Synchronize Sockets + self._sync_sockets() + ## TODO: Perhaps re-init() all loose sockets anyway? + + @loose_output_sockets.setter + def loose_output_sockets( + self, value: dict[str, ct.schemas.SocketDef], + ) -> None: + self.ser_loose_output_sockets = self._ser_loose_sockets(value) + + # Synchronize Sockets + self._sync_sockets() + ## TODO: Perhaps re-init() all loose sockets anyway? + + #################### + # - Socket Management + #################### + def _prune_inactive_sockets(self): + """Remove all inactive sockets from the node. + + **NOTE**: Socket names must be unique within direction, active socket set, and loose socket set. + """ + for direc in ["input", "output"]: + sockets = self.active_sockets(direc) + bl_sockets = self.active_bl_sockets(direc) + + # Determine Sockets to Remove + bl_sockets_to_remove = [ + bl_socket + for socket_name, bl_socket in bl_sockets.items() + if socket_name not in sockets + ] + + # Remove Sockets + for bl_socket in bl_sockets_to_remove: + bl_sockets.remove(bl_socket) + + def _add_new_active_sockets(self): + """Add and initialize all non-existing active sockets to the node. + + Existing sockets within the given direction are not re-created. + """ + for direc in ["input", "output"]: + sockets = self.active_sockets(direc) + bl_sockets = self.active_bl_sockets(direc) + + # Define BL Sockets + created_sockets = {} + for socket_name, socket_def in sockets.items(): + # Skip Existing Sockets + if socket_name in bl_sockets: continue + + # Create BL Socket from Socket + bl_socket = bl_sockets.new( + str(socket_def.socket_type.value), + socket_name, ) + bl_socket.display_shape = bl_socket.socket_shape + ## `display_shape` needs to be dynamically set - bl_socket = self.outputs[socket_def.label] - socket_def.init(bl_socket) + # Record Created Socket + created_sockets[socket_name] = socket_def + + # Initialize Just-Created BL Sockets + for socket_name, socket_def in created_sockets.items(): + socket_def.init(bl_sockets[socket_name]) + + def _sync_sockets(self) -> None: + """Synchronize the node's sockets with the active sockets. - # Update "Previous" - self.socket_set_previous = self.socket_set + - Any non-existing active socket will be added and initialized. + - Any existing active socket will not be changed. + - Any existing inactive socket will be removed. + + Must be called after any change to socket definitions, including loose + sockets. + """ + self._prune_inactive_sockets() + self._add_new_active_sockets() + + #################### + # - Preset Management + #################### + def _sync_active_preset(self) -> None: + """Applies the active preset by overwriting the value of + preset-defined input sockets. + """ + if not (preset_def := self.presets.get(self.active_preset)): + msg = f"Tried to apply active preset, but the active preset ({self.active_preset}) is not in presets ({self.presets})" + raise RuntimeError(msg) + + for socket_name, socket_value in preset_def.values.items(): + if not (bl_socket := self.inputs.get(socket_name)): + msg = f"Tried to set preset socket/value pair ({socket_name}={socket_value}), but socket is not in active input sockets ({self.inputs})" + raise ValueError(msg) + + bl_socket.value = socket_value + ## TODO: Lazy-valued presets? #################### # - UI Methods @@ -357,187 +442,66 @@ class MaxwellSimTreeNode(bpy.types.Node): context: bpy.types.Context, layout: bpy.types.UILayout, ) -> None: - """This method draws the UI of the node itself. + if self.locked: layout.enabled = False - Specifically, it is used to expose the Presets dropdown. - """ - if self.preset is not None: - layout.prop(self, "preset", text="") + if self.active_preset: + layout.prop(self, "active_preset", text="") - if hasattr(self, "socket_set"): - layout.prop(self, "socket_set", text="") + if self.active_socket_set: + layout.prop(self, "active_socket_set", text="") - if hasattr(self, "draw_operators"): - self.draw_operators(context, layout) + # Draw Name + col = layout.column(align=False) + if self.use_sim_node_name: + col.prop(self, "sim_node_name", text="") - if hasattr(self, "draw_props"): - self.draw_props(context, layout) - - if hasattr(self, "draw_info"): - self.draw_info(context, layout) + # Draw Name + self.draw_props(context, col) + self.draw_operators(context, col) + self.draw_info(context, col) + + ## TODO: Managed Operators instead of this shit + def draw_props(self, context, layout): pass + def draw_operators(self, context, layout): pass + def draw_info(self, context, layout): pass + + def draw_buttons_ext(self, context, layout): pass + ## TODO: Side panel buttons for fanciness. + + def draw_plot_settings(self, context, layout): + if self.locked: layout.enabled = False #################### - # - Socket Getters + # - Data Flow #################### - def g_input_bl_socket( + def _compute_input( self, - input_socket_name: contracts.SocketName, - ) -> bpy.types.NodeSocket: - """Returns the `bpy.types.NodeSocket` of an input socket by name. + input_socket_name: ct.SocketName, + kind: ct.DataFlowKind = ct.DataFlowKind.Value, + ) -> typ.Any | None: + """Computes the data of an input socket, by socket name and data flow kind, by asking the socket nicely via `bl_socket.compute_data`. Args: input_socket_name: The name of the input socket, as defined in `self.input_sockets`. - - Returns: - Blender's own node socket object. + kind: The data flow kind to compute retrieve. """ + if not (bl_socket := self.inputs.get(input_socket_name)): + return None + #msg = f"Input socket name {input_socket_name} is not an active input sockets." + #raise ValueError(msg) - if input_socket_name in self.input_sockets: - # Check Nicely that it Exists - if input_socket_name not in self.input_sockets: - msg = f"Input socket with name {input_socket_name} does not exist" - raise ValueError(msg) - - return self.inputs[self.input_sockets[input_socket_name].label] - - elif hasattr(self, "socket_set"): - return self.inputs[next( - socket_def.label - for socket_set, socket_dict in self.input_socket_sets.items() - for socket_name, socket_def in socket_dict.items() - if socket_name == input_socket_name - if socket_set == self.socket_set - )] + return bl_socket.compute_data(kind=kind) - def g_output_bl_socket( - self, - output_socket_name: contracts.SocketName, - ) -> bpy.types.NodeSocket: - """Returns the `bpy.types.NodeSocket` of an output socket by name. - - Args: - output_socket_name: The name of the output socket, as defined in - `self.output_sockets`. - - Returns: - Blender's own node socket object. - """ - - - if output_socket_name in self.output_sockets: - # (Guard) Socket Exists - if output_socket_name not in self.output_sockets: - msg = f"Input socket with name {output_socket_name} does not exist" - raise ValueError(msg) - - return self.outputs[self.output_sockets[output_socket_name].label] - - elif hasattr(self, "socket_set"): - return self.outputs[next( - socket_def.label - for socket_set, socket_dict in self.input_socket_sets.items() - for socket_name, socket_def in socket_dict.items() - if socket_name == output_socket_name - )] - - def g_output_socket_name( - self, - output_bl_socket_name: contracts.BLSocketName, - ) -> contracts.SocketName: - output_socket_names = [ - output_socket_name - for output_socket_name in self.output_sockets.keys() - if self.output_sockets[ - output_socket_name - ].label == output_bl_socket_name - ] - if hasattr(self, "socket_set"): - output_socket_names += [ - socket_name - for socket_set, socket_dict in self.output_socket_sets.items() - for socket_name, socket_def in socket_dict.items() - if socket_def.label == output_bl_socket_name - ] - return output_socket_names[0] - - #################### - # - Socket Setters - #################### - def s_input_value( - self, - input_socket_name: contracts.SocketName, - value: typ.Any, - ) -> None: - """Sets the value of an input socket, if the value is compatible with - the socket. - - Args: - input_socket_name: The name of the input socket. - value: The value to set, which must be compatible with the - socket. - - Raises: - ValueError: If the value is incompatible with the socket, for - example due to incompatible types, then a ValueError will be - raised. - """ - bl_socket = self.g_input_bl_socket(input_socket_name) - - # Set the Value - bl_socket.default_value = value - - #################### - # - Socket Computation - #################### - def compute_input( - self, - input_socket_name: contracts.SocketName, - ) -> typ.Any: - """Computes the value of an input socket, by its name. Will - automatically compute the output socket value of any linked - nodes. - - Args: - input_socket_name: The name of the input socket, as defined in - `self.input_sockets`. - """ - bl_socket = self.g_input_bl_socket(input_socket_name) - - # Linked: Compute Output of Linked Socket - if bl_socket.is_linked: - linked_node = bl_socket.links[0].from_node - - # Compute the Linked Socket Name - linked_bl_socket_name: contracts.BLSocketName = bl_socket.links[0].from_socket.name - linked_socket_name = linked_node.g_output_socket_name( - linked_bl_socket_name - ) - - # Compute the Linked Socket Value - linked_socket_value = linked_node.compute_output( - linked_socket_name - ) - - # (Guard) Check the Compatibility of the Linked Socket Value - if not bl_socket.is_compatible(linked_socket_value): - msg = f"Tried setting socket ({input_socket_name}) to incompatible value ({linked_socket_value}) of type {type(linked_socket_value)}" - raise ValueError(msg) - - return linked_socket_value - - # Unlinked: Simply Retrieve Socket Value - return bl_socket.default_value - def compute_output( self, - output_socket_name: contracts.SocketName, + output_socket_name: ct.SocketName, + kind: ct.DataFlowKind = ct.DataFlowKind.Value, ) -> typ.Any: """Computes the value of an output socket name, from its socket name. - Searches for methods decorated with `@computes_output_socket("name")`, - which describe the computation that occurs to actually compute the - value of an output socket from ex. input sockets and node properties. + Searches methods decorated with `@computes_output_socket(output_socket_name, kind=..., ...)`, for a perfect match to the pair `socket_name..kind`. + This method is run to produce the value. Args: output_socket_name: The name declaring the output socket, @@ -547,17 +511,388 @@ class MaxwellSimTreeNode(bpy.types.Node): The value of the output socket, as computed by the dedicated method registered using the `@computes_output_socket` decorator. """ - # Lookup the Function that Computes the Output Socket - ## The decorator ALWAYS produces a FuncOutputSocket. - ## Thus, we merely need to find a FuncOutputSocket - output_socket_func = next( - method.output_socket_func - for attr_name in dir(self) ## Lookup self.* - if isinstance( - method := getattr(self, attr_name), - FuncOutputSocket, - ) - if method.output_socket_name == output_socket_name - ) + if not ( + output_socket_method := self._output_socket_methods.get( + (output_socket_name, kind) + ) + ): + msg = f"No output method for ({output_socket_name}, {str(kind.value)}" + raise ValueError(msg) - return output_socket_func(self) + return output_socket_method(self) + + #################### + # - Action Chain + #################### + def sync_prop(self, prop_name: str, context: bpy.types.Context): + """Called when a property has been updated. + """ + if not hasattr(self, prop_name): + msg = f"Property {prop_name} not defined on socket {self}" + raise RuntimeError(msg) + + self.trigger_action("value_changed", prop_name=prop_name) + + def trigger_action( + self, + action: typx.Literal["enable_lock", "disable_lock", "value_changed", "show_preview", "show_plot"], + socket_name: ct.SocketName | None = None, + prop_name: ct.SocketName | None = None, + ) -> None: + """Reports that the input socket is changed. + + Invalidates (recursively) the cache of any managed object or + output socket method that implicitly depends on this input socket. + """ + # Forwards Chains + if action == "value_changed": + # Run User Callbacks + ## Careful with these, they run BEFORE propagation... + ## ...because later-chain methods may rely on the results of this. + for method in self._on_value_changed_methods: + if ( + socket_name + and socket_name == method._extra_data.get("changed_socket") + ) or ( + prop_name + and socket_name == method._extra_data.get("changed_prop") + ): + method(self) + + # Propagate via Output Sockets + for bl_socket in self.active_bl_sockets("output"): + bl_socket.trigger_action(action) + + # Backwards Chains + elif action == "enable_lock": + self.locked = True + + ## Propagate via Input Sockets + for bl_socket in self.active_bl_sockets("input"): + bl_socket.trigger_action(action) + + elif action == "disable_lock": + self.locked = False + + ## Propagate via Input Sockets + for bl_socket in self.active_bl_sockets("input"): + bl_socket.trigger_action(action) + + elif action == "show_preview": + # Run User Callbacks + for method in self._on_show_preview: + method(self) + + ## Propagate via Input Sockets + for bl_socket in self.active_bl_sockets("input"): + bl_socket.trigger_action(action) + + elif action == "show_plot": + # Run User Callbacks + ## These shouldn't change any data, BUT... + ## ...because they can stop propagation, they should go first. + for method in self._on_show_plot: + method(self) + if method._extra_data["stop_propagation"]: + return + + ## Propagate via Input Sockets + for bl_socket in self.active_bl_sockets("input"): + bl_socket.trigger_action(action) + + #################### + # - Blender Node Methods + #################### + @classmethod + def poll(cls, node_tree: bpy.types.NodeTree) -> bool: + """Run (by Blender) to determine instantiability. + + Restricted to the MaxwellSimTreeType. + """ + + return node_tree.bl_idname == ct.TreeType.MaxwellSim.value + + def init(self, context: bpy.types.Context): + """Run (by Blender) on node creation. + """ + global CACHE + + # Initialize Cache and Instance ID + self.instance_id = str(uuid.uuid4()) + CACHE[self.instance_id] = {} + + # Initialize Name + self.sim_node_name = self.name + ## Only shown in draw_buttons if 'self.use_sim_node_name' + + # Initialize Sockets + self._sync_sockets() + + # Apply Default Preset + if self.active_preset: + self._sync_active_preset() + + def update(self) -> None: + pass + + def free(self) -> None: + """Run (by Blender) when deleting the node. + """ + global CACHE + if not CACHE.get(self.instance_id): + CACHE[self.instance_id] = {} + node_tree = self.id_data + + # Free Managed Objects + for managed_obj in self.managed_objs.values(): + managed_obj.free() + + # Update NodeTree Caches + ## The NodeTree keeps caches to for optimized event triggering. + ## However, ex. deleted nodes also deletes links, without cache update. + ## By reporting that we're deleting the node, the cache stays happy. + node_tree.sync_node_removed(self) + + # Finally: Free Instance Cache + if self.instance_id in CACHE: + del CACHE[self.instance_id] + + + +def chain_event_decorator( + callback_type: typ.Literal[ + "computes_output_socket", + "on_value_changed", + "on_show_preview", + "on_show_plot", + ], + index_by: typ.Any | None = None, + extra_data: dict[str, typ.Any] | None = None, + + kind: ct.DataFlowKind = ct.DataFlowKind.Value, + input_sockets: set[str] = set(), ## For now, presume + output_sockets: set[str] = set(), ## For now, presume + props: set[str] = set(), + managed_objs: set[str] = set(), + + req_params: set[str] = set() +): + def decorator(method: typ.Callable) -> typ.Callable: + # Check Function Signature Validity + func_sig = set(inspect.signature(method).parameters.keys()) + + ## Too Little + if func_sig != req_params and func_sig.issubset(req_params): + msg = f"Decorated method {method.__name__} is missing arguments {req_params - func_sig}" + + ## Too Much + if func_sig != req_params and func_sig.issuperset(req_params): + msg = f"Decorated method {method.__name__} has superfluous arguments {func_sig - req_params}" + raise ValueError(msg) + + ## Just Right :) + + # TODO: Check Function Annotation Validity + # - w/pydantic and/or socket capabilities + + def decorated(node: MaxwellSimNode): + # Assemble Keyword Arguments + method_kw_args = {} + + ## Add Input Sockets + if input_sockets: + _input_sockets = { + input_socket_name: node._compute_input(input_socket_name, kind) + for input_socket_name in input_sockets + } + method_kw_args |= dict(input_sockets=_input_sockets) + + ## Add Output Sockets + if output_sockets: + _output_sockets = { + output_socket_name: node.compute_output(output_socket_name, kind) + for output_socket_name in output_sockets + } + method_kw_args |= dict(output_sockets=_output_sockets) + + ## Add Props + if props: + _props = { + prop_name: getattr(node, prop_name) + for prop_name in props + } + method_kw_args |= dict(props=_props) + + ## Add Managed Object + if managed_objs: + _managed_objs = { + managed_obj_name: node.managed_objs[managed_obj_name] + for managed_obj_name in managed_objs + } + method_kw_args |= dict(managed_objs=_managed_objs) + + # Call Method + return method( + node, + **method_kw_args, + ) + + # Set Attributes for Discovery + decorated._callback_type = callback_type + if index_by: + decorated._index_by = index_by + if extra_data: + decorated._extra_data = extra_data + + return decorated + return decorator + + +#################### +# - Decorator: Output Socket +#################### +def computes_output_socket( + output_socket_name: ct.SocketName, + kind: ct.DataFlowKind = ct.DataFlowKind.Value, + input_sockets: set[str] = set(), + props: set[str] = set(), + managed_objs: set[str] = set(), + cacheable: bool = True, +): + """Given a socket name, defines a function-that-makes-a-function (aka. + decorator) which has the name of the socket attached. + + Must be used as a decorator, ex. `@compute_output_socket("name")`. + + Args: + output_socket_name: The name of the output socket to attach the + decorated method to. + input_sockets: The values of these input sockets will be computed + using `_compute_input`, then passed to the decorated function + as `input_sockets: list[Any]`. If the input socket doesn't exist (ex. is contained in an inactive loose socket or socket set), then None is returned. + managed_objs: These managed objects will be passed to the + function as `managed_objs: list[Any]`. + kind: Requests for this `output_socket_name, DataFlowKind` pair will + be returned by the decorated function. + cacheable: The output of th + be returned by the decorated function. + + Returns: + The decorator, which takes the output-socket-computing method + and returns a new output-socket-computing method, now annotated + and discoverable by the `MaxwellSimTreeNode`. + """ + req_params = {"self"} | ( + {"input_sockets"} if input_sockets else set() + ) | ( + {"props"} if props else set() + ) | ( + {"managed_objs"} if managed_objs else set() + ) + + return chain_event_decorator( + callback_type="computes_output_socket", + index_by=(output_socket_name, kind), + kind=kind, + input_sockets=input_sockets, + props=props, + managed_objs=managed_objs, + req_params=req_params, + ) + + + +#################### +# - Decorator: On Show Preview +#################### +def on_value_changed( + socket_name: ct.SocketName | None = None, + prop_name: str | None = None, + kind: ct.DataFlowKind = ct.DataFlowKind.Value, + input_sockets: set[str] = set(), + props: set[str] = set(), + managed_objs: set[str] = set(), +): + if socket_name is not None and prop_name is not None: + msg = "Either socket_name or prop_name, not both" + raise ValueError(msg) + + req_params = {"self"} | ( + {"input_sockets"} if input_sockets else set() + ) | ( + {"props"} if props else set() + ) | ( + {"managed_objs"} if managed_objs else set() + ) + + return chain_event_decorator( + callback_type="on_value_changed", + index_by=(socket_name, prop_name), + extra_data={ + "changed_socket": socket_name, + "changed_prop": prop_name, + }, + kind=kind, + input_sockets=input_sockets, + props=props, + managed_objs=managed_objs, + req_params=req_params, + ) + +def on_show_preview( + kind: ct.DataFlowKind = ct.DataFlowKind.Value, + input_sockets: set[str] = set(), ## For now, presume + output_sockets: set[str] = set(), ## For now, presume + props: set[str] = set(), + managed_objs: set[str] = set(), +): + req_params = {"self"} | ( + {"input_sockets"} if input_sockets else set() + ) | ( + {"output_sockets"} if output_sockets else set() + ) | ( + {"props"} if props else set() + ) | ( + {"managed_objs"} if managed_objs else set() + ) + + return chain_event_decorator( + callback_type="on_show_preview", + kind=kind, + input_sockets=input_sockets, + output_sockets=output_sockets, + props=props, + managed_objs=managed_objs, + req_params=req_params, + ) + +def on_show_plot( + kind: ct.DataFlowKind = ct.DataFlowKind.Value, + input_sockets: set[str] = set(), + output_sockets: set[str] = set(), + props: set[str] = set(), + managed_objs: set[str] = set(), + stop_propagation: bool = False, +): + req_params = {"self"} | ( + {"input_sockets"} if input_sockets else set() + ) | ( + {"output_sockets"} if output_sockets else set() + ) | ( + {"props"} if props else set() + ) | ( + {"managed_objs"} if managed_objs else set() + ) + + return chain_event_decorator( + callback_type="on_show_plot", + extra_data={ + "stop_propagation": stop_propagation, + }, + kind=kind, + input_sockets=input_sockets, + output_sockets=output_sockets, + props=props, + managed_objs=managed_objs, + req_params=req_params, + ) diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/__init__.py index d37b595..e8e2741 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/__init__.py @@ -1,20 +1,23 @@ -from . import unit_system +#from . import unit_system +from . import importers from . import constants -from . import lists -from . import scene +#from . import lists +#from . import scene BL_REGISTER = [ - *unit_system.BL_REGISTER, - - *scene.BL_REGISTER, + *importers.BL_REGISTER, +# *unit_system.BL_REGISTER, +# +# *scene.BL_REGISTER, *constants.BL_REGISTER, - *lists.BL_REGISTER, + #*lists.BL_REGISTER, ] BL_NODES = { - **unit_system.BL_NODES, - - **scene.BL_NODES, + **importers.BL_NODES, +# **unit_system.BL_NODES, +# +# **scene.BL_NODES, **constants.BL_NODES, - **lists.BL_NODES, +# **lists.BL_NODES, } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py index 760a729..1ef23e8 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py @@ -1,23 +1,23 @@ from . import wave_constant -from . import scientific_constant - -from . import number_constant -from . import physical_constant -from . import blender_constant +#from . import scientific_constant +# +#from . import number_constant +#from . import physical_constant +#from . import blender_constant BL_REGISTER = [ *wave_constant.BL_REGISTER, - *scientific_constant.BL_REGISTER, - - *number_constant.BL_REGISTER, - *physical_constant.BL_REGISTER, - *blender_constant.BL_REGISTER, +# *scientific_constant.BL_REGISTER, +# +# *number_constant.BL_REGISTER, +# *physical_constant.BL_REGISTER, +# *blender_constant.BL_REGISTER, ] BL_NODES = { **wave_constant.BL_NODES, - **scientific_constant.BL_NODES, - - **number_constant.BL_NODES, - **physical_constant.BL_NODES, - **blender_constant.BL_NODES, +# **scientific_constant.BL_NODES, +# +# **number_constant.BL_NODES, +# **physical_constant.BL_NODES, +# **blender_constant.BL_NODES, } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/wave_constant.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/wave_constant.py index 13cd8a5..2651f92 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/wave_constant.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/wave_constant.py @@ -3,75 +3,64 @@ import sympy as sp import sympy.physics.units as spu import scipy as sc -from .... import contracts +from .... import contracts as ct from .... import sockets from ... import base -vac_speed_of_light = ( +VAC_SPEED_OF_LIGHT = ( sc.constants.speed_of_light * spu.meter/spu.second ) -class WaveConstantNode(base.MaxwellSimTreeNode): - node_type = contracts.NodeType.WaveConstant +class WaveConstantNode(base.MaxwellSimNode): + node_type = ct.NodeType.WaveConstant bl_label = "Wave Constant" - input_sockets = {} input_socket_sets = { - "vac_wl": { - "vac_wl": sockets.PhysicalVacWLSocketDef( - label="Vac WL", - ), + "Vacuum WL": { + "WL": sockets.PhysicalLengthSocketDef(), }, - "freq": { - "freq": sockets.PhysicalFreqSocketDef( - label="Freq", - ), + "Frequency": { + "Freq": sockets.PhysicalFreqSocketDef(), }, } output_sockets = { - "vac_wl": sockets.PhysicalVacWLSocketDef( - label="Vac WL", - ), - "freq": sockets.PhysicalVacWLSocketDef( - label="Freq", - ), + "WL": sockets.PhysicalLengthSocketDef(), + "Freq": sockets.PhysicalFreqSocketDef(), } - output_socket_sets = {} #################### # - Callbacks #################### - @base.computes_output_socket("vac_wl") - def compute_vac_wl(self: contracts.NodeTypeProtocol) -> sp.Expr: - if self.socket_set == "vac_wl": - return self.compute_input("vac_wl") - - elif self.socket_set == "freq": - freq = self.compute_input("freq") + @base.computes_output_socket( + "WL", + kind=ct.DataFlowKind.Value, + input_sockets={"WL", "Freq"}, + ) + def compute_vac_wl(self, input_socket_values: dict) -> sp.Expr: + if (vac_wl := input_socket_values["WL"]): + return vac_wl + elif (freq := input_socket_values["Freq"]): return spu.convert_to( - vac_speed_of_light / freq, + VAC_SPEED_OF_LIGHT / freq, spu.meter, ) - raise ValueError("No valid socket set.") + raise RuntimeError("Vac WL and Freq are both non-truthy") - @base.computes_output_socket("freq") - def compute_freq(self: contracts.NodeTypeProtocol) -> sp.Expr: - if self.socket_set == "vac_wl": - vac_wl = self.compute_input("vac_wl") + @base.computes_output_socket( + "Freq", + input_sockets={"WL", "Freq"}, + ) + def compute_freq(self, input_sockets: dict) -> sp.Expr: + if (vac_wl := input_sockets["WL"]): return spu.convert_to( - vac_speed_of_light / vac_wl, + VAC_SPEED_OF_LIGHT / vac_wl, spu.hertz, ) - - elif self.socket_set == "freq": - return self.compute_input("freq") - - raise ValueError("No valid socket set.") - - + elif (freq := input_sockets["Freq"]): + return freq #################### # - Blender Registration @@ -80,7 +69,7 @@ BL_REGISTER = [ WaveConstantNode, ] BL_NODES = { - contracts.NodeType.WaveConstant: ( - contracts.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS + ct.NodeType.WaveConstant: ( + ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS ) } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/importers/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/importers/__init__.py new file mode 100644 index 0000000..374f8ca --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/importers/__init__.py @@ -0,0 +1,8 @@ +from . import tidy_3d_web_importer + +BL_REGISTER = [ + *tidy_3d_web_importer.BL_REGISTER, +] +BL_NODES = { + **tidy_3d_web_importer.BL_NODES, +} diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/importers/tidy_3d_web_importer.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/importers/tidy_3d_web_importer.py new file mode 100644 index 0000000..c8d7490 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/importers/tidy_3d_web_importer.py @@ -0,0 +1,105 @@ +import functools +import tempfile +from pathlib import Path +import typing as typ +from pathlib import Path + +import bpy +import sympy as sp +import pydantic as pyd +import tidy3d as td +import tidy3d.web as _td_web + +from ......utils.auth_td_web import g_td_web, is_td_web_authed +from .... import contracts as ct +from .... import sockets +from ... import base + +@functools.cache +def task_status(task_id: str): + task = _td_web.api.webapi.get_info(task_id) + return task.status + +#################### +# - Node +#################### +class Tidy3DWebImporterNode(base.MaxwellSimNode): + node_type = ct.NodeType.Tidy3DWebImporter + bl_label = "Tidy3DWebImporter" + + input_sockets = { + "Cloud Task": sockets.Tidy3DCloudTaskSocketDef( + task_exists=True, + ), + } + output_sockets = {} + + #################### + # - UI + #################### + def draw_info(self, context, layout): pass + + #################### + # - Output Methods + #################### + @base.computes_output_socket( + "FDTD Sim", + input_sockets={"Cloud Task"}, + ) + def compute_cloud_task(self, input_sockets: dict) -> str: + if not isinstance(task_id := input_sockets["Cloud Task"], str): + msg ="Input task does not exist" + raise ValueError(msg) + + # Load the Simulation + td_web = g_td_web(None) ## Presume already auth'ed + with tempfile.NamedTemporaryFile(delete=False) as f: + _path_tmp = Path(f.name) + _path_tmp.rename(f.name + ".json") + path_tmp = Path(f.name + ".json") + + cloud_sim = _td_web.api.webapi.load_simulation( + task_id, + path=str(path_tmp), + ) + Path(path_tmp).unlink() + + return cloud_sim + + #################### + # - Update + #################### + @base.on_value_changed( + socket_name="Cloud Task", + input_sockets={"Cloud Task"} + ) + def on_value_changed__cloud_task(self, input_sockets: dict): + task_status.cache_clear() + if ( + (task_id := input_sockets["Cloud Task"]) is None + or isinstance(task_id, dict) + or task_status(task_id) != "success" + or not is_td_web_authed + ): + if self.loose_output_sockets: self.loose_output_sockets = {} + return + + td_web = g_td_web(None) ## Presume already auth'ed + + self.loose_output_sockets = { + "FDTD Sim": sockets.MaxwellFDTDSimSocketDef(), + "FDTD Sim Data": sockets.AnySocketDef(), + } + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + Tidy3DWebImporterNode, +] +BL_NODES = { + ct.NodeType.Tidy3DWebImporter: ( + ct.NodeCategory.MAXWELLSIM_INPUTS_IMPORTERS + ) +} diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/kitchen_sink.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/kitchen_sink.py index fac1788..9885e85 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/kitchen_sink.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/kitchen_sink.py @@ -1,13 +1,15 @@ +from pathlib import Path + import tidy3d as td import sympy as sp import sympy.physics.units as spu -from .. import contracts +from .. import contracts as ct from .. import sockets from . import base -class KitchenSinkNode(base.MaxwellSimTreeNode): - node_type = contracts.NodeType.KitchenSink +class KitchenSinkNode(base.MaxwellSimNode): + node_type = ct.NodeType.KitchenSink bl_label = "Kitchen Sink" #bl_icon = ... @@ -15,75 +17,75 @@ class KitchenSinkNode(base.MaxwellSimTreeNode): #################### # - Sockets #################### - input_sockets = {} + input_sockets = { + "Static Data": sockets.AnySocketDef(), + } input_socket_sets = { - "basic": { - "basic_any": sockets.AnySocketDef(label="Any"), - "basic_bool": sockets.BoolSocketDef(label="Bool"), - "basic_filepath": sockets.FilePathSocketDef(label="FilePath"), - "basic_text": sockets.TextSocketDef(label="Text"), + "Basic": { + "Any": sockets.AnySocketDef(), + "Bool": sockets.BoolSocketDef(), + "FilePath": sockets.FilePathSocketDef(), + "Text": sockets.TextSocketDef(), }, - "number": { - "number_integer": sockets.IntegerNumberSocketDef(label="IntegerNumber"), - "number_rational": sockets.RationalNumberSocketDef(label="RationalNumber"), - "number_real": sockets.RealNumberSocketDef(label="RealNumber"), - "number_complex": sockets.ComplexNumberSocketDef(label="ComplexNumber"), + "Number": { + "Integer": sockets.IntegerNumberSocketDef(), + "Rational": sockets.RationalNumberSocketDef(), + "Real": sockets.RealNumberSocketDef(), + "Complex": sockets.ComplexNumberSocketDef(), }, - "vector": { - "vector_real2dvector": sockets.Real2DVectorSocketDef(label="Real2DVector"), - "vector_complex2dvector": sockets.Complex2DVectorSocketDef(label="Complex2DVector"), - "vector_real3dvector": sockets.Real3DVectorSocketDef(label="Real3DVector"), - "vector_complex3dvector": sockets.Complex3DVectorSocketDef(label="Complex3DVector"), + "Vector": { + "Real 2D": sockets.Real2DVectorSocketDef(), + "Real 3D": sockets.Real3DVectorSocketDef( + default_value=sp.Matrix([0.0, 0.0, 0.0]) + ), + "Complex 2D": sockets.Complex2DVectorSocketDef(), + "Complex 3D": sockets.Complex3DVectorSocketDef(), }, - "physical": { - "physical_time": sockets.PhysicalTimeSocketDef(label="PhysicalTime"), - #"physical_point_2d": sockets.PhysicalPoint2DSocketDef(label="PhysicalPoint2D"), - "physical_angle": sockets.PhysicalAngleSocketDef(label="PhysicalAngle"), - "physical_length": sockets.PhysicalLengthSocketDef(label="PhysicalLength"), - "physical_area": sockets.PhysicalAreaSocketDef(label="PhysicalArea"), - "physical_volume": sockets.PhysicalVolumeSocketDef(label="PhysicalVolume"), - "physical_point_3d": sockets.PhysicalPoint3DSocketDef(label="PhysicalPoint3D"), - #"physical_size_2d": sockets.PhysicalSize2DSocketDef(label="PhysicalSize2D"), - "physical_size_3d": sockets.PhysicalSize3DSocketDef(label="PhysicalSize3D"), - "physical_mass": sockets.PhysicalMassSocketDef(label="PhysicalMass"), - "physical_speed": sockets.PhysicalSpeedSocketDef(label="PhysicalSpeed"), - "physical_accel_scalar": sockets.PhysicalAccelScalarSocketDef(label="PhysicalAccelScalar"), - "physical_force_scalar": sockets.PhysicalForceScalarSocketDef(label="PhysicalForceScalar"), - #"physical_accel_3dvector": sockets.PhysicalAccel3DVectorSocketDef(label="PhysicalAccel3DVector"), - #"physical_force_3dvector": sockets.PhysicalForce3DVectorSocketDef(label="PhysicalForce3DVector"), - "physical_pol": sockets.PhysicalPolSocketDef(label="PhysicalPol"), - "physical_freq": sockets.PhysicalFreqSocketDef(label="PhysicalFreq"), - "physical_spec_power_dist": sockets.PhysicalSpecPowerDistSocketDef(label="PhysicalSpecPowerDist"), - "physical_spec_rel_perm_dist": sockets.PhysicalSpecRelPermDistSocketDef(label="PhysicalSpecRelPermDist"), + "Physical": { + "Time": sockets.PhysicalTimeSocketDef(), + #"physical_point_2d": sockets.PhysicalPoint2DSocketDef(), + "Angle": sockets.PhysicalAngleSocketDef(), + "Length": sockets.PhysicalLengthSocketDef(), + "Area": sockets.PhysicalAreaSocketDef(), + "Volume": sockets.PhysicalVolumeSocketDef(), + "Point 3D": sockets.PhysicalPoint3DSocketDef(), + ##"physical_size_2d": sockets.PhysicalSize2DSocketDef(), + "Size 3D": sockets.PhysicalSize3DSocketDef(), + "Mass": sockets.PhysicalMassSocketDef(), + "Speed": sockets.PhysicalSpeedSocketDef(), + "Accel Scalar": sockets.PhysicalAccelScalarSocketDef(), + "Force Scalar": sockets.PhysicalForceScalarSocketDef(), + #"physical_accel_3dvector": sockets.PhysicalAccel3DVectorSocketDef(), + ##"physical_force_3dvector": sockets.PhysicalForce3DVectorSocketDef(), + "Pol": sockets.PhysicalPolSocketDef(), + "Freq": sockets.PhysicalFreqSocketDef(), }, - "blender": { - "blender_object": sockets.BlenderObjectSocketDef(label="BlenderObject"), - "blender_collection": sockets.BlenderCollectionSocketDef(label="BlenderCollection"), - "blender_image": sockets.BlenderImageSocketDef(label="BlenderImage"), - "blender_volume": sockets.BlenderVolumeSocketDef(label="BlenderVolume"), - "blender_geonodes": sockets.BlenderGeoNodesSocketDef(label="BlenderGeoNodes"), - "blender_text": sockets.BlenderTextSocketDef(label="BlenderText"), + "Blender": { + "Object": sockets.BlenderObjectSocketDef(), + "Collection": sockets.BlenderCollectionSocketDef(), + "Image": sockets.BlenderImageSocketDef(), + "GeoNodes": sockets.BlenderGeoNodesSocketDef(), + "Text": sockets.BlenderTextSocketDef(), }, - "maxwell": { - "maxwell_source": sockets.MaxwellSourceSocketDef(label="MaxwellSource"), - "maxwell_temporal_shape": sockets.MaxwellTemporalShapeSocketDef(label="MaxwellTemporalShape"), - "maxwell_medium": sockets.MaxwellMediumSocketDef(label="MaxwellMedium"), - #"maxwell_medium_nonlinearity": sockets.MaxwellMediumNonLinearitySocketDef(label="MaxwellMediumNonLinearity"), - "maxwell_structure": sockets.MaxwellStructureSocketDef(label="MaxwellMedium"), - "maxwell_bound_box": sockets.MaxwellBoundBoxSocketDef(label="MaxwellBoundBox"), - "maxwell_bound_face": sockets.MaxwellBoundFaceSocketDef(label="MaxwellBoundFace"), - "maxwell_monitor": sockets.MaxwellMonitorSocketDef(label="MaxwellMonitor"), - "maxwell_fdtd_sim": sockets.MaxwellFDTDSimSocketDef(label="MaxwellFDTDSim"), - "maxwell_sim_grid": sockets.MaxwellSimGridSocketDef(label="MaxwellSimGrid"), - "maxwell_sim_grid_axis": sockets.MaxwellSimGridAxisSocketDef(label="MaxwellSimGridAxis"), + "Maxwell": { + "Source": sockets.MaxwellSourceSocketDef(), + "Temporal Shape": sockets.MaxwellTemporalShapeSocketDef(), + "Medium": sockets.MaxwellMediumSocketDef(), + "Medium Non-Linearity": sockets.MaxwellMediumNonLinearitySocketDef(), + "Structure": sockets.MaxwellStructureSocketDef(), + "Bound Box": sockets.MaxwellBoundBoxSocketDef(), + "Bound Face": sockets.MaxwellBoundFaceSocketDef(), + "Monitor": sockets.MaxwellMonitorSocketDef(), + "FDTD Sim": sockets.MaxwellFDTDSimSocketDef(), + "Sim Grid": sockets.MaxwellSimGridSocketDef(), + "Sim Grid Axis": sockets.MaxwellSimGridAxisSocketDef(), }, } - output_sockets = {} - output_socket_sets = { - k + " Output": v - for k, v in input_socket_sets.items() + output_sockets = { + "Static Data": sockets.AnySocketDef(), } + output_socket_sets = input_socket_sets @@ -94,7 +96,7 @@ BL_REGISTER = [ KitchenSinkNode, ] BL_NODES = { - contracts.NodeType.KitchenSink: ( - contracts.NodeCategory.MAXWELLSIM_INPUTS + ct.NodeType.KitchenSink: ( + ct.NodeCategory.MAXWELLSIM_INPUTS ) } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/__init__.py index 5576078..a4d181e 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/__init__.py @@ -1,47 +1,47 @@ from . import library_medium -from . import pec_medium -from . import isotropic_medium -from . import anisotropic_medium - -from . import triple_sellmeier_medium -from . import sellmeier_medium -from . import pole_residue_medium -from . import drude_medium -from . import drude_lorentz_medium -from . import debye_medium - -from . import non_linearities +#from . import pec_medium +#from . import isotropic_medium +#from . import anisotropic_medium +# +#from . import triple_sellmeier_medium +#from . import sellmeier_medium +#from . import pole_residue_medium +#from . import drude_medium +#from . import drude_lorentz_medium +#from . import debye_medium +# +#from . import non_linearities BL_REGISTER = [ *library_medium.BL_REGISTER, - *pec_medium.BL_REGISTER, - *isotropic_medium.BL_REGISTER, - *anisotropic_medium.BL_REGISTER, - - *triple_sellmeier_medium.BL_REGISTER, - *sellmeier_medium.BL_REGISTER, - *pole_residue_medium.BL_REGISTER, - *drude_medium.BL_REGISTER, - *drude_lorentz_medium.BL_REGISTER, - *debye_medium.BL_REGISTER, - - *non_linearities.BL_REGISTER, +# *pec_medium.BL_REGISTER, +# *isotropic_medium.BL_REGISTER, +# *anisotropic_medium.BL_REGISTER, +# +# *triple_sellmeier_medium.BL_REGISTER, +# *sellmeier_medium.BL_REGISTER, +# *pole_residue_medium.BL_REGISTER, +# *drude_medium.BL_REGISTER, +# *drude_lorentz_medium.BL_REGISTER, +# *debye_medium.BL_REGISTER, +# +# *non_linearities.BL_REGISTER, ] BL_NODES = { **library_medium.BL_NODES, - **pec_medium.BL_NODES, - **isotropic_medium.BL_NODES, - **anisotropic_medium.BL_NODES, - - **triple_sellmeier_medium.BL_NODES, - **sellmeier_medium.BL_NODES, - **pole_residue_medium.BL_NODES, - **drude_medium.BL_NODES, - **drude_lorentz_medium.BL_NODES, - **debye_medium.BL_NODES, - - **non_linearities.BL_NODES, +# **pec_medium.BL_NODES, +# **isotropic_medium.BL_NODES, +# **anisotropic_medium.BL_NODES, +# +# **triple_sellmeier_medium.BL_NODES, +# **sellmeier_medium.BL_NODES, +# **pole_residue_medium.BL_NODES, +# **drude_medium.BL_NODES, +# **drude_lorentz_medium.BL_NODES, +# **debye_medium.BL_NODES, +# +# **non_linearities.BL_NODES, } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/library_medium.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/library_medium.py index 86662f4..0d0a0c1 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/library_medium.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/library_medium.py @@ -1,3 +1,6 @@ +import typing as typ +import functools + import bpy import tidy3d as td import sympy as sp @@ -6,37 +9,33 @@ import numpy as np import scipy as sc from .....utils import extra_sympy_units as spuex -from ... import contracts +from ... import contracts as ct from ... import sockets +from ... import managed_objs from .. import base -class ExperimentOperator00(bpy.types.Operator): - bl_idname = "blender_maxwell.experiment_operator_00" - bl_label = "exp" +VAC_SPEED_OF_LIGHT = ( + sc.constants.speed_of_light + * spu.meter/spu.second +) - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - node = context.node - node.invoke_matplotlib_and_update_image() - return {'FINISHED'} - -class LibraryMediumNode(base.MaxwellSimTreeNode): - node_type = contracts.NodeType.LibraryMedium - +class LibraryMediumNode(base.MaxwellSimNode): + node_type = ct.NodeType.LibraryMedium bl_label = "Library Medium" - #bl_icon = ... #################### # - Sockets #################### input_sockets = {} output_sockets = { - "medium": sockets.MaxwellMediumSocketDef( - label="Medium" - ), + "Medium": sockets.MaxwellMediumSocketDef(), + } + + managed_obj_defs = { + "nk_plot": ct.schemas.ManagedObjDef( + mk=lambda name: managed_objs.ManagedBLImage(name), + name_prefix="nkplot_", + ) } #################### @@ -61,19 +60,12 @@ class LibraryMediumNode(base.MaxwellSimTreeNode): if mat_key != "graphene" ## For some reason, it's unique... ], default="Au", - update=(lambda self,context: self.update()), + update=(lambda self, context: self.sync_prop("material", context)), ) - #################### - # - UI - #################### - def draw_props(self, context, layout): - layout.prop(self, "material", text="") - - def draw_info(self, context, layout): - layout.operator(ExperimentOperator00.bl_idname, text="Experiment") - vac_speed_of_light = sc.constants.speed_of_light * spu.meter/spu.second - + @property + def freq_range_str(self) -> tuple[sp.Expr, sp.Expr]: + ## TODO: Cache (node instances don't seem able to keep data outside of properties, not even cached_property) mat = td.material_library[self.material] freq_range = [ spu.convert_to( @@ -82,80 +74,89 @@ class LibraryMediumNode(base.MaxwellSimTreeNode): ) / spuex.terahertz for val in mat.medium.frequency_range ] + return sp.pretty( + [freq_range[0].n(4), freq_range[1].n(4)], + use_unicode=True + ) + + @property + def nm_range_str(self) -> str: + ## TODO: Cache (node instances don't seem able to keep data outside of properties, not even cached_property) + mat = td.material_library[self.material] nm_range = [ spu.convert_to( - vac_speed_of_light / (val * spu.hertz), + VAC_SPEED_OF_LIGHT / (val * spu.hertz), spu.nanometer, ) / spu.nanometer - for val in mat.medium.frequency_range + for val in reversed(mat.medium.frequency_range) ] - - layout.label(text=f"nm: [{nm_range[1].n(2)}, {nm_range[0].n(2)}]") - layout.label(text=f"THz: [{freq_range[0].n(2)}, {freq_range[1].n(2)}]") + return sp.pretty( + [nm_range[0].n(4), nm_range[1].n(4)], + use_unicode=True + ) #################### - # - Output Socket Computation + # - UI #################### - @base.computes_output_socket("medium") - def compute_medium(self: contracts.NodeTypeProtocol) -> td.AbstractMedium: + def draw_props(self, context, layout): + layout.prop(self, "material", text="") + + def draw_info(self, context, col): + # UI Drawing + split = col.split(factor=0.23, align=True) + + _col = split.column(align=True) + _col.alignment = "LEFT" + _col.label(text="nm") + _col.label(text="THz") + + _col = split.column(align=True) + _col.alignment = "RIGHT" + _col.label(text=self.nm_range_str) + _col.label(text=self.freq_range_str) + + #################### + # - Output Sockets + #################### + @base.computes_output_socket("Medium") + def compute_vac_wl(self) -> sp.Expr: return td.material_library[self.material].medium #################### - # - Experiment + # - Event Callbacks #################### - def invoke_matplotlib_and_update_image(self): - import matplotlib.pyplot as plt - mat = td.material_library[self.material] + @base.on_show_plot( + managed_objs={"nk_plot"}, + props={"material"}, + stop_propagation=True, ## Plot only the first plottable node + ) + def on_show_plot( + self, + managed_objs: dict[str, ct.schemas.ManagedObj], + props: dict[str, typ.Any], + ): + medium = td.material_library[props["material"]].medium + freq_range = [ + spu.convert_to( + val * spu.hertz, + spuex.terahertz, + ) / spu.hertz + for val in medium.frequency_range + ] - aspect_ratio = 1.0 - for area in bpy.context.screen.areas: - if area.type == 'IMAGE_EDITOR': - width = area.width - height = area.height - aspect_ratio = width / height - - # Generate a plot with matplotlib - fig_width = 6 - fig_height = fig_width / aspect_ratio - fig, ax = plt.subplots(figsize=(fig_width, fig_height)) - ax.set_aspect(aspect_ratio) - mat.medium.plot( - np.linspace(*mat.medium.frequency_range[:2], 50), - ax=ax, + managed_objs["nk_plot"].mpl_plot_to_image( + lambda ax: medium.plot(medium.frequency_range, ax=ax), + bl_select=True, ) - - # Save the plot to a temporary file - temp_plot_file = bpy.path.abspath('//temp_plot.png') - fig.savefig(temp_plot_file, bbox_inches='tight') - plt.close(fig) # Close the figure to free up memory - - # Load or reload the image in Blender - if "matplotlib_plot" in bpy.data.images: - image = bpy.data.images["matplotlib_plot"] - image.reload() - else: - image = bpy.data.images.load(temp_plot_file) - image.name = "matplotlib_plot" - - # Write the plot to an image datablock in Blender - for area in bpy.context.screen.areas: - if area.type == 'IMAGE_EDITOR': - for space in area.spaces: - if space.type == 'IMAGE_EDITOR': - space.image = image - return True - - #################### # - Blender Registration #################### BL_REGISTER = [ - ExperimentOperator00, LibraryMediumNode, ] BL_NODES = { - contracts.NodeType.LibraryMedium: ( - contracts.NodeCategory.MAXWELLSIM_MEDIUMS + ct.NodeType.LibraryMedium: ( + ct.NodeCategory.MAXWELLSIM_MEDIUMS ) } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/__init__.py index e47bc45..5234037 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/__init__.py @@ -1,10 +1,11 @@ -from . import viewers +from . import viewer from . import exporters -from . import plotters BL_REGISTER = [ + *viewer.BL_REGISTER, *exporters.BL_REGISTER, ] BL_NODES = { + **viewer.BL_NODES, **exporters.BL_NODES, } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/__init__.py index 7f2f7b3..fcf9b95 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/__init__.py @@ -1,8 +1,11 @@ from . import json_file_exporter +from . import tidy3d_web_exporter BL_REGISTER = [ *json_file_exporter.BL_REGISTER, + *tidy3d_web_exporter.BL_REGISTER, ] BL_NODES = { **json_file_exporter.BL_NODES, + **tidy3d_web_exporter.BL_NODES, } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/json_file_exporter.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/json_file_exporter.py index 1c2ab5b..4c5bed2 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/json_file_exporter.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/json_file_exporter.py @@ -7,39 +7,13 @@ import sympy as sp import pydantic as pyd import tidy3d as td -from .... import contracts +from .... import contracts as ct from .... import sockets from ... import base #################### # - Operators #################### -class JSONFileExporterPrintJSON(bpy.types.Operator): - bl_idname = "blender_maxwell.json_file_exporter_print_json" - bl_label = "Print the JSON of what's linked into a JSONFileExporterNode." - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - node = context.node - print(node.linked_data_as_json()) - return {'FINISHED'} - -class JSONFileExporterMeshData(bpy.types.Operator): - bl_idname = "blender_maxwell.json_file_exporter_mesh_data" - bl_label = "Print any mesh data linked into a JSONFileExporterNode." - - @classmethod - def poll(cls, context): - return True - - def execute(self, context): - node = context.node - print(node.linked_mesh_data()) - return {'FINISHED'} - class JSONFileExporterSaveJSON(bpy.types.Operator): bl_idname = "blender_maxwell.json_file_exporter_save_json" bl_label = "Save the JSON of what's linked into a JSONFileExporterNode." @@ -56,22 +30,24 @@ class JSONFileExporterSaveJSON(bpy.types.Operator): #################### # - Node #################### -class JSONFileExporterNode(base.MaxwellSimTreeNode): - node_type = contracts.NodeType.JSONFileExporter +class JSONFileExporterNode(base.MaxwellSimNode): + node_type = ct.NodeType.JSONFileExporter bl_label = "JSON File Exporter" #bl_icon = constants.ICON_SIM_INPUT input_sockets = { - "json_path": sockets.FilePathSocketDef( - label="JSON Path", - default_path="simulation.json" + "Data": sockets.AnySocketDef(), + "JSON Path": sockets.FilePathSocketDef( + default_path=Path("simulation.json") ), - "data": sockets.AnySocketDef( - label="Data", + "JSON Indent": sockets.IntegerNumberSocketDef( + default_value=4, ), } - output_sockets = {} + output_sockets = { + "JSON String": sockets.TextSocketDef(), + } #################### # - UI Layout @@ -81,53 +57,50 @@ class JSONFileExporterNode(base.MaxwellSimTreeNode): context: bpy.types.Context, layout: bpy.types.UILayout, ) -> None: - layout.operator(JSONFileExporterPrintJSON.bl_idname, text="Print") - layout.operator(JSONFileExporterSaveJSON.bl_idname, text="Save") - layout.operator(JSONFileExporterMeshData.bl_idname, text="Mesh Info") + layout.operator(JSONFileExporterSaveJSON.bl_idname, text="Save JSON") #################### # - Methods #################### - def linked_data_as_json(self) -> str | None: - if self.g_input_bl_socket("data").is_linked: - data: typ.Any = self.compute_input("data") - - # Tidy3D Objects: Call .json() - if hasattr(data, "json"): - return data.json() - - # Pydantic Models: Call .model_dump_json() - elif isinstance(data, pyd.BaseModel): - return data.model_dump_json() - - else: - json.dumps(data) - - def linked_mesh_data(self) -> str | None: - if self.g_input_bl_socket("data").is_linked: - data: typ.Any = self.compute_input("data") - - if isinstance(data, td.Structure): - return data.geometry - def export_data_as_json(self) -> None: - if (data := self.linked_data_as_json()): - data_dict = json.loads(data) - with self.compute_input("json_path").open("w") as f: - json.dump(data_dict, f, ensure_ascii=False, indent=4) + if (json_str := self.compute_output("JSON String")): + data_dict = json.loads(json_str) + with self._compute_input("JSON Path").open("w") as f: + indent = self._compute_input("JSON Indent") + json.dump(data_dict, f, ensure_ascii=False, indent=indent) + + #################### + # - Output Sockets + #################### + @base.computes_output_socket( + "JSON String", + input_sockets={"Data"}, + ) + def compute_json_string(self, input_sockets: dict[str, typ.Any]) -> str | None: + if not (data := input_sockets["Data"]): + return None + + # Tidy3D Objects: Call .json() + if hasattr(data, "json"): + return data.json() + + # Pydantic Models: Call .model_dump_json() + elif isinstance(data, pyd.BaseModel): + return data.model_dump_json() + + else: + json.dumps(data) #################### # - Blender Registration #################### BL_REGISTER = [ - JSONFileExporterPrintJSON, - JSONFileExporterMeshData, JSONFileExporterSaveJSON, JSONFileExporterNode, ] BL_NODES = { - contracts.NodeType.JSONFileExporter: ( - contracts.NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS + ct.NodeType.JSONFileExporter: ( + ct.NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS ) } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/tidy3d_web_exporter.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/tidy3d_web_exporter.py new file mode 100644 index 0000000..fffd368 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/tidy3d_web_exporter.py @@ -0,0 +1,393 @@ +import json +import tempfile +import functools +import typing as typ +import json +from pathlib import Path + +import bpy +import sympy as sp +import pydantic as pyd +import tidy3d as td +import tidy3d.web as _td_web + +from ......utils.auth_td_web import g_td_web, is_td_web_authed +from .... import contracts as ct +from .... import sockets +from ... import base + +#################### +# - Task Getters +#################### +## TODO: We should probably refactor this setup. +@functools.cache +def estimated_task_cost(task_id: str): + return _td_web.api.webapi.estimate_cost(task_id) + +@functools.cache +def billed_task_cost(task_id: str): + return _td_web.api.webapi.real_cost(task_id) + +@functools.cache +def task_status(task_id: str): + task = _td_web.api.webapi.get_info(task_id) + return task.status + +#################### +# - Progress Timer +#################### +## TODO: We should probably refactor this too. +class Tidy3DTaskStatusModalOperator(bpy.types.Operator): + bl_idname = "blender_maxwell.tidy_3d_task_status_modal_operator" + bl_label = "Tidy3D Task Status Modal Operator" + + _timer = None + _task_id = None + _node = None + _status = None + _reported_done = False + + def modal(self, context, event): + # Retrieve New Status + task_status.cache_clear() + new_status = task_status(self._task_id) + if new_status != self._status: + task_status.cache_clear() + self._status = new_status + + # Check Done Status + if self._status in {"success", "error"}: + # Report Done + if not self._reported_done: + self._node.trigger_action("value_changed") + self._reported_done = True + + # Finish when Billing is Known + if not billed_task_cost(self._task_id): + billed_task_cost.cache_clear() + else: + return {'FINISHED'} + + return {'PASS_THROUGH'} + + def execute(self, context): + node = context.node + wm = context.window_manager + + self._timer = wm.event_timer_add(0.25, window=context.window) + self._task_id = node.uploaded_task_id + self._node = node + self._status = task_status(self._task_id) + + wm.modal_handler_add(self) + return {'RUNNING_MODAL'} + +#################### +# - Web Uploader / Loader / Runner / Releaser +#################### +## TODO: We should probably refactor this too. +class Tidy3DWebUploadOperator(bpy.types.Operator): + bl_idname = "blender_maxwell.tidy_3d_web_upload_operator" + bl_label = "Tidy3D Web Upload Operator" + bl_description = "Upload the attached (locked) simulation, such that it is ready to run on the Tidy3D cloud" + + @classmethod + def poll(cls, context): + space = context.space_data + return ( + space.type == 'NODE_EDITOR' + and space.node_tree is not None + and space.node_tree.bl_idname == "MaxwellSimTreeType" + and is_td_web_authed() + and hasattr(context, "node") + and context.node.lock_tree + ) + + def execute(self, context): + node = context.node + node.web_upload() + return {'FINISHED'} + +class Tidy3DLoadUploadedOperator(bpy.types.Operator): + bl_idname = "blender_maxwell.tidy_3d_load_uploaded_operator" + bl_label = "Tidy3D Load Uploaded Operator" + bl_description = "Load an already-uploaded simulation, as selected in the dropdown of the 'Cloud Task' socket" + + @classmethod + def poll(cls, context): + space = context.space_data + return ( + space.type == 'NODE_EDITOR' + and space.node_tree is not None + and space.node_tree.bl_idname == "MaxwellSimTreeType" + and is_td_web_authed() + and hasattr(context, "node") + and context.node.lock_tree + ) + + def execute(self, context): + node = context.node + node.load_uploaded_task() + + # Load Simulation to Compare + ## Load Local Sim + local_sim = node._compute_input("FDTD Sim") + + ## Load Cloud Sim + task_id = node.compute_output("Cloud Task") + with tempfile.NamedTemporaryFile(delete=False) as f: + _path_tmp = Path(f.name) + _path_tmp.rename(f.name + ".json") + path_tmp = Path(f.name + ".json") + cloud_sim = _td_web.api.webapi.load_simulation(task_id, path=str(path_tmp)) + + Path(path_tmp).unlink() + + ## Compare + if local_sim != cloud_sim: + node.release_uploaded_task() + msg = "Loaded simulation doesn't match input simulation" + raise ValueError(msg) + + return {'FINISHED'} + +class RunUploadedTidy3DSim(bpy.types.Operator): + bl_idname = "blender_maxwell.run_uploaded_tidy_3d_sim" + bl_label = "Run Uploaded Tidy3D Sim" + bl_description = "Run the currently uploaded (and loaded) simulation" + + @classmethod + def poll(cls, context): + space = context.space_data + return ( + space.type == 'NODE_EDITOR' + and space.node_tree is not None + and space.node_tree.bl_idname == "MaxwellSimTreeType" + and is_td_web_authed() + and hasattr(context, "node") + and context.node.lock_tree + and context.node.uploaded_task_id + and task_status(context.node.uploaded_task_id) == "draft" + ) + + def execute(self, context): + node = context.node + node.run_uploaded_task() + bpy.ops.blender_maxwell.tidy_3d_task_status_modal_operator() + return {'FINISHED'} + +class ReleaseTidy3DExportOperator(bpy.types.Operator): + bl_idname = "blender_maxwell.release_tidy_3d_export_operator" + bl_label = "Release Tidy3D Export Operator" + + @classmethod + def poll(cls, context): + space = context.space_data + return ( + space.type == 'NODE_EDITOR' + and space.node_tree is not None + and space.node_tree.bl_idname == "MaxwellSimTreeType" + and is_td_web_authed() + and hasattr(context, "node") + and context.node.lock_tree + and context.node.uploaded_task_id + ) + + def execute(self, context): + node = context.node + node.release_uploaded_task() + return {'FINISHED'} + + + +#################### +# - Web Exporter Node +#################### +class Tidy3DWebExporterNode(base.MaxwellSimNode): + node_type = ct.NodeType.Tidy3DWebExporter + bl_label = "Tidy3DWebExporter" + + input_sockets = { + "FDTD Sim": sockets.MaxwellFDTDSimSocketDef(), + "Cloud Task": sockets.Tidy3DCloudTaskSocketDef( + task_exists=False, + ), + } + output_sockets = { + "Cloud Task": sockets.Tidy3DCloudTaskSocketDef( + task_exists=True, + ), + } + + lock_tree: bpy.props.BoolProperty( + name="Whether to lock the attached tree", + description="Whether or not to lock the attached tree", + default=False, + update=(lambda self, context: self.sync_lock_tree(context)), + ) + uploaded_task_id: bpy.props.StringProperty( + name="Uploaded Task ID", + description="The uploaded task ID", + default="", + ) + + #################### + # - Sync Methods + #################### + def sync_lock_tree(self, context): + node_tree = self.id_data + + if self.lock_tree: + self.trigger_action("enable_lock") + self.locked = False + for bl_socket in self.inputs: + if bl_socket.name == "FDTD Sim": continue + bl_socket.locked = False + + else: + self.trigger_action("disable_lock") + + #################### + # - Output Socket Callbacks + #################### + def web_upload(self): + if not (sim := self._compute_input("FDTD Sim")): + raise ValueError("Must attach simulation") + + if not (new_task_dict := self._compute_input("Cloud Task")): + raise ValueError("No valid cloud task defined") + + td_web = g_td_web(None) ## Presume already auth'ed + + self.uploaded_task_id = td_web.api.webapi.upload( + sim, + **new_task_dict, + verbose=True, + ) + + self.inputs["Cloud Task"].sync_task_loaded(self.uploaded_task_id) + + def load_uploaded_task(self): + self.inputs["Cloud Task"].sync_task_loaded(None) + self.uploaded_task_id = self._compute_input("Cloud Task") + + self.trigger_action("value_changed") + + def run_uploaded_task(self): + td_web = g_td_web(None) ## Presume already auth'ed + td_web.api.webapi.start(self.uploaded_task_id) + + self.trigger_action("value_changed") + + def release_uploaded_task(self): + self.uploaded_task_id = "" + self.inputs["Cloud Task"].sync_task_released(specify_new_task=True) + + self.trigger_action("value_changed") + + #################### + # - UI + #################### + def draw_operators(self, context, layout): + is_authed = is_td_web_authed() + has_uploaded_task_id = bool(self.uploaded_task_id) + + # Row: Run Simulation + row = layout.row(align=True) + if has_uploaded_task_id: row.enabled = False + row.operator( + Tidy3DWebUploadOperator.bl_idname, + text="Upload Sim", + ) + tree_lock_icon = "LOCKED" if self.lock_tree else "UNLOCKED" + row.prop(self, "lock_tree", toggle=True, icon=tree_lock_icon, text="") + + # Row: Run Simulation + row = layout.row(align=True) + if is_authed and has_uploaded_task_id: + run_sim_text = f"Run Sim (~{estimated_task_cost(self.uploaded_task_id):.3f} credits)" + else: + run_sim_text = f"Run Sim" + + row.operator( + RunUploadedTidy3DSim.bl_idname, + text=run_sim_text, + ) + if has_uploaded_task_id: + tree_lock_icon = "LOOP_BACK" + row.operator( + ReleaseTidy3DExportOperator.bl_idname, + icon="LOOP_BACK", + text="", + ) + else: + row.operator( + Tidy3DLoadUploadedOperator.bl_idname, + icon="TRIA_UP_BAR", + text="", + ) + + # Row: Simulation Progress + if is_authed and has_uploaded_task_id: + progress = { + "draft": (0.0, "Waiting to Run..."), + "initialized": (0.0, "Initializing..."), + "queued": (0.0, "Queued..."), + "preprocessing": (0.05, "Pre-processing..."), + "running": (0.2, "Running..."), + "postprocessing": (0.85, "Post-processing..."), + "success": (1.0, f"Success (={billed_task_cost(self.uploaded_task_id)} credits)"), + "error": (1.0, f"Error (={billed_task_cost(self.uploaded_task_id)} credits)"), + }[task_status(self.uploaded_task_id)] + + layout.separator() + row = layout.row(align=True) + row.progress( + factor=progress[0], + type="BAR", + text=progress[1], + ) + + #################### + # - Output Methods + #################### + @base.computes_output_socket( + "Cloud Task", + input_sockets={"Cloud Task"}, + ) + def compute_cloud_task(self, input_sockets: dict) -> str | None: + if self.uploaded_task_id: return self.uploaded_task_id + return None + + #################### + # - Update + #################### + @base.on_value_changed(socket_name="FDTD Sim") + def on_value_changed__fdtd_sim(self): + estimated_task_cost.cache_clear() + task_status.cache_clear() + billed_task_cost.cache_clear() + + @base.on_value_changed(socket_name="Cloud Task") + def on_value_changed__cloud_task(self): + estimated_task_cost.cache_clear() + task_status.cache_clear() + billed_task_cost.cache_clear() + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + Tidy3DWebUploadOperator, + Tidy3DTaskStatusModalOperator, + RunUploadedTidy3DSim, + Tidy3DLoadUploadedOperator, + ReleaseTidy3DExportOperator, + Tidy3DWebExporterNode, +] +BL_NODES = { + ct.NodeType.Tidy3DWebExporter: ( + ct.NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS + ) +} diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/plotters/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/plotters/__init__.py deleted file mode 100644 index 9118eb8..0000000 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/plotters/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -BL_REGISTER = [] -BL_NODES = {} diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py new file mode 100644 index 0000000..38944f8 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py @@ -0,0 +1,97 @@ +import functools +import typing as typ +import json +from pathlib import Path + +import bpy +import sympy as sp +import pydantic as pyd +import tidy3d as td + +from ... import contracts as ct +from ... import sockets +from .. import base + + +class ConsoleViewOperator(bpy.types.Operator): + bl_idname = "blender_maxwell.console_view_operator" + bl_label = "View Plots" + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + node = context.node + node.print_data_to_console() + return {'FINISHED'} + +class RefreshPlotViewOperator(bpy.types.Operator): + bl_idname = "blender_maxwell.refresh_plot_view_operator" + bl_label = "Refresh Plots" + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + node = context.node + node.trigger_action("value_changed", "Data") + return {'FINISHED'} + +#################### +# - Node +#################### +class ViewerNode(base.MaxwellSimNode): + node_type = ct.NodeType.Viewer + bl_label = "Viewer" + + input_sockets = { + "Data": sockets.AnySocketDef(), + } + + #################### + # - UI + #################### + def draw_operators(self, context, layout): + row = layout.row(align=True) + row.label(text="Console") + row.operator(ConsoleViewOperator.bl_idname, text="Print") + + row = layout.row(align=True) + row.label(text="Plot") + row.operator(RefreshPlotViewOperator.bl_idname, text="", icon="FILE_REFRESH") + + #################### + # - Methods + #################### + def print_data_to_console(self): + if not (data := self._compute_input("Data")): + return + + if isinstance(data, sp.Basic): + sp.pprint(data, use_unicode=True) + + print(str(data)) + + #################### + # - Update + #################### + @base.on_value_changed(socket_name="Data") + def on_value_changed__data(self): + self.trigger_action("show_plot") + + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + ConsoleViewOperator, + RefreshPlotViewOperator, + ViewerNode, +] +BL_NODES = { + ct.NodeType.Viewer: ( + ct.NodeCategory.MAXWELLSIM_OUTPUTS + ) +} diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/__init__.py deleted file mode 100644 index 23d3fcc..0000000 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from . import viewer_3d -from . import value_viewer -from . import console_viewer - -BL_REGISTER = [ - *viewer_3d.BL_REGISTER, - *value_viewer.BL_REGISTER, - *console_viewer.BL_REGISTER, -] -BL_NODES = { - **viewer_3d.BL_NODES, - **value_viewer.BL_NODES, - **console_viewer.BL_NODES, -} diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/console_viewer.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/console_viewer.py deleted file mode 100644 index 8f4a665..0000000 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/console_viewer.py +++ /dev/null @@ -1,6 +0,0 @@ -#################### -# - Blender Registration -#################### -BL_REGISTER = [] -BL_NODES = {} - diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/value_viewer.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/value_viewer.py deleted file mode 100644 index 8f4a665..0000000 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/value_viewer.py +++ /dev/null @@ -1,6 +0,0 @@ -#################### -# - Blender Registration -#################### -BL_REGISTER = [] -BL_NODES = {} - diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/viewer_3d.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/viewer_3d.py deleted file mode 100644 index 8b1ab2c..0000000 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewers/viewer_3d.py +++ /dev/null @@ -1,50 +0,0 @@ -import typing as typ -import json -from pathlib import Path - -import bpy -import sympy as sp -import pydantic as pyd -import tidy3d as td - -from .... import contracts -from .... import sockets -from ... import base - -INTERNAL_GEONODES = { - -} - -#################### -# - Node -#################### -class Viewer3DNode(base.MaxwellSimTreeNode): - node_type = contracts.NodeType.Viewer3D - - bl_label = "3D Viewer" - - input_sockets = { - "data": sockets.AnySocketDef( - label="Data", - ), - } - output_sockets = {} - - #################### - # - Update - #################### - def update_cb(self): - pass - - -#################### -# - Blender Registration -#################### -BL_REGISTER = [ - Viewer3DNode, -] -BL_NODES = { - contracts.NodeType.Viewer3D: ( - contracts.NodeCategory.MAXWELLSIM_OUTPUTS_VIEWERS - ) -} diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/__init__.py index 24da237..fbb823c 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/__init__.py @@ -1,15 +1,19 @@ -from . import sim_grid -from . import sim_grid_axes +from . import sim_domain + +#from . import sim_grid +#from . import sim_grid_axes from . import fdtd_sim BL_REGISTER = [ - *sim_grid.BL_REGISTER, - *sim_grid_axes.BL_REGISTER, + *sim_domain.BL_REGISTER, +# *sim_grid.BL_REGISTER, +# *sim_grid_axes.BL_REGISTER, *fdtd_sim.BL_REGISTER, ] BL_NODES = { - **sim_grid.BL_NODES, - **sim_grid_axes.BL_NODES, + **sim_domain.BL_NODES, +# **sim_grid.BL_NODES, +# **sim_grid_axes.BL_NODES, **fdtd_sim.BL_NODES, } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/fdtd_sim.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/fdtd_sim.py index 10a9d57..340ced6 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/fdtd_sim.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/fdtd_sim.py @@ -2,71 +2,57 @@ import tidy3d as td import sympy as sp import sympy.physics.units as spu -from ... import contracts +from ... import contracts as ct from ... import sockets from .. import base -class FDTDSimNode(base.MaxwellSimTreeNode): - node_type = contracts.NodeType.FDTDSim - +class FDTDSimNode(base.MaxwellSimNode): + node_type = ct.NodeType.FDTDSim bl_label = "FDTD Simulation" - #bl_icon = ... #################### # - Sockets #################### input_sockets = { - "run_time": sockets.PhysicalTimeSocketDef( - label="Run Time", - ), - "domain": sockets.PhysicalSize3DSocketDef( - label="Domain", - ), - "ambient_medium": sockets.MaxwellMediumSocketDef( - label="Ambient Medium", - ), - "source": sockets.MaxwellSourceSocketDef( - label="Source", - ), - "structure": sockets.MaxwellStructureSocketDef( - label="Structure", - ), - "bound": sockets.MaxwellBoundBoxSocketDef( - label="Bound", - ), + "Domain": sockets.MaxwellSimDomainSocketDef(), + "BCs": sockets.MaxwellBoundBoxSocketDef(), + "Sources": sockets.MaxwellSourceSocketDef(), + "Structures": sockets.MaxwellStructureSocketDef(), + "Monitors": sockets.MaxwellMonitorSocketDef(), } output_sockets = { - "fdtd_sim": sockets.MaxwellFDTDSimSocketDef( - label="FDTD Sim", - ), + "FDTD Sim": sockets.MaxwellFDTDSimSocketDef(), } #################### # - Output Socket Computation #################### - @base.computes_output_socket("fdtd_sim") - def compute_simulation(self: contracts.NodeTypeProtocol) -> td.Simulation: - _run_time = self.compute_input("run_time") - _domain = self.compute_input("domain") - ambient_medium = self.compute_input("ambient_medium") - structures = [self.compute_input("structure")] - sources = [self.compute_input("source")] - bound = self.compute_input("bound") + @base.computes_output_socket( + "FDTD Sim", + kind=ct.DataFlowKind.Value, + input_sockets={ + "Sources", "Structures", "Domain", "BCs", "Monitors" + }, + ) + def compute_fdtd_sim(self, input_sockets: dict) -> sp.Expr: + sim_domain = input_sockets["Domain"] + sources = input_sockets["Sources"] + structures = input_sockets["Structures"] + bounds = input_sockets["BCs"] + monitors = input_sockets["Monitors"] - run_time = spu.convert_to(_run_time, spu.second) / spu.second - domain = tuple(spu.convert_to(_domain, spu.um) / spu.um) + if not isinstance(sources, list): + sources = [sources] + if not isinstance(structures, list): + structures = [structures] return td.Simulation( - size=domain, - medium=ambient_medium, + **sim_domain, ## run_time=, size=, grid=, medium= structures=structures, sources=sources, - boundary_spec=bound, - run_time=run_time, + boundary_spec=bounds, ) - - #################### # - Blender Registration #################### @@ -74,7 +60,7 @@ BL_REGISTER = [ FDTDSimNode, ] BL_NODES = { - contracts.NodeType.FDTDSim: ( - contracts.NodeCategory.MAXWELLSIM_SIMS + ct.NodeType.FDTDSim: ( + ct.NodeCategory.MAXWELLSIM_SIMS ) } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py new file mode 100644 index 0000000..3754017 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py @@ -0,0 +1,60 @@ +import bpy +import sympy as sp +import sympy.physics.units as spu +import scipy as sc + +from ... import contracts as ct +from ... import sockets +from .. import base + +class SimDomainNode(base.MaxwellSimNode): + node_type = ct.NodeType.SimDomain + bl_label = "Sim Domain" + + input_sockets = { + "Duration": sockets.PhysicalTimeSocketDef( + default_value = 5 * spu.ps, + default_unit = spu.ps, + ), + "Size": sockets.PhysicalSize3DSocketDef(), + "Grid": sockets.MaxwellSimGridSocketDef(), + "Ambient Medium": sockets.MaxwellMediumSocketDef(), + } + output_sockets = { + "Domain": sockets.MaxwellSimDomainSocketDef(), + } + + #################### + # - Callbacks + #################### + @base.computes_output_socket( + "Domain", + input_sockets={"Duration", "Size", "Grid", "Ambient Medium"}, + ) + def compute_sim_domain(self, input_sockets: dict) -> sp.Expr: + if all([ + (_duration := input_sockets["Duration"]), + (_size := input_sockets["Size"]), + (grid := input_sockets["Grid"]), + (medium := input_sockets["Ambient Medium"]), + ]): + duration = spu.convert_to(_duration, spu.second) / spu.second + size = tuple(spu.convert_to(_size, spu.um) / spu.um) + return dict( + run_time=duration, + size=size, + grid_spec=grid, + medium=medium, + ) + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + SimDomainNode, +] +BL_NODES = { + ct.NodeType.SimDomain: ( + ct.NodeCategory.MAXWELLSIM_SIMS + ) +} diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/__init__.py index 1a3fd4d..0555c9f 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/__init__.py @@ -1,27 +1,27 @@ from . import temporal_shapes from . import point_dipole_source -from . import uniform_current_source +#from . import uniform_current_source from . import plane_wave_source -from . import gaussian_beam_source -from . import astigmatic_gaussian_beam_source -from . import tfsf_source +#from . import gaussian_beam_source +#from . import astigmatic_gaussian_beam_source +#from . import tfsf_source BL_REGISTER = [ *temporal_shapes.BL_REGISTER, *point_dipole_source.BL_REGISTER, - *uniform_current_source.BL_REGISTER, +# *uniform_current_source.BL_REGISTER, *plane_wave_source.BL_REGISTER, - *gaussian_beam_source.BL_REGISTER, - *astigmatic_gaussian_beam_source.BL_REGISTER, - *tfsf_source.BL_REGISTER, +# *gaussian_beam_source.BL_REGISTER, +# *astigmatic_gaussian_beam_source.BL_REGISTER, +# *tfsf_source.BL_REGISTER, ] BL_NODES = { **temporal_shapes.BL_NODES, **point_dipole_source.BL_NODES, - **uniform_current_source.BL_NODES, +# **uniform_current_source.BL_NODES, **plane_wave_source.BL_NODES, - **gaussian_beam_source.BL_NODES, - **astigmatic_gaussian_beam_source.BL_NODES, - **tfsf_source.BL_NODES, +# **gaussian_beam_source.BL_NODES, +# **astigmatic_gaussian_beam_source.BL_NODES, +# **tfsf_source.BL_NODES, } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/plane_wave_source.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/plane_wave_source.py index 7898f6e..999fb96 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/plane_wave_source.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/plane_wave_source.py @@ -4,79 +4,93 @@ import tidy3d as td import sympy as sp import sympy.physics.units as spu -from ... import contracts +import bpy + +from ... import contracts as ct from ... import sockets from .. import base -class PlaneWaveSourceNode(base.MaxwellSimTreeNode): - node_type = contracts.NodeType.PlaneWaveSource - +class PlaneWaveSourceNode(base.MaxwellSimNode): + node_type = ct.NodeType.PlaneWaveSource bl_label = "Plane Wave Source" - #bl_icon = ... #################### # - Sockets #################### input_sockets = { - "temporal_shape": sockets.MaxwellTemporalShapeSocketDef( - label="Temporal Shape", - ), - "center": sockets.PhysicalPoint3DSocketDef( - label="Center", - ), - "size": sockets.PhysicalSize3DSocketDef( - label="Size", - ), - "direction": sockets.BoolSocketDef( - label="+ Direction?", + "Temporal Shape": sockets.MaxwellTemporalShapeSocketDef(), + "Center": sockets.PhysicalPoint3DSocketDef(), + "Direction": sockets.BoolSocketDef( default_value=True, ), - "angle_theta": sockets.PhysicalAngleSocketDef( - label="θ", - ), - "angle_phi": sockets.PhysicalAngleSocketDef( - label="φ", - ), - "angle_pol": sockets.PhysicalAngleSocketDef( - label="Pol Angle", - ), + "Pol": sockets.PhysicalPolSocketDef(), } output_sockets = { - "source": sockets.MaxwellSourceSocketDef( - label="Source", - ), + "Source": sockets.MaxwellSourceSocketDef(), } + #################### + # - Properties + #################### + inj_axis: bpy.props.EnumProperty( + name="Injection Axis", + description="Axis to inject plane wave along", + items=[ + ("X", "X", "X-Axis"), + ("Y", "Y", "Y-Axis"), + ("Z", "Z", "Z-Axis"), + ], + default="Y", + update=(lambda self, context: self.sync_prop("inj_axis")), + ) + #################### # - Output Socket Computation #################### - @base.computes_output_socket("source") - def compute_source(self: contracts.NodeTypeProtocol) -> td.PointDipole: - temporal_shape = self.compute_input("temporal_shape") - _center = self.compute_input("center") - _size = self.compute_input("size") - _direction = self.compute_input("direction") - _angle_theta = self.compute_input("angle_theta") - _angle_phi = self.compute_input("angle_phi") - _angle_pol = self.compute_input("angle_pol") + @base.computes_output_socket( + "Source", + input_sockets={"Temporal Shape", "Center", "Direction", "Pol"}, + props={"inj_axis"}, + ) + def compute_source(self, input_sockets: dict, props: dict): + temporal_shape = input_sockets["Temporal Shape"] + _center = input_sockets["Center"] + _direction = input_sockets["Direction"] + _inj_axis = props["inj_axis"] + pol = input_sockets["Pol"] + direction = { + False: "-", + True: "+", + }[_direction] center = tuple(spu.convert_to(_center, spu.um) / spu.um) - size = tuple( - 0 if val == 1.0 else math.inf - for val in spu.convert_to(_size, spu.um) / spu.um - ) - angle_theta = spu.convert_to(_angle_theta, spu.rad) / spu.rad - angle_phi = spu.convert_to(_angle_phi, spu.rad) / spu.rad - angle_pol = spu.convert_to(_angle_pol, spu.rad) / spu.rad + size = { + "X": (0, math.inf, math.inf), + "Y": (math.inf, 0, math.inf), + "Z": (math.inf, math.inf, 0), + }[_inj_axis] + S0, S1, S2, S3 = tuple(pol) + + chi = 0.5 * sp.atan2(S2, S1) + psi = 0.5 * sp.asin(S3/S0) + ## chi: Pol angle + ## psi: Ellipticity + + ## TODO: Something's wonky. + #angle_theta = chi + #angle_phi = psi + pol_angle = sp.pi/2 - chi + + # Display the results return td.PlaneWave( - center=center, + center=tuple(_center), size=size, source_time=temporal_shape, direction="+" if _direction else "-", - angle_theta=angle_theta, - angle_phi=angle_phi, - pol_angle=angle_pol, + #angle_theta=angle_theta, + #angle_phi=angle_phi, + #pol_angle=pol_angle, ) @@ -88,7 +102,7 @@ BL_REGISTER = [ PlaneWaveSourceNode, ] BL_NODES = { - contracts.NodeType.PlaneWaveSource: ( - contracts.NodeCategory.MAXWELLSIM_SOURCES + ct.NodeType.PlaneWaveSource: ( + ct.NodeCategory.MAXWELLSIM_SOURCES ) } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/point_dipole_source.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/point_dipole_source.py index 80d72ba..e7264e7 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/point_dipole_source.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/point_dipole_source.py @@ -1,59 +1,81 @@ +import typing as typ import tidy3d as td import sympy as sp import sympy.physics.units as spu -from ... import contracts +import bpy + +from ... import contracts as ct from ... import sockets from .. import base -class PointDipoleSourceNode(base.MaxwellSimTreeNode): - node_type = contracts.NodeType.PointDipoleSource - +class PointDipoleSourceNode(base.MaxwellSimNode): + node_type = ct.NodeType.PointDipoleSource bl_label = "Point Dipole Source" - #bl_icon = ... #################### # - Sockets #################### input_sockets = { - "polarization": sockets.PhysicalPolSocketDef( - label="Polarization", - ), - "temporal_shape": sockets.MaxwellTemporalShapeSocketDef( - label="Temporal Shape", - ), - "center": sockets.PhysicalPoint3DSocketDef( - label="Center", - ), - "interpolate": sockets.BoolSocketDef( - label="Interpolate", + "Temporal Shape": sockets.MaxwellTemporalShapeSocketDef(), + "Center": sockets.PhysicalPoint3DSocketDef(), + "Interpolate": sockets.BoolSocketDef( default_value=True, ), } output_sockets = { - "source": sockets.MaxwellSourceSocketDef( - label="Source", - ), + "Source": sockets.MaxwellSourceSocketDef(), } + #################### + # - Properties + #################### + pol_axis: bpy.props.EnumProperty( + name="Polarization Axis", + description="Polarization Axis", + items=[ + ("EX", "Ex", "Electric field in x-dir"), + ("EY", "Ey", "Electric field in y-dir"), + ("EZ", "Ez", "Electric field in z-dir"), + ], + default="EX", + update=(lambda self, context: self.sync_prop("pol_axis")), + ) + + #################### + # - UI + #################### + def draw_props(self, context, layout): + layout.prop(self, "pol_axis", text="Pol Axis") + #################### # - Output Socket Computation #################### - @base.computes_output_socket("source") - def compute_source(self: contracts.NodeTypeProtocol) -> td.PointDipole: - polarization = self.compute_input("polarization") - temporal_shape = self.compute_input("temporal_shape") - _center = self.compute_input("center") - interpolate = self.compute_input("interpolate") + @base.computes_output_socket( + "Source", + input_sockets={"Temporal Shape", "Center", "Interpolate"}, + props={"pol_axis"}, + ) + def compute_source(self, input_sockets: dict[str, typ.Any], props: dict[str, typ.Any]) -> td.PointDipole: + pol_axis = { + "EX": "Ex", + "EY": "Ey", + "EZ": "Ez", + }[props["pol_axis"]] + + temporal_shape = input_sockets["Temporal Shape"] + _center = input_sockets["Center"] + interpolate = input_sockets["Interpolate"] center = tuple(spu.convert_to(_center, spu.um) / spu.um) - return td.PointDipole( + _res = td.PointDipole( center=center, source_time=temporal_shape, interpolate=interpolate, - polarization=polarization, + polarization=pol_axis, ) + return _res @@ -64,7 +86,7 @@ BL_REGISTER = [ PointDipoleSourceNode, ] BL_NODES = { - contracts.NodeType.PointDipoleSource: ( - contracts.NodeCategory.MAXWELLSIM_SOURCES + ct.NodeType.PointDipoleSource: ( + ct.NodeCategory.MAXWELLSIM_SOURCES ) } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/temporal_shapes/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/temporal_shapes/__init__.py index 6ff27eb..9618aa1 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/temporal_shapes/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/temporal_shapes/__init__.py @@ -1,14 +1,14 @@ from . import gaussian_pulse_temporal_shape -from . import continuous_wave_temporal_shape -from . import array_temporal_shape +#from . import continuous_wave_temporal_shape +#from . import array_temporal_shape BL_REGISTER = [ *gaussian_pulse_temporal_shape.BL_REGISTER, - *continuous_wave_temporal_shape.BL_REGISTER, - *array_temporal_shape.BL_REGISTER, +# *continuous_wave_temporal_shape.BL_REGISTER, +# *array_temporal_shape.BL_REGISTER, ] BL_NODES = { **gaussian_pulse_temporal_shape.BL_NODES, - **continuous_wave_temporal_shape.BL_NODES, - **array_temporal_shape.BL_NODES, +# **continuous_wave_temporal_shape.BL_NODES, +# **array_temporal_shape.BL_NODES, } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/temporal_shapes/gaussian_pulse_temporal_shape.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/temporal_shapes/gaussian_pulse_temporal_shape.py index 61d5687..416b6de 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/temporal_shapes/gaussian_pulse_temporal_shape.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/temporal_shapes/gaussian_pulse_temporal_shape.py @@ -1,13 +1,20 @@ +import typing as typ + import tidy3d as td +import numpy as np import sympy as sp import sympy.physics.units as spu -from .... import contracts +import bpy + +from ......utils import extra_sympy_units as spuex +from .... import contracts as ct from .... import sockets +from .... import managed_objs from ... import base -class GaussianPulseTemporalShapeNode(base.MaxwellSimTreeNode): - node_type = contracts.NodeType.GaussianPulseTemporalShape +class GaussianPulseTemporalShapeNode(base.MaxwellSimNode): + node_type = ct.NodeType.GaussianPulseTemporalShape bl_label = "Gaussian Pulse Temporal Shape" #bl_icon = ... @@ -19,45 +26,85 @@ class GaussianPulseTemporalShapeNode(base.MaxwellSimTreeNode): #"amplitude": sockets.RealNumberSocketDef( # label="Temporal Shape", #), ## Should have a unit of some kind... - "phase": sockets.PhysicalAngleSocketDef( - label="Phase", + "Freq Center": sockets.PhysicalFreqSocketDef( + default_value=500 * spuex.terahertz, ), - "freq_center": sockets.PhysicalFreqSocketDef( - label="Freq Center", + "Freq Std.": sockets.PhysicalFreqSocketDef( + default_value=200 * spuex.terahertz, ), - "freq_std": sockets.PhysicalFreqSocketDef( - label="Freq STD", - ), - "time_delay_rel_ang_freq": sockets.RealNumberSocketDef( - label="Time Delay rel. Ang. Freq", + "Phase": sockets.PhysicalAngleSocketDef(), + "Delay rel. AngFreq": sockets.RealNumberSocketDef( default_value=5.0, ), - "remove_dc_component": sockets.BoolSocketDef( - label="Remove DC", + "Remove DC": sockets.BoolSocketDef( default_value=True, ), } output_sockets = { - "temporal_shape": sockets.MaxwellTemporalShapeSocketDef( - label="Temporal Shape", - ), + "Temporal Shape": sockets.MaxwellTemporalShapeSocketDef(), } + managed_obj_defs = { + "amp_time": ct.schemas.ManagedObjDef( + mk=lambda name: managed_objs.ManagedBLImage(name), + name_prefix="amp_time_", + ) + } + + #################### + # - Properties + #################### + plot_time_start: bpy.props.FloatProperty( + name="Plot Time Start (ps)", + description="The instance ID of a particular MaxwellSimNode instance, used to index caches", + default=0.0, + update=(lambda self, context: self.sync_prop("plot_time_start", context)), + ) + plot_time_end: bpy.props.FloatProperty( + name="Plot Time End (ps)", + description="The instance ID of a particular MaxwellSimNode instance, used to index caches", + default=5, + update=(lambda self, context: self.sync_prop("plot_time_start", context)), + ) + + #################### + # - UI + #################### + def draw_props(self, context, layout): + layout.label(text="Plot Settings") + split = layout.split(factor=0.6) + + col = split.column() + col.label(text="t-Range (ps)") + + col = split.column() + col.prop(self, "plot_time_start", text="") + col.prop(self, "plot_time_end", text="") + #################### # - Output Socket Computation #################### - @base.computes_output_socket("temporal_shape") - def compute_source(self: contracts.NodeTypeProtocol) -> td.PointDipole: - _phase = self.compute_input("phase") - _freq_center = self.compute_input("freq_center") - _freq_std = self.compute_input("freq_std") - time_delay_rel_ang_freq = self.compute_input("time_delay_rel_ang_freq") - remove_dc_component = self.compute_input("remove_dc_component") + @base.computes_output_socket( + "Temporal Shape", + input_sockets={ + "Freq Center", "Freq Std.", "Phase", "Delay rel. AngFreq", + "Remove DC", + } + ) + def compute_source(self, input_sockets: dict) -> td.GaussianPulse: + if ( + (_freq_center := input_sockets["Freq Center"]) is None + or (_freq_std := input_sockets["Freq Std."]) is None + or (_phase := input_sockets["Phase"]) is None + or (time_delay_rel_ang_freq := input_sockets["Delay rel. AngFreq"]) is None + or (remove_dc_component := input_sockets["Remove DC"]) is None + ): + raise ValueError("Inputs not defined") cheating_amplitude = 1.0 - phase = spu.convert_to(_phase, spu.radian) / spu.radian freq_center = spu.convert_to(_freq_center, spu.hertz) / spu.hertz freq_std = spu.convert_to(_freq_std, spu.hertz) / spu.hertz + phase = spu.convert_to(_phase, spu.radian) / spu.radian return td.GaussianPulse( amplitude=cheating_amplitude, @@ -67,6 +114,29 @@ class GaussianPulseTemporalShapeNode(base.MaxwellSimTreeNode): offset=time_delay_rel_ang_freq, remove_dc_component=remove_dc_component, ) + + @base.on_show_plot( + managed_objs={"amp_time"}, + props={"plot_time_start", "plot_time_end"}, + output_sockets={"Temporal Shape"}, + stop_propagation=True, + ) + def on_show_plot( + self, + managed_objs: dict[str, ct.schemas.ManagedObj], + output_sockets: dict[str, typ.Any], + props: dict[str, typ.Any], + ): + temporal_shape = output_sockets["Temporal Shape"] + plot_time_start = props["plot_time_start"] * 1e-15 + plot_time_end = props["plot_time_end"] * 1e-15 + + times = np.linspace(plot_time_start, plot_time_end) + + managed_objs["amp_time"].mpl_plot_to_image( + lambda ax: temporal_shape.plot_spectrum(times, ax=ax), + bl_select=True, + ) @@ -77,7 +147,7 @@ BL_REGISTER = [ GaussianPulseTemporalShapeNode, ] BL_NODES = { - contracts.NodeType.GaussianPulseTemporalShape: ( - contracts.NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES + ct.NodeType.GaussianPulseTemporalShape: ( + ct.NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES ) } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/__init__.py index 6374ddc..358a4a8 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/__init__.py @@ -1,18 +1,18 @@ -from . import object_structure -from . import geonodes_structure -from . import scripted_structure +#from . import object_structure +#from . import geonodes_structure +#from . import scripted_structure from . import primitives BL_REGISTER = [ - *object_structure.BL_REGISTER, - *geonodes_structure.BL_REGISTER, - *scripted_structure.BL_REGISTER, +# *object_structure.BL_REGISTER, +# *geonodes_structure.BL_REGISTER, +# *scripted_structure.BL_REGISTER, *primitives.BL_REGISTER, ] BL_NODES = { - **object_structure.BL_NODES, - **geonodes_structure.BL_NODES, - **scripted_structure.BL_NODES, +# **object_structure.BL_NODES, +# **geonodes_structure.BL_NODES, +# **scripted_structure.BL_NODES, **primitives.BL_NODES, } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/__init__.py index e880562..bbb8c96 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/__init__.py @@ -1,14 +1,14 @@ from . import box_structure -from . import cylinder_structure -from . import sphere_structure +#from . import cylinder_structure +#from . import sphere_structure BL_REGISTER = [ *box_structure.BL_REGISTER, - *cylinder_structure.BL_REGISTER, - *sphere_structure.BL_REGISTER, +# *cylinder_structure.BL_REGISTER, +# *sphere_structure.BL_REGISTER, ] BL_NODES = { **box_structure.BL_NODES, - **cylinder_structure.BL_NODES, - **sphere_structure.BL_NODES, +# **cylinder_structure.BL_NODES, +# **sphere_structure.BL_NODES, } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py index 2a0c141..2167450 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py @@ -2,43 +2,37 @@ import tidy3d as td import sympy as sp import sympy.physics.units as spu -from .... import contracts +from .... import contracts as ct from .... import sockets from ... import base -class BoxStructureNode(base.MaxwellSimTreeNode): - node_type = contracts.NodeType.BoxStructure +class BoxStructureNode(base.MaxwellSimNode): + node_type = ct.NodeType.BoxStructure bl_label = "Box Structure" - #bl_icon = ... #################### # - Sockets #################### input_sockets = { - "medium": sockets.MaxwellMediumSocketDef( - label="Medium", - ), - "center": sockets.PhysicalPoint3DSocketDef( - label="Center", - ), - "size": sockets.PhysicalSize3DSocketDef( - label="Size", - ), + "Medium": sockets.MaxwellMediumSocketDef(), + "Center": sockets.PhysicalPoint3DSocketDef(), + "Size": sockets.PhysicalSize3DSocketDef(), } output_sockets = { - "structure": sockets.MaxwellStructureSocketDef( - label="Structure", - ), + "Structure": sockets.MaxwellStructureSocketDef(), } #################### # - Output Socket Computation #################### - @base.computes_output_socket("structure") - def compute_simulation(self: contracts.NodeTypeProtocol) -> td.Box: - medium = self.compute_input("medium") - _center = self.compute_input("center") - _size = self.compute_input("size") + @base.computes_output_socket( + "Structure", + input_sockets={"Medium", "Center", "Size"}, + ) + def compute_simulation(self, input_sockets: dict) -> td.Box: + medium = input_sockets["Medium"] + _center = input_sockets["Center"] + _size = input_sockets["Size"] center = tuple(spu.convert_to(_center, spu.um) / spu.um) size = tuple(spu.convert_to(_size, spu.um) / spu.um) @@ -60,7 +54,7 @@ BL_REGISTER = [ BoxStructureNode, ] BL_NODES = { - contracts.NodeType.BoxStructure: ( - contracts.NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES + ct.NodeType.BoxStructure: ( + ct.NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES ) } diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/utilities/combine.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/utilities/combine.py index e77f1aa..3de537e 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/utilities/combine.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/utilities/combine.py @@ -6,7 +6,7 @@ from ... import contracts from ... import sockets from .. import base -class CombineNode(base.MaxwellSimTreeNode): +class CombineNode(base.MaxwellSimNode): node_type = contracts.NodeType.Combine bl_label = "Combine" #bl_icon = ... diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/__init__.py index 32f71d1..f2b517a 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/__init__.py @@ -31,23 +31,19 @@ PhysicalAccelScalarSocketDef = physical.PhysicalAccelScalarSocketDef PhysicalForceScalarSocketDef = physical.PhysicalForceScalarSocketDef PhysicalPolSocketDef = physical.PhysicalPolSocketDef PhysicalFreqSocketDef = physical.PhysicalFreqSocketDef -PhysicalVacWLSocketDef = physical.PhysicalVacWLSocketDef -PhysicalSpecRelPermDistSocketDef = physical.PhysicalSpecRelPermDistSocketDef -PhysicalSpecPowerDistSocketDef = physical.PhysicalSpecPowerDistSocketDef from . import blender BlenderObjectSocketDef = blender.BlenderObjectSocketDef BlenderCollectionSocketDef = blender.BlenderCollectionSocketDef BlenderImageSocketDef = blender.BlenderImageSocketDef -BlenderVolumeSocketDef = blender.BlenderVolumeSocketDef BlenderGeoNodesSocketDef = blender.BlenderGeoNodesSocketDef BlenderTextSocketDef = blender.BlenderTextSocketDef -BlenderPreviewTargetSocketDef = blender.BlenderPreviewTargetSocketDef from . import maxwell MaxwellBoundBoxSocketDef = maxwell.MaxwellBoundBoxSocketDef MaxwellBoundFaceSocketDef = maxwell.MaxwellBoundFaceSocketDef MaxwellMediumSocketDef = maxwell.MaxwellMediumSocketDef +MaxwellMediumNonLinearitySocketDef = maxwell.MaxwellMediumNonLinearitySocketDef MaxwellSourceSocketDef = maxwell.MaxwellSourceSocketDef MaxwellTemporalShapeSocketDef = maxwell.MaxwellTemporalShapeSocketDef MaxwellStructureSocketDef = maxwell.MaxwellStructureSocketDef @@ -55,6 +51,10 @@ MaxwellMonitorSocketDef = maxwell.MaxwellMonitorSocketDef MaxwellFDTDSimSocketDef = maxwell.MaxwellFDTDSimSocketDef MaxwellSimGridSocketDef = maxwell.MaxwellSimGridSocketDef MaxwellSimGridAxisSocketDef = maxwell.MaxwellSimGridAxisSocketDef +MaxwellSimDomainSocketDef = maxwell.MaxwellSimDomainSocketDef + +from . import tidy3d +Tidy3DCloudTaskSocketDef = tidy3d.Tidy3DCloudTaskSocketDef BL_REGISTER = [ *basic.BL_REGISTER, @@ -63,4 +63,5 @@ BL_REGISTER = [ *physical.BL_REGISTER, *blender.BL_REGISTER, *maxwell.BL_REGISTER, + *tidy3d.BL_REGISTER, ] diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py index de16042..ecd1197 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py @@ -1,175 +1,316 @@ import typing as typ +import typing_extensions as typx +import functools + import bpy +import pydantic as pyd import sympy as sp import sympy.physics.units as spu -from .. import contracts +from .. import contracts as ct -class BLSocket(bpy.types.NodeSocket): - """A base type for nodes that greatly simplifies the implementation of - reliable, powerful nodes. +class MaxwellSimSocket(bpy.types.NodeSocket): + # Fundamentals + socket_type: ct.SocketType + bl_label: str - Should be used together with `contracts.BLSocketProtocol`. - """ + # Style + display_shape: typx.Literal[ + "CIRCLE", "SQUARE", "DIAMOND", "CIRCLE_DOT", "SQUARE_DOT", + "DIAMOND_DOT", + ] + socket_color: tuple + + # Options + #link_limit: int = 0 + use_units: bool = False + + # Computed + bl_idname: str + + #################### + # - Initialization + #################### def __init_subclass__(cls, **kwargs: typ.Any): super().__init_subclass__(**kwargs) ## Yucky superclass setup. - # Set bl_idname - cls.bl_idname = cls.socket_type.value - cls.socket_color = contracts.SocketType_to_color[ - cls.socket_type.value - ] + # Setup Blender ID for Node + if not hasattr(cls, "socket_type"): + msg = f"Socket class {cls} does not define 'socket_type'" + raise ValueError(msg) + cls.bl_idname = str(cls.socket_type.value) + + # Setup Locked Property for Node + cls.__annotations__["locked"] = bpy.props.BoolProperty( + name="Locked State", + description="The lock-state of a particular socket, which determines the socket's user editability", + default=False, + ) + + # Setup Style + cls.socket_color = ct.SOCKET_COLORS[cls.socket_type] + cls.socket_shape = ct.SOCKET_SHAPES[cls.socket_type] # Configure Use of Units - if ( - hasattr(cls, "use_units") - and cls.socket_type in contracts.SocketType_to_units - ): - # Set Unit Properties - cls.__annotations__["raw_unit"] = bpy.props.EnumProperty( + if cls.use_units: + if not (socket_units := ct.SOCKET_UNITS.get(cls.socket_type)): + msg = "Tried to `use_units` on {cls.bl_idname} socket, but `SocketType` has no units defined in `contracts.SOCKET_UNITS`" + raise RuntimeError(msg) + + # Current Unit + cls.__annotations__["active_unit"] = bpy.props.EnumProperty( name="Unit", description="Choose a unit", items=[ (unit_name, str(unit_value), str(unit_value)) - for unit_name, unit_value in contracts.SocketType_to_units[ - cls.socket_type - ]["values"].items() + for unit_name, unit_value in socket_units["values"].items() ], - default=contracts.SocketType_to_units[ - cls.socket_type - ]["default"], - update=lambda self, context: self._update_unit(), + default=socket_units["default"], + update=lambda self, context: self.sync_unit_change(), ) - cls.__annotations__["raw_unit_previous"] = bpy.props.StringProperty( - default=contracts.SocketType_to_units[ - cls.socket_type - ]["default"] - ) - - # Declare Node Property: 'preset' EnumProperty - if hasattr(cls, "draw_preview"): - cls.__annotations__["preview_active"] = bpy.props.BoolProperty( - name="Preview", - description="Preview the socket value", - default=False, + + # Previous Unit (for conversion) + cls.__annotations__["prev_active_unit"] = bpy.props.StringProperty( + default=socket_units["default"], ) #################### - # - Internal Methods + # - Action Chain + #################### + def trigger_action( + self, + action: typx.Literal["enable_lock", "disable_lock", "value_changed", "show_preview", "show_plot"], + ) -> None: + """Called whenever the socket's output value has changed. + + This also invalidates any of the socket's caches. + + When called on an input node, the containing node's + `trigger_action` method will be called with this socket. + + When called on a linked output node, the linked socket's + `trigger_action` method will be called. + """ + # Forwards Chains + if action in {"value_changed"}: + ## Input Socket + if not self.is_output: + self.node.trigger_action(action, socket_name=self.name) + + ## Linked Output Socket + elif self.is_output and self.is_linked: + for link in self.links: + link.to_socket.trigger_action(action) + + # Backwards Chains + elif action in {"enable_lock", "disable_lock", "show_preview", "show_plot"}: + if action == "enable_lock": + self.locked = True + + if action == "disable_lock": + self.locked = False + + ## Output Socket + if self.is_output: + self.node.trigger_action(action, socket_name=self.name) + + ## Linked Input Socket + elif not self.is_output and self.is_linked: + for link in self.links: + link.from_socket.trigger_action(action) + + #################### + # - Action Chain: Event Handlers + #################### + def sync_prop(self, prop_name: str, context: bpy.types.Context): + """Called when a property has been updated. + """ + if not hasattr(self, prop_name): + msg = f"Property {prop_name} not defined on socket {self}" + raise RuntimeError(msg) + + self.trigger_action("value_changed") + + def sync_link_added(self, link) -> bool: + """Called when a link has been added to this (input) socket. + + Returns a bool, whether or not the socket consents to the link change. + """ + if self.locked: return False + if self.is_output: + msg = f"Tried to sync 'link add' on output socket" + raise RuntimeError(msg) + + self.trigger_action("value_changed") + + return True + + def sync_link_removed(self, from_socket) -> bool: + """Called when a link has been removed from this (input) socket. + + Returns a bool, whether or not the socket consents to the link change. + """ + if self.locked: return False + if self.is_output: + msg = f"Tried to sync 'link add' on output socket" + raise RuntimeError(msg) + + self.trigger_action("value_changed") + + return True + + #################### + # - Data Chain #################### @property - def units(self) -> dict[str, sp.Expr]: - return contracts.SocketType_to_units[ + def value(self) -> typ.Any: + raise NotImplementedError + + @value.setter + def value(self, value: typ.Any) -> None: + raise NotImplementedError + + @property + def lazy_value(self) -> None: + raise NotImplementedError + + @lazy_value.setter + def lazy_value(self, lazy_value: typ.Any) -> None: + raise NotImplementedError + + @property + def capabilities(self) -> None: + raise NotImplementedError + + def _compute_data( + self, + kind: ct.DataFlowKind = ct.DataFlowKind.Value, + ) -> typ.Any: + """Computes the internal data of this socket, ONLY. + + **NOTE**: Low-level method. Use `compute_data` instead. + """ + if kind == ct.DataFlowKind.Value: + return self.value + if kind == ct.DataFlowKind.LazyValue: + return self.lazy_value + if kind == ct.DataFlowKind.Capabilities: + return self.capabilities + return None + + def compute_data( + self, + kind: ct.DataFlowKind = ct.DataFlowKind.Value, + ): + """Computes the value of this socket, including all relevant factors: + - If input socket, and unlinked, compute internal data. + - If input socket, and linked, compute linked socket data. + - If output socket, ask node for data. + """ + # Compute Output Socket + if self.is_output: + return self.node.compute_output(self.name, kind=kind) + + # Compute Input Socket + ## Unlinked: Retrieve Socket Value + if not self.is_linked: return self._compute_data(kind) + + ## Linked: Compute Output of Linked Sockets + linked_values = [ + link.from_socket.compute_data(kind) + for link in self.links + ] + + ## Return Single Value / List of Values + if len(linked_values) == 1: return linked_values[0] + return linked_values + + #################### + # - Unit Properties + #################### + @functools.cached_property + def possible_units(self) -> dict[str, sp.Expr]: + if not self.use_units: + msg = "Tried to get possible units for socket {self}, but socket doesn't `use_units`" + raise ValueError(msg) + + return ct.SOCKET_UNITS[ self.socket_type ]["values"] @property def unit(self) -> sp.Expr: - return contracts.SocketType_to_units[ - self.socket_type - ]["values"][self.raw_unit] - - @unit.setter - def unit(self, value) -> sp.Expr: - raw_unit_name = [ - raw_unit_name - for raw_unit_name, unit_value in contracts.SocketType_to_units[ - self.socket_type - ]["values"].items() - if value == unit_value - ][0] - - self.raw_unit = raw_unit_name + return self.possible_units[self.active_unit] @property - def _unit_previous(self) -> sp.Expr: - return contracts.SocketType_to_units[ - self.socket_type - ]["values"][self.raw_unit_previous] + def prev_unit(self) -> sp.Expr: + return self.possible_units[self.prev_active_unit] - @_unit_previous.setter - def _unit_previous(self, value) -> sp.Expr: - raw_unit_name = [ - raw_unit_name - for raw_unit_name, unit_value in contracts.SocketType_to_units[ - self.socket_type - ]["values"].items() - if value == unit_value - ][0] + @unit.setter + def unit(self, value: str | sp.Expr) -> None: + # Retrieve Unit by String + if isinstance(value, str) and value in self.possible_units: + self.active_unit = self.possible_units[value] + return - self.raw_unit_previous = raw_unit_name + # Retrieve =1 Matching Unit Name + matching_unit_names = [ + unit_name + for unit_name, unit_sympy in self.possible_units.items() + if value == unit_sympy + ] + if len(matching_unit_names) == 0: + msg = f"Tried to set unit for socket {self} with value {value}, but it is not one of possible units {''.join(possible.units.values())} for this socket (as defined in `contracts.SOCKET_UNITS`)" + raise ValueError(msg) + + if len(matching_unit_names) > 1: + msg = f"Tried to set unit for socket {self} with value {value}, but multiple possible matching units {''.join(possible.units.values())} for this socket (as defined in `contracts.SOCKET_UNITS`); there may only be one" + raise RuntimeError(msg) + + self.active_unit = matching_unit_names[0] - def value_as_unit(self, value) -> typ.Any: - """Return the given value expresse as the current internal unit, - without the unit. + def sync_unit_change(self) -> None: + """In unit-aware sockets, the internal `value()` property multiplies the Blender property value by the current active unit. + + When the unit is changed, `value()` will display the old scalar with the new unit. + To fix this, we need to update the scalar to use the new unit. + + Can be overridden if more specific logic is required. """ - if hasattr(self, "raw_value") and hasattr(self, "unit"): - # (Guard) Value Compatibility - if not self.is_compatible(value): - msg = f"Tried setting socket ({self}) to incompatible value ({value}) of type {type(value)}" - raise ValueError(msg) - - # Return Converted Unit - return spu.convert_to( - value, self.unit - ) / self.unit - else: - raise ValueError("Tried to get 'raw_value_as_unit', but class has no 'raw_value'") + prev_value = self.value / self.unit * self.prev_unit + ## After changing units, self.value is expressed in the wrong unit. + ## - Therefore, we removing the new unit, and re-add the prev unit. + ## - Using only self.value avoids implementation-specific details. + + self.value = spu.convert_to( + prev_value, + self.unit + ) ## Now, the unit conversion can be done correctly. + + self.prev_active_unit = self.active_unit - def _update_unit(self) -> None: - """Convert (if needed) the `raw_value` property, to use the unit - set in the `unit` property. - - If the `raw_value` property isn't set, this only sets "unit_previous". - - Run right after setting the `unit` property, in order to synchronize - the value with the new unit. + #################### + # - Style + #################### + def draw_color( + self, + context: bpy.types.Context, + node: bpy.types.Node, + ) -> ct.BLColorRGBA: + """Color of the socket icon, when embedded in a node. """ - if hasattr(self, "raw_value") and hasattr(self, "unit"): - if hasattr(self.raw_value, "__getitem__"): - self.raw_value = tuple(spu.convert_to( - sp.Matrix(tuple(self.raw_value)) * self._unit_previous, - self.unit, - ) / self.unit) - else: - self.raw_value = spu.convert_to( - self.raw_value * self._unit_previous, - self.unit, - ) / self.unit - - self._unit_previous = self.unit + return self.socket_color - #################### - # - Callback Dispatcher - #################### - def trigger_updates(self) -> None: - if not self.is_output: - self.node.update() - - #################### - # - Methods - #################### - def is_compatible(self, value: typ.Any) -> bool: - if not hasattr(self, "compatible_types"): - return True - - for compatible_type, checks in self.compatible_types.items(): - if ( - compatible_type is typ.Any or - isinstance(value, compatible_type) - ): - return all(check(self, value) for check in checks) - - return False - - #################### - # - UI - #################### @classmethod - def draw_color_simple(cls) -> contracts.BlenderColorRGB: + def draw_color_simple(cls) -> ct.BLColorRGBA: + """Fallback color of the socket icon (ex.when not embedded in a node). + """ return cls.socket_color + #################### + # - UI Methods + #################### def draw( self, context: bpy.types.Context, @@ -177,6 +318,10 @@ class BLSocket(bpy.types.NodeSocket): node: bpy.types.Node, text: str, ) -> None: + """Called by Blender to draw the socket UI. + """ + if self.locked: layout.enabled = False + if self.is_output: self.draw_output(context, layout, node, text) else: @@ -189,44 +334,31 @@ class BLSocket(bpy.types.NodeSocket): node: bpy.types.Node, text: str, ) -> None: + """Draws the socket UI, when the socket is an input socket. + """ + # Draw Linked Input: Label Row if self.is_linked: layout.label(text=text) return - # Column - col = layout.column(align=True) + # Parent Column + col = layout.column(align=False) - # Row: Label & Preview Toggle - label_col_row = col.row(align=True) - if hasattr(self, "draw_label_row"): - self.draw_label_row(label_col_row, text) - elif hasattr(self, "raw_unit"): - label_col_row.label(text=text) - label_col_row.prop(self, "raw_unit", text="") + # Draw Label Row + row = col.row(align=True) + if self.use_units: + split = row.split(factor=0.65, align=True) + + _row = split.row(align=True) + self.draw_label_row(_row, text) + + _col = split.column(align=True) + _col.prop(self, "active_unit", text="") else: - label_col_row.label(text=text) + self.draw_label_row(row, text) - if hasattr(self, "draw_preview"): - label_col_row.prop( - self, - "preview_active", - toggle=True, - text="", - icon="SEQ_PREVIEW", - ) - - # Row: Preview (in Box) - if hasattr(self, "draw_preview"): - if self.preview_active: - col_box = col.box() - self.draw_preview(col_box) - - # Row(s): Value - if hasattr(self, "draw_value"): - self.draw_value(col) - elif hasattr(self, "raw_value"): - #col_row = col.row(align=True) - col.prop(self, "raw_value", text="") + # Draw Value Row(s) + self.draw_value(col) def draw_output( self, @@ -235,23 +367,28 @@ class BLSocket(bpy.types.NodeSocket): node: bpy.types.Node, text: str, ) -> None: - col = layout.column() - row_col = col.row() - row_col.alignment = "RIGHT" - # Row: Label & Preview Toggle - if hasattr(self, "draw_preview"): - row_col.prop( - self, - "preview_active", - toggle=True, - text="", - icon="SEQ_PREVIEW", - ) + """Draws the socket UI, when the socket is an output socket. + """ + layout.label(text=text) + + #################### + # - UI Methods + #################### + def draw_label_row( + self, + row: bpy.types.UILayout, + text: str, + ) -> None: + """Called to draw the label row (same height as socket shape). - row_col.label(text=text) + Can be overridden. + """ + row.label(text=text) + + def draw_value(self, col: bpy.types.UILayout) -> None: + """Called to draw the value column in unlinked input sockets. - # Row: Preview (in box) - if hasattr(self, "draw_preview"): - if self.preview_active: - col_box = col.box() - self.draw_preview(col_box) + Can be overridden. + """ + pass + diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/any_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/any_socket.py index 1da41b2..6961145 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/any_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/any_socket.py @@ -5,46 +5,20 @@ import sympy as sp import pydantic as pyd from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class AnyBLSocket(base.BLSocket): - socket_type = contracts.SocketType.Any - socket_color = (0.0, 0.0, 0.0, 1.0) - +class AnyBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.Any bl_label = "Any" - - compatible_types = { - typ.Any: {}, - } - - #################### - # - Socket UI - #################### - def draw_label_row(self, label_col_row: bpy.types.UILayout, text: str) -> None: - """Draw the value of the real number. - """ - label_col_row.label(text=text) - - #################### - # - Computation of Default Value - #################### - @property - def default_value(self) -> None: - return None - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass #################### # - Socket Configuration #################### class AnySocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.Any - label: str + socket_type: ct.SocketType = ct.SocketType.Any def init(self, bl_socket: AnyBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/bool_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/bool_socket.py index 0eb9f12..b0cd559 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/bool_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/bool_socket.py @@ -5,27 +5,23 @@ import sympy as sp import pydantic as pyd from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class BoolBLSocket(base.BLSocket): - socket_type = contracts.SocketType.Bool +class BoolBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.Bool bl_label = "Bool" - compatible_types = { - bool: {}, - } - #################### # - Properties #################### raw_value: bpy.props.BoolProperty( name="Boolean", - description="Represents a boolean", + description="Represents a boolean value", default=False, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) #################### @@ -35,36 +31,27 @@ class BoolBLSocket(base.BLSocket): label_col_row.label(text=text) label_col_row.prop(self, "raw_value", text="") - def draw_value(self, label_col_row: bpy.types.UILayout) -> None: - pass - #################### # - Computation of Default Value #################### @property - def default_value(self) -> str: + def value(self) -> bool: return self.raw_value - @default_value.setter - def default_value(self, value: typ.Any) -> None: - # (Guard) Value Compatibility - if not self.is_compatible(value): - msg = f"Tried setting socket ({self}) to incompatible value ({value}) of type {type(value)}" - raise ValueError(msg) - - self.raw_value = bool(value) + @value.setter + def value(self, value: bool) -> None: + self.raw_value = value #################### # - Socket Configuration #################### class BoolSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.Bool - label: str + socket_type: ct.SocketType = ct.SocketType.Bool default_value: bool = False def init(self, bl_socket: BoolBLSocket) -> None: - bl_socket.raw_value = self.default_value + bl_socket.value = self.default_value #################### # - Blender Registration diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/file_path_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/file_path_socket.py index 7dac99b..f81fc9f 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/file_path_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/file_path_socket.py @@ -6,29 +6,25 @@ import sympy as sp import pydantic as pyd from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class FilePathBLSocket(base.BLSocket): - socket_type = contracts.SocketType.FilePath +class FilePathBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.FilePath bl_label = "File Path" - compatible_types = { - Path: {}, - } - #################### # - Properties #################### raw_value: bpy.props.StringProperty( name="File Path", description="Represents the path to a file", - #default="", subtype="FILE_PATH", - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + ## TODO: Use bpy methods to constrain the path #################### # - Socket UI @@ -41,39 +37,23 @@ class FilePathBLSocket(base.BLSocket): # - Computation of Default Value #################### @property - def default_value(self) -> Path: - """Return the text. - - Returns: - The text as a string. - """ - - return Path(str(self.raw_value)) + def value(self) -> Path: + return Path(bpy.path.abspath(self.raw_value)) - @default_value.setter - def default_value(self, value: typ.Any) -> None: - """Set the real number from some compatible type, namely - real sympy expressions with no symbols, or floats. - """ - - # (Guard) Value Compatibility - if not self.is_compatible(value): - msg = f"Tried setting socket ({self}) to incompatible value ({value}) of type {type(value)}" - raise ValueError(msg) - - self.raw_value = str(Path(value)) + @value.setter + def value(self, value: Path) -> None: + self.raw_value = bpy.path.relpath(str(value)) #################### # - Socket Configuration #################### class FilePathSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.FilePath - label: str + socket_type: ct.SocketType = ct.SocketType.FilePath default_path: Path = Path("") def init(self, bl_socket: FilePathBLSocket) -> None: - bl_socket.default_value = self.default_path + bl_socket.value = self.default_path #################### # - Blender Registration diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/text_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/text_socket.py index 779d1a7..68a1a2b 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/text_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/text_socket.py @@ -5,21 +5,15 @@ import sympy as sp import pydantic as pyd from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class TextBLSocket(base.BLSocket): - socket_type = contracts.SocketType.Text - socket_color = (0.2, 0.2, 0.2, 1.0) - +class TextBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.Text bl_label = "Text" - compatible_types = { - str: {}, - } - #################### # - Properties #################### @@ -27,7 +21,7 @@ class TextBLSocket(base.BLSocket): name="Text", description="Represents some text", default="", - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) #################### @@ -38,44 +32,27 @@ class TextBLSocket(base.BLSocket): """ label_col_row.prop(self, "raw_value", text=text) - def draw_value(self, label_col_row: bpy.types.UILayout) -> None: - pass - #################### # - Computation of Default Value #################### @property - def default_value(self) -> str: - """Return the text. - - Returns: - The text as a string. - """ - + def value(self) -> str: return self.raw_value - @default_value.setter - def default_value(self, value: typ.Any) -> None: - """Set the real number from some compatible type, namely - real sympy expressions with no symbols, or floats. - """ - - # (Guard) Value Compatibility - if not self.is_compatible(value): - msg = f"Tried setting socket ({self}) to incompatible value ({value}) of type {type(value)}" - raise ValueError(msg) - - self.raw_value = str(value) + @value.setter + def value(self, value: str) -> None: + self.raw_value = value #################### # - Socket Configuration #################### class TextSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.Text - label: str + socket_type: ct.SocketType = ct.SocketType.Text + + default_text: str = "" def init(self, bl_socket: TextBLSocket) -> None: - pass + bl_socket.value = self.default_text #################### # - Blender Registration diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/__init__.py index 479e52c..6d47392 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/__init__.py @@ -4,25 +4,18 @@ BlenderObjectSocketDef = object_socket.BlenderObjectSocketDef BlenderCollectionSocketDef = collection_socket.BlenderCollectionSocketDef from . import image_socket -from . import volume_socket BlenderImageSocketDef = image_socket.BlenderImageSocketDef -BlenderVolumeSocketDef = volume_socket.BlenderVolumeSocketDef from . import geonodes_socket from . import text_socket BlenderGeoNodesSocketDef = geonodes_socket.BlenderGeoNodesSocketDef BlenderTextSocketDef = text_socket.BlenderTextSocketDef -from . import target_socket -BlenderPreviewTargetSocketDef = target_socket.BlenderPreviewTargetSocketDef - BL_REGISTER = [ *object_socket.BL_REGISTER, *collection_socket.BL_REGISTER, *image_socket.BL_REGISTER, - *volume_socket.BL_REGISTER, - *target_socket.BL_REGISTER, *geonodes_socket.BL_REGISTER, *text_socket.BL_REGISTER, diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/collection_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/collection_socket.py index 8e541ef..a684343 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/collection_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/collection_socket.py @@ -4,14 +4,14 @@ import bpy import pydantic as pyd from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class BlenderCollectionBLSocket(base.BLSocket): - socket_type = contracts.SocketType.BlenderCollection - bl_label = "BlenderCollection" +class BlenderCollectionBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.BlenderCollection + bl_label = "Blender Collection" #################### # - Properties @@ -20,26 +20,31 @@ class BlenderCollectionBLSocket(base.BLSocket): name="Blender Collection", description="Represents a Blender collection", type=bpy.types.Collection, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + #################### + # - UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + #################### # - Default Value #################### @property - def default_value(self) -> bpy.types.Collection | None: + def value(self) -> bpy.types.Collection | None: return self.raw_value - @default_value.setter - def default_value(self, value: bpy.types.Collection) -> None: + @value.setter + def value(self, value: bpy.types.Collection) -> None: self.raw_value = value #################### # - Socket Configuration #################### class BlenderCollectionSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.BlenderCollection - label: str + socket_type: ct.SocketType = ct.SocketType.BlenderCollection def init(self, bl_socket: BlenderCollectionBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes_socket.py index 277758d..e8486b6 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes_socket.py @@ -4,7 +4,7 @@ import bpy import pydantic as pyd from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Operators @@ -22,28 +22,19 @@ class BlenderMaxwellResetGeoNodesSocket(bpy.types.Operator): #################### # - Blender Socket #################### -class BlenderGeoNodesBLSocket(base.BLSocket): - socket_type = contracts.SocketType.BlenderGeoNodes - bl_label = "BlenderGeoNodes" +class BlenderGeoNodesBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.BlenderGeoNodes + bl_label = "Geometry Node Tree" #################### # - Properties #################### - def update_geonodes_node(self): - if hasattr(self.node, "update_sockets_from_geonodes"): - self.node.update_sockets_from_geonodes() - else: - raise ValueError("Node doesn't have GeoNodes socket update method.") - - # Run the Usual Updates - self.trigger_updates() - raw_value: bpy.props.PointerProperty( name="Blender GeoNodes Tree", description="Represents a Blender GeoNodes Tree", type=bpy.types.NodeTree, poll=(lambda self, obj: obj.bl_idname == 'GeometryNodeTree'), - update=(lambda self, context: self.update_geonodes_node()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) #################### @@ -58,23 +49,28 @@ class BlenderGeoNodesBLSocket(base.BLSocket): icon="FILE_REFRESH", ) + #################### + # - UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + #################### # - Default Value #################### @property - def default_value(self) -> bpy.types.Object | None: + def value(self) -> bpy.types.NodeTree | None: return self.raw_value - @default_value.setter - def default_value(self, value: bpy.types.Object) -> None: + @value.setter + def value(self, value: bpy.types.NodeTree) -> None: self.raw_value = value #################### # - Socket Configuration #################### class BlenderGeoNodesSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.BlenderGeoNodes - label: str + socket_type: ct.SocketType = ct.SocketType.BlenderGeoNodes def init(self, bl_socket: BlenderGeoNodesBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/image_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/image_socket.py index 1786d8d..ebc7f1c 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/image_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/image_socket.py @@ -4,32 +4,47 @@ import bpy import pydantic as pyd from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class BlenderImageBLSocket(base.BLSocket): - socket_type = contracts.SocketType.BlenderImage - bl_label = "BlenderImage" +class BlenderImageBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.BlenderImage + bl_label = "Blender Image" + + #################### + # - Properties + #################### + raw_value: bpy.props.PointerProperty( + name="Blender Image", + description="Represents a Blender Image", + type=bpy.types.Image, + update=(lambda self, context: self.sync_prop("raw_value", context)), + ) + + #################### + # - UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") #################### # - Default Value #################### @property - def default_value(self) -> None: - pass + def value(self) -> bpy.types.Image | None: + return self.raw_value - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass + @value.setter + def value(self, value: bpy.types.Image) -> None: + self.raw_value = value #################### # - Socket Configuration #################### class BlenderImageSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.BlenderImage - label: str + socket_type: ct.SocketType = ct.SocketType.BlenderImage def init(self, bl_socket: BlenderImageBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/object_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/object_socket.py index d272726..a2c41a2 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/object_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/object_socket.py @@ -4,7 +4,7 @@ import bpy import pydantic as pyd from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket @@ -13,6 +13,7 @@ class BlenderMaxwellCreateAndAssignBLObject(bpy.types.Operator): bl_idname = "blender_maxwell.create_and_assign_bl_object" bl_label = "Create and Assign BL Object" + ## TODO: Refactor def execute(self, context): mesh = bpy.data.meshes.new("GenMesh") new_bl_object = bpy.data.objects.new("GenObj", mesh) @@ -32,9 +33,9 @@ class BlenderMaxwellCreateAndAssignBLObject(bpy.types.Operator): #################### # - Blender Socket #################### -class BlenderObjectBLSocket(base.BLSocket): - socket_type = contracts.SocketType.BlenderObject - bl_label = "BlenderObject" +class BlenderObjectBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.BlenderObject + bl_label = "Blender Object" #################### # - Properties @@ -43,7 +44,7 @@ class BlenderObjectBLSocket(base.BLSocket): name="Blender Object", description="Represents a Blender object", type=bpy.types.Object, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) #################### @@ -57,23 +58,25 @@ class BlenderObjectBLSocket(base.BLSocket): icon="ADD", ) + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + #################### # - Default Value #################### @property - def default_value(self) -> bpy.types.Object | None: + def value(self) -> bpy.types.Object | None: return self.raw_value - @default_value.setter - def default_value(self, value: bpy.types.Object) -> None: + @value.setter + def value(self, value: bpy.types.Object) -> None: self.raw_value = value #################### # - Socket Configuration #################### class BlenderObjectSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.BlenderObject - label: str + socket_type: ct.SocketType = ct.SocketType.BlenderObject def init(self, bl_socket: BlenderObjectBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/text_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/text_socket.py index 9381b77..7559b0e 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/text_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/text_socket.py @@ -9,27 +9,42 @@ from ... import contracts #################### # - Blender Socket #################### -class BlenderTextBLSocket(base.BLSocket): +class BlenderTextBLSocket(base.MaxwellSimSocket): socket_type = contracts.SocketType.BlenderText - bl_label = "BlenderText" + bl_label = "Blender Text" + + #################### + # - Properties + #################### + raw_value: bpy.props.PointerProperty( + name="Blender Text", + description="Represents a Blender text datablock", + type=bpy.types.Text, + update=(lambda self, context: self.sync_prop("raw_value", context)), + ) + + #################### + # - UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") #################### # - Default Value #################### @property - def default_value(self) -> None: - pass + def value(self) -> bpy.types.Text: + return self.raw_value - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass + @value.setter + def value(self, value: bpy.types.Text) -> None: + self.raw_value = value #################### # - Socket Configuration #################### class BlenderTextSocketDef(pyd.BaseModel): socket_type: contracts.SocketType = contracts.SocketType.BlenderText - label: str def init(self, bl_socket: BlenderTextBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/volume_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/volume_socket.py deleted file mode 100644 index dc4aadc..0000000 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/volume_socket.py +++ /dev/null @@ -1,42 +0,0 @@ -import typing as typ - -import bpy -import pydantic as pyd - -from .. import base -from ... import contracts - -#################### -# - Blender Socket -#################### -class BlenderVolumeBLSocket(base.BLSocket): - socket_type = contracts.SocketType.BlenderVolume - bl_label = "BlenderVolume" - - #################### - # - Default Value - #################### - @property - def default_value(self) -> None: - pass - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass - -#################### -# - Socket Configuration -#################### -class BlenderVolumeSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.BlenderVolume - label: str - - def init(self, bl_socket: BlenderVolumeBLSocket) -> None: - pass - -#################### -# - Blender Registration -#################### -BL_REGISTER = [ - BlenderVolumeBLSocket -] diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/__init__.py index 6088df8..196f9ac 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/__init__.py @@ -4,7 +4,9 @@ MaxwellBoundBoxSocketDef = bound_box_socket.MaxwellBoundBoxSocketDef MaxwellBoundFaceSocketDef = bound_face_socket.MaxwellBoundFaceSocketDef from . import medium_socket +from . import medium_non_linearity_socket MaxwellMediumSocketDef = medium_socket.MaxwellMediumSocketDef +MaxwellMediumNonLinearitySocketDef = medium_non_linearity_socket.MaxwellMediumNonLinearitySocketDef from . import source_socket from . import temporal_shape_socket @@ -20,15 +22,18 @@ MaxwellMonitorSocketDef = monitor_socket.MaxwellMonitorSocketDef from . import fdtd_sim_socket from . import sim_grid_socket from . import sim_grid_axis_socket +from . import sim_domain_socket MaxwellFDTDSimSocketDef = fdtd_sim_socket.MaxwellFDTDSimSocketDef MaxwellSimGridSocketDef = sim_grid_socket.MaxwellSimGridSocketDef MaxwellSimGridAxisSocketDef = sim_grid_axis_socket.MaxwellSimGridAxisSocketDef +MaxwellSimDomainSocketDef = sim_domain_socket.MaxwellSimDomainSocketDef BL_REGISTER = [ *bound_box_socket.BL_REGISTER, *bound_face_socket.BL_REGISTER, *medium_socket.BL_REGISTER, + *medium_non_linearity_socket.BL_REGISTER, *source_socket.BL_REGISTER, *temporal_shape_socket.BL_REGISTER, *structure_socket.BL_REGISTER, @@ -36,4 +41,5 @@ BL_REGISTER = [ *fdtd_sim_socket.BL_REGISTER, *sim_grid_socket.BL_REGISTER, *sim_grid_axis_socket.BL_REGISTER, + *sim_domain_socket.BL_REGISTER, ] diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_box_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_box_socket.py index 69b4878..8935d19 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_box_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_box_socket.py @@ -5,7 +5,7 @@ import pydantic as pyd import tidy3d as td from .. import base -from ... import contracts +from ... import contracts as ct BOUND_FACE_ITEMS = [ ("PML", "PML", "Perfectly matched layer"), @@ -13,98 +13,121 @@ BOUND_FACE_ITEMS = [ ("PMC", "PMC", "Perfect magnetic conductor"), ("PERIODIC", "Periodic", "Infinitely periodic layer"), ] +BOUND_MAP = { + "PML": td.PML(), + "PEC": td.PECBoundary(), + "PMC": td.PMCBoundary(), + "PERIODIC": td.Periodic(), +} -class MaxwellBoundBoxBLSocket(base.BLSocket): - socket_type = contracts.SocketType.MaxwellBoundBox +class MaxwellBoundBoxBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.MaxwellBoundBox bl_label = "Maxwell Bound Box" - compatible_types = { - td.BoundarySpec: {} - } - #################### # - Properties #################### + show_definition: bpy.props.BoolProperty( + name="Show Bounds Definition", + description="Toggle to show bound faces", + default=False, + update=(lambda self, context: self.sync_prop("show_definition", context)), + ) + x_pos: bpy.props.EnumProperty( name="+x Bound Face", description="+x choice of default boundary face", items=BOUND_FACE_ITEMS, default="PML", - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("x_pos", context)), ) x_neg: bpy.props.EnumProperty( name="-x Bound Face", description="-x choice of default boundary face", items=BOUND_FACE_ITEMS, default="PML", - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("x_neg", context)), ) y_pos: bpy.props.EnumProperty( name="+y Bound Face", description="+y choice of default boundary face", items=BOUND_FACE_ITEMS, default="PML", - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("y_pos", context)), ) y_neg: bpy.props.EnumProperty( name="-y Bound Face", description="-y choice of default boundary face", items=BOUND_FACE_ITEMS, default="PML", - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("y_neg", context)), ) z_pos: bpy.props.EnumProperty( name="+z Bound Face", description="+z choice of default boundary face", items=BOUND_FACE_ITEMS, default="PML", - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("z_pos", context)), ) z_neg: bpy.props.EnumProperty( name="-z Bound Face", description="-z choice of default boundary face", items=BOUND_FACE_ITEMS, default="PML", - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("z_neg", context)), ) #################### # - UI #################### + def draw_label_row(self, row: bpy.types.UILayout, text) -> None: + row.label(text=text) + row.prop( + self, "show_definition", toggle=True, text="", icon="MOD_LENGTH" + ) + def draw_value(self, col: bpy.types.UILayout) -> None: - col.label(text="-/+ x") - col_row = col.row(align=True) - col_row.prop(self, "x_neg", text="") - col_row.prop(self, "x_pos", text="") + if not self.show_definition: return - col.label(text="-/+ y") - col_row = col.row(align=True) - col_row.prop(self, "y_neg", text="") - col_row.prop(self, "y_pos", text="") - - col.label(text="-/+ z") - col_row = col.row(align=True) - col_row.prop(self, "z_neg", text="") - col_row.prop(self, "z_pos", text="") + for axis in ["x", "y", "z"]: + row = col.row(align=False) + split = row.split(factor=0.2, align=False) + + _col = split.column(align=True) + _col.alignment = "RIGHT" + _col.label(text=axis + " -") + _col.label(text=" +") + + _col = split.column(align=True) + _col.prop(self, axis + "_neg", text="") + _col.prop(self, axis + "_pos", text="") #################### # - Computation of Default Value #################### @property - def default_value(self) -> td.BoundarySpec: - return td.BoundarySpec() - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - return None + def value(self) -> td.BoundarySpec: + return td.BoundarySpec( + x=td.Boundary( + plus=BOUND_MAP[self.x_pos], + minus=BOUND_MAP[self.x_neg], + ), + y=td.Boundary( + plus=BOUND_MAP[self.y_pos], + minus=BOUND_MAP[self.y_neg], + ), + z=td.Boundary( + plus=BOUND_MAP[self.z_pos], + minus=BOUND_MAP[self.z_neg], + ), + ) #################### # - Socket Configuration #################### class MaxwellBoundBoxSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.MaxwellBoundBox - label: str + socket_type: ct.SocketType = ct.SocketType.MaxwellBoundBox def init(self, bl_socket: MaxwellBoundBoxBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_face_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_face_socket.py index 9e3fc0d..d7f201a 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_face_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_face_socket.py @@ -1,14 +1,15 @@ import typing as typ +import typing_extensions as typx import bpy import pydantic as pyd import tidy3d as td from .. import base -from ... import contracts +from ... import contracts as ct -class MaxwellBoundFaceBLSocket(base.BLSocket): - socket_type = contracts.SocketType.MaxwellBoundFace +class MaxwellBoundFaceBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.MaxwellBoundFace bl_label = "Maxwell Bound Face" #################### @@ -24,21 +25,20 @@ class MaxwellBoundFaceBLSocket(base.BLSocket): ("PERIODIC", "Periodic", "Infinitely periodic layer"), ], default="PML", - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("default_choice", context)), ) #################### # - UI #################### def draw_value(self, col: bpy.types.UILayout) -> None: - col_row = col.row(align=True) - col_row.prop(self, "default_choice", text="") + col.prop(self, "default_choice", text="") #################### # - Computation of Default Value #################### @property - def default_value(self) -> td.BoundarySpec: + def value(self) -> td.BoundarySpec: return { "PML": td.PML(num_layers=12), "PEC": td.PECBoundary(), @@ -46,21 +46,20 @@ class MaxwellBoundFaceBLSocket(base.BLSocket): "PERIODIC": td.Periodic(), }[self.default_choice] - @default_value.setter - def default_value(self, value: typ.Any) -> None: - return None + @value.setter + def value(self, value: typx.Literal["PML", "PEC", "PMC", "PERIODIC"]) -> None: + self.default_choice = value #################### # - Socket Configuration #################### class MaxwellBoundFaceSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.MaxwellBoundFace - label: str + socket_type: ct.SocketType = ct.SocketType.MaxwellBoundFace - default_choice: str = "PML" + default_choice: typx.Literal["PML", "PEC", "PMC", "PERIODIC"] = "PML" def init(self, bl_socket: MaxwellBoundFaceBLSocket) -> None: - bl_socket.default_choice = self.default_choice + bl_socket.value = self.default_choice #################### # - Blender Registration diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/fdtd_sim_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/fdtd_sim_socket.py index f70cb1e..f883cf2 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/fdtd_sim_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/fdtd_sim_socket.py @@ -5,33 +5,17 @@ import pydantic as pyd import tidy3d as td from .. import base -from ... import contracts +from ... import contracts as ct -class MaxwellFDTDSimBLSocket(base.BLSocket): - socket_type = contracts.SocketType.MaxwellFDTDSim - bl_label = "Maxwell Source" - - compatible_types = { - td.Simulation: {}, - } - - #################### - # - Computation of Default Value - #################### - @property - def default_value(self) -> None: - return None - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass +class MaxwellFDTDSimBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.MaxwellFDTDSim + bl_label = "Maxwell FDTD Simulation" #################### # - Socket Configuration #################### class MaxwellFDTDSimSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.MaxwellFDTDSim - label: str + socket_type: ct.SocketType = ct.SocketType.MaxwellFDTDSim def init(self, bl_socket: MaxwellFDTDSimBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium_non_linearity_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium_non_linearity_socket.py new file mode 100644 index 0000000..85a3289 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium_non_linearity_socket.py @@ -0,0 +1,28 @@ +import typing as typ + +import bpy +import pydantic as pyd +import tidy3d as td + +from .. import base +from ... import contracts as ct + +class MaxwellMediumNonLinearityBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.MaxwellMediumNonLinearity + bl_label = "Medium Non-Linearity" + +#################### +# - Socket Configuration +#################### +class MaxwellMediumNonLinearitySocketDef(pyd.BaseModel): + socket_type: ct.SocketType = ct.SocketType.MaxwellMediumNonLinearity + + def init(self, bl_socket: MaxwellMediumNonLinearityBLSocket) -> None: + pass + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + MaxwellMediumNonLinearityBLSocket, +] diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium_socket.py index e4ce5ce..944e41d 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium_socket.py @@ -2,83 +2,114 @@ import typing as typ import bpy import pydantic as pyd +import sympy as sp +import sympy.physics.units as spu import tidy3d as td +import scipy as sc +from .....utils.pydantic_sympy import ConstrSympyExpr, Complex from .. import base -from ... import contracts +from ... import contracts as ct -class MaxwellMediumBLSocket(base.BLSocket): - socket_type = contracts.SocketType.MaxwellMedium +VAC_SPEED_OF_LIGHT = ( + sc.constants.speed_of_light + * spu.meter/spu.second +) + +class MaxwellMediumBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.MaxwellMedium bl_label = "Maxwell Medium" - - compatible_types = { - td.components.medium.AbstractMedium: {} - } + use_units = True #################### # - Properties #################### - rel_permittivity: bpy.props.FloatProperty( - name="Permittivity", - description="Represents a simple, real permittivity.", - default=0.0, + wl: bpy.props.FloatProperty( + name="WL", + description="WL to evaluate conductivity at", + default=500.0, precision=4, - update=(lambda self, context: self.trigger_updates()), + step=50, + update=(lambda self, context: self.sync_prop("wl", context)), + ) + + rel_permittivity: bpy.props.FloatVectorProperty( + name="Relative Permittivity", + description="Represents a simple, complex permittivity", + size=2, + default=(1.0, 0.0), + precision=2, + update=(lambda self, context: self.sync_prop("rel_permittivity", context)), ) #################### # - Socket UI #################### def draw_value(self, col: bpy.types.UILayout) -> None: - """Draw the value of the area, including a toggle for - specifying the active unit. - """ - col_row = col.row(align=True) - col_row.prop(self, "rel_permittivity", text="ϵr") + col.prop(self, "wl", text="λ") + col.separator(factor=1.0) + + split = col.split(factor=0.35, align=False) + + col = split.column(align=True) + col.label(text="ϵ_r (ℂ)") + + col = split.column(align=True) + col.prop(self, "rel_permittivity", text="") #################### # - Computation of Default Value #################### @property - def default_value(self) -> td.Medium: - """Return the built-in medium representation as a `tidy3d` object, - ready to use in the simulation. - - Returns: - A completely normal medium with permittivity set. - """ - - return td.Medium( - permittivity=self.rel_permittivity, + def value(self) -> td.Medium: + freq = spu.convert_to( + VAC_SPEED_OF_LIGHT / (self.wl*self.unit), + spu.hertz, + ) / spu.hertz + return td.Medium.from_nk( + n=self.rel_permittivity[0], + k=self.rel_permittivity[1], + freq=freq, ) - @default_value.setter - def default_value(self, value: typ.Any) -> None: - """Set the built-in medium representation by adjusting the - permittivity, ONLY. + @value.setter + def value( + self, + value: tuple[ConstrSympyExpr(allow_variables=False), complex] + ) -> None: + _wl, rel_permittivity = value - Args: - value: Must be a tidy3d.Medium, or similar subclass. - """ + wl = float( + spu.convert_to( + _wl, + self.unit, + ) / self.unit + ) + self.wl = wl + self.rel_permittivity = (rel_permittivity.real, rel_permittivity.imag) + + def sync_unit_change(self): + """Override unit change to only alter frequency unit.""" - # ONLY Allow td.Medium - if isinstance(value, td.Medium): - self.rel_permittivity = value.permittivity - - msg = f"Tried setting MaxwellMedium socket ({self}) to something that isn't a simple `tidy3d.Medium`" - raise ValueError(msg) + self.value = ( + self.wl * self.prev_unit, + complex(*self.rel_permittivity) + ) + self.prev_active_unit = self.active_unit #################### # - Socket Configuration #################### class MaxwellMediumSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.MaxwellMedium - label: str + socket_type: ct.SocketType = ct.SocketType.MaxwellMedium - rel_permittivity: float = 1.0 + default_permittivity_real: float = 1.0 + default_permittivity_imag: float = 0.0 def init(self, bl_socket: MaxwellMediumBLSocket) -> None: - bl_socket.rel_permittivity = self.rel_permittivity + bl_socket.rel_permittivity = ( + self.default_permittivity_real, self.default_permittivity_imag + ) #################### # - Blender Registration diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/monitor_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/monitor_socket.py index bb02841..91dfa61 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/monitor_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/monitor_socket.py @@ -1,37 +1,54 @@ import typing as typ import bpy +import sympy.physics.units as spu import pydantic as pyd import tidy3d as td +import scipy as sc from .. import base -from ... import contracts +from ... import contracts as ct -class MaxwellMonitorBLSocket(base.BLSocket): - socket_type = contracts.SocketType.MaxwellMonitor - bl_label = "Maxwell Bound Box" - - compatible_types = { - td.BoundarySpec: {} - } +VAC_SPEED_OF_LIGHT = ( + sc.constants.speed_of_light + * spu.meter/spu.second +) + +class MaxwellMonitorBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.MaxwellMonitor + bl_label = "Maxwell Monitor" + use_units = True #################### - # - Computation of Default Value + # - Properties #################### + wl: bpy.props.FloatProperty( + name="WL", + description="WL to store in monitor", + default=500.0, + precision=4, + step=50, + update=(lambda self, context: self.sync_prop("wl", context)), + ) + @property - def default_value(self) -> td.BoundarySpec: - return td.BoundarySpec() - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - return None + def value(self) -> td.Monitor: + freq = spu.convert_to( + VAC_SPEED_OF_LIGHT / (self.wl*self.unit), + spu.hertz, + ) / spu.hertz + return td.FieldMonitor( + size=(td.inf, td.inf, 0), + freqs=[freq], + name="fields", + colocate=True, + ) #################### # - Socket Configuration #################### class MaxwellMonitorSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.MaxwellMonitor - label: str + socket_type: ct.SocketType = ct.SocketType.MaxwellMonitor def init(self, bl_socket: MaxwellMonitorBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_domain_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_domain_socket.py new file mode 100644 index 0000000..8de522e --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_domain_socket.py @@ -0,0 +1,28 @@ +import typing as typ + +import bpy +import pydantic as pyd +import tidy3d as td + +from .. import base +from ... import contracts as ct + +class MaxwellSimDomainBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.MaxwellSimDomain + bl_label = "Sim Domain" + +#################### +# - Socket Configuration +#################### +class MaxwellSimDomainSocketDef(pyd.BaseModel): + socket_type: ct.SocketType = ct.SocketType.MaxwellSimDomain + + def init(self, bl_socket: MaxwellSimDomainBLSocket) -> None: + pass + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + MaxwellSimDomainBLSocket, +] diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_grid_axis_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_grid_axis_socket.py index 25d84a4..1cf325f 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_grid_axis_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_grid_axis_socket.py @@ -5,33 +5,17 @@ import pydantic as pyd import tidy3d as td from .. import base -from ... import contracts +from ... import contracts as ct -class MaxwellSimGridAxisBLSocket(base.BLSocket): - socket_type = contracts.SocketType.MaxwellSimGridAxis +class MaxwellSimGridAxisBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.MaxwellSimGridAxis bl_label = "Maxwell Bound Box" - - compatible_types = { - td.BoundarySpec: {} - } - - #################### - # - Computation of Default Value - #################### - @property - def default_value(self) -> td.BoundarySpec: - return td.BoundarySpec() - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - return None #################### # - Socket Configuration #################### class MaxwellSimGridAxisSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.MaxwellSimGridAxis - label: str + socket_type: ct.SocketType = ct.SocketType.MaxwellSimGridAxis def init(self, bl_socket: MaxwellSimGridAxisBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_grid_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_grid_socket.py index 57a3887..4b4f2fe 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_grid_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_grid_socket.py @@ -5,36 +5,56 @@ import pydantic as pyd import tidy3d as td from .. import base -from ... import contracts +from ... import contracts as ct -class MaxwellSimGridBLSocket(base.BLSocket): - socket_type = contracts.SocketType.MaxwellSimGrid - bl_label = "Maxwell Bound Box" +class MaxwellSimGridBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.MaxwellSimGrid + bl_label = "Maxwell Sim Grid" - compatible_types = { - td.BoundarySpec: {} - } + #################### + # - Properties + #################### + min_steps_per_wl: bpy.props.FloatProperty( + name="Minimum Steps per Wavelength", + description="How many grid steps to ensure per wavelength", + default=10.0, + min=0.01, + #step=10, + precision=2, + update=(lambda self, context: self.sync_prop("min_steps_per_wl", context)), + ) + + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + split = col.split(factor=0.5, align=False) + + col = split.column(align=True) + col.label(text="min. stp/λ") + + col = split.column(align=True) + col.prop(self, "min_steps_per_wl", text="") #################### # - Computation of Default Value #################### @property - def default_value(self) -> td.BoundarySpec: - return td.BoundarySpec() - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - return None + def value(self) -> td.GridSpec: + return td.GridSpec.auto( + min_steps_per_wvl=self.min_steps_per_wl, + ) #################### # - Socket Configuration #################### class MaxwellSimGridSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.MaxwellSimGrid - label: str + socket_type: ct.SocketType = ct.SocketType.MaxwellSimGrid + + min_steps_per_wl: float = 10.0 def init(self, bl_socket: MaxwellSimGridBLSocket) -> None: - pass + bl_socket.min_steps_per_wl = self.min_steps_per_wl #################### # - Blender Registration diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/source_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/source_socket.py index ed8f642..1f6bb92 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/source_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/source_socket.py @@ -5,33 +5,17 @@ import pydantic as pyd import tidy3d as td from .. import base -from ... import contracts +from ... import contracts as ct -class MaxwellSourceBLSocket(base.BLSocket): - socket_type = contracts.SocketType.MaxwellSource +class MaxwellSourceBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.MaxwellSource bl_label = "Maxwell Source" - - compatible_types = { - td.components.base_sim.source.AbstractSource: {} - } - - #################### - # - Computation of Default Value - #################### - @property - def default_value(self) -> td.Medium: - return None - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass #################### # - Socket Configuration #################### class MaxwellSourceSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.MaxwellSource - label: str + socket_type: ct.SocketType = ct.SocketType.MaxwellSource def init(self, bl_socket: MaxwellSourceBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/structure_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/structure_socket.py index 6098ac1..246febf 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/structure_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/structure_socket.py @@ -2,36 +2,19 @@ import typing as typ import bpy import pydantic as pyd -import tidy3d as td from .. import base -from ... import contracts +from ... import contracts as ct -class MaxwellStructureBLSocket(base.BLSocket): - socket_type = contracts.SocketType.MaxwellStructure +class MaxwellStructureBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.MaxwellStructure bl_label = "Maxwell Structure" - - compatible_types = { - td.components.structure.AbstractStructure: {} - } - - #################### - # - Computation of Default Value - #################### - @property - def default_value(self) -> None: - return None - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass #################### # - Socket Configuration #################### class MaxwellStructureSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.MaxwellStructure - label: str + socket_type: ct.SocketType = ct.SocketType.MaxwellStructure def init(self, bl_socket: MaxwellStructureBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/temporal_shape_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/temporal_shape_socket.py index 0f4760a..29676e1 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/temporal_shape_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/temporal_shape_socket.py @@ -5,29 +5,17 @@ import pydantic as pyd import tidy3d as td from .. import base -from ... import contracts +from ... import contracts as ct -class MaxwellTemporalShapeBLSocket(base.BLSocket): - socket_type = contracts.SocketType.MaxwellTemporalShape +class MaxwellTemporalShapeBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.MaxwellTemporalShape bl_label = "Maxwell Temporal Shape" - - #################### - # - Computation of Default Value - #################### - @property - def default_value(self) -> td.Medium: - return None - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass #################### # - Socket Configuration #################### class MaxwellTemporalShapeSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.MaxwellTemporalShape - label: str + socket_type: ct.SocketType = ct.SocketType.MaxwellTemporalShape def init(self, bl_socket: MaxwellTemporalShapeBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/complex_number_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/complex_number_socket.py index 563d8ee..5b6e93b 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/complex_number_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/complex_number_socket.py @@ -4,24 +4,17 @@ import bpy import sympy as sp import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class ComplexNumberBLSocket(base.BLSocket): - socket_type = contracts.SocketType.ComplexNumber +class ComplexNumberBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.ComplexNumber bl_label = "Complex Number" - compatible_types = { - complex: {}, - sp.Expr: { - lambda self, v: v.is_complex, - lambda self, v: len(v.free_symbols) == 0, - }, - } - #################### # - Properties #################### @@ -31,7 +24,7 @@ class ComplexNumberBLSocket(base.BLSocket): size=2, default=(0.0, 0.0), subtype='NONE', - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) coord_sys: bpy.props.EnumProperty( name="Coordinate System", @@ -41,7 +34,7 @@ class ComplexNumberBLSocket(base.BLSocket): ("POLAR", "Polar", "Use Polar Coordinates", "DRIVER_ROTATIONAL_DIFFERENCE", 1), ], default="CARTESIAN", - update=lambda self, context: self._update_coord_sys(), + update=lambda self, context: self._sync_coord_sys(context), ) #################### @@ -55,34 +48,11 @@ class ComplexNumberBLSocket(base.BLSocket): col_row.prop(self, "raw_value", text="") col.prop(self, "coord_sys", text="") - def draw_preview(self, col_box: bpy.types.UILayout) -> None: - """Draw a live-preview value for the complex number, into the - given preview box. - - - Cartesian: a,b -> a + ib - - Polar: r,t -> re^(it) - - Returns: - The sympy expression representing the complex number. - """ - if self.coord_sys == "CARTESIAN": - text = f"= {self.default_value.n(2)}" - - elif self.coord_sys == "POLAR": - r = sp.Abs(self.default_value).n(2) - theta_rad = sp.arg(self.default_value).n(2) - text = f"= {r*sp.exp(sp.I*theta_rad)}" - - else: - raise RuntimeError("Invalid coordinate system for complex number") - - col_box.label(text=text) - #################### # - Computation of Default Value #################### @property - def default_value(self) -> sp.Expr: + def value(self) -> SympyExpr: """Return the complex number as a sympy expression, of a form determined by the coordinate system. @@ -100,8 +70,8 @@ class ComplexNumberBLSocket(base.BLSocket): "POLAR": v1 * sp.exp(sp.I*v2), }[self.coord_sys] - @default_value.setter - def default_value(self, value: typ.Any) -> None: + @value.setter + def value(self, value: SympyExpr) -> None: """Set the complex number from a sympy expression, using an internal representation determined by the coordinate system. @@ -122,7 +92,7 @@ class ComplexNumberBLSocket(base.BLSocket): #################### # - Internal Update Methods #################### - def _update_coord_sys(self): + def _sync_coord_sys(self, context: bpy.types.Context): if self.coord_sys == "CARTESIAN": r, theta_rad = self.raw_value self.raw_value = ( @@ -137,20 +107,17 @@ class ComplexNumberBLSocket(base.BLSocket): sp.arg(cart_value) if y != 0 else 0, ) - self.trigger_updates() + self.sync_prop("coord_sys", context) #################### # - Socket Configuration #################### class ComplexNumberSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.ComplexNumber - label: str + socket_type: ct.SocketType = ct.SocketType.ComplexNumber - preview: bool = False coord_sys: typ.Literal["CARTESIAN", "POLAR"] = "CARTESIAN" def init(self, bl_socket: ComplexNumberBLSocket) -> None: - bl_socket.preview_active = self.preview bl_socket.coord_sys = self.coord_sys #################### diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/integer_number_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/integer_number_socket.py index f29f4b2..d964161 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/integer_number_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/integer_number_socket.py @@ -4,14 +4,14 @@ import bpy import pydantic as pyd from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class IntegerNumberBLSocket(base.BLSocket): - socket_type = contracts.SocketType.IntegerNumber - bl_label = "IntegerNumber" +class IntegerNumberBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.IntegerNumber + bl_label = "Integer Number" #################### # - Properties @@ -20,31 +20,37 @@ class IntegerNumberBLSocket(base.BLSocket): name="Integer", description="Represents an integer", default=0, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col_row = col.row() + col_row.prop(self, "raw_value", text="") + #################### # - Default Value #################### @property - def default_value(self) -> None: + def value(self) -> int: return self.raw_value - @default_value.setter - def default_value(self, value: typ.Any) -> None: - self.raw_value = int(value) + @value.setter + def value(self, value: int) -> None: + self.raw_value = value #################### # - Socket Configuration #################### class IntegerNumberSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.IntegerNumber - label: str + socket_type: ct.SocketType = ct.SocketType.IntegerNumber default_value: int = 0 def init(self, bl_socket: IntegerNumberBLSocket) -> None: - bl_socket.raw_value = self.default_value + bl_socket.value = self.default_value #################### # - Blender Registration diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/rational_number_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/rational_number_socket.py index fb39775..20d4723 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/rational_number_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/rational_number_socket.py @@ -1,34 +1,62 @@ import typing as typ +import bpy +import sympy as sp import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class RationalNumberBLSocket(base.BLSocket): - socket_type = contracts.SocketType.RationalNumber +class RationalNumberBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.RationalNumber bl_label = "Rational Number" + #################### + # - Properties + #################### + raw_value: bpy.props.IntVectorProperty( + name="Rational Number", + description="Represents a rational number (int / int)", + size=2, + default=(1, 1), + subtype='NONE', + update=(lambda self, context: self.sync_prop("raw_value", context)), + ) + + #################### + # - UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col_row = col.row(align=True) + col_row.prop(self, "raw_value", text="") + #################### # - Default Value #################### @property - def default_value(self) -> None: - pass + def value(self) -> sp.Rational: + p, q = self.raw_value + return sp.Rational(p, q) - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass + @value.setter + def value(self, value: float | tuple[int, int] | SympyExpr) -> None: + if isinstance(value, float): + approx_rational = sp.nsimplify(value) + self.raw_value = (approx_rational.p, approx_rational.q) + elif isinstance(value, tuple): + self.raw_value = value + else: + self.raw_value = (value.p, value.q) #################### # - Socket Configuration #################### class RationalNumberSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.RationalNumber - label: str + socket_type: ct.SocketType = ct.SocketType.RationalNumber def init(self, bl_socket: RationalNumberBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/real_number_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/real_number_socket.py index 5ce1c5b..53df059 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/real_number_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/number/real_number_socket.py @@ -4,24 +4,17 @@ import bpy import sympy as sp import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class RealNumberBLSocket(base.BLSocket): - socket_type = contracts.SocketType.RealNumber +class RealNumberBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.RealNumber bl_label = "Real Number" - compatible_types = { - float: {}, - sp.Expr: { - lambda self, v: v.is_real, - lambda self, v: len(v.free_symbols) == 0, - }, - } - #################### # - Properties #################### @@ -30,46 +23,40 @@ class RealNumberBLSocket(base.BLSocket): description="Represents a real number", default=0.0, precision=6, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col_row = col.row() + col_row.prop(self, "raw_value", text="") + #################### # - Computation of Default Value #################### @property - def default_value(self) -> float: - """Return the real number. - - Returns: - The real number as a float. - """ - + def value(self) -> float: return self.raw_value - @default_value.setter - def default_value(self, value: typ.Any) -> None: - """Set the real number from some compatible type, namely - real sympy expressions with no symbols, or floats. - """ - - # (Guard) Value Compatibility - if not self.is_compatible(value): - msg = f"Tried setting socket ({self}) to incompatible value ({value}) of type {type(value)}" - raise ValueError(msg) - - self.raw_value = float(value) + @value.setter + def value(self, value: float | SympyExpr) -> None: + if isinstance(value, float): + self.raw_value = value + else: + float(value.n()) #################### # - Socket Configuration #################### class RealNumberSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.RealNumber - label: str + socket_type: ct.SocketType = ct.SocketType.RealNumber default_value: float = 0.0 def init(self, bl_socket: RealNumberBLSocket) -> None: - bl_socket.default_value = self.default_value + bl_socket.value = self.default_value #################### # - Blender Registration diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/__init__.py index 1d5672a..e33a204 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/__init__.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/__init__.py @@ -34,14 +34,7 @@ from . import pol_socket PhysicalPolSocketDef = pol_socket.PhysicalPolSocketDef from . import freq_socket -from . import vac_wl_socket PhysicalFreqSocketDef = freq_socket.PhysicalFreqSocketDef -PhysicalVacWLSocketDef = vac_wl_socket.PhysicalVacWLSocketDef - -from . import spec_rel_permit_dist_socket -from . import spec_power_dist_socket -PhysicalSpecRelPermDistSocketDef = spec_rel_permit_dist_socket.PhysicalSpecRelPermDistSocketDef -PhysicalSpecPowerDistSocketDef = spec_power_dist_socket.PhysicalSpecPowerDistSocketDef BL_REGISTER = [ @@ -68,7 +61,4 @@ BL_REGISTER = [ *pol_socket.BL_REGISTER, *freq_socket.BL_REGISTER, - *vac_wl_socket.BL_REGISTER, - *spec_rel_permit_dist_socket.BL_REGISTER, - *spec_power_dist_socket.BL_REGISTER, ] diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/accel_scalar_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/accel_scalar_socket.py index ffff7a3..7ea03ef 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/accel_scalar_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/accel_scalar_socket.py @@ -1,17 +1,19 @@ import typing as typ import bpy +import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class PhysicalAccelScalarBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalAccelScalar - bl_label = "PhysicalAccel" +class PhysicalAccelScalarBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalAccelScalar + bl_label = "Accel Scalar" use_units = True #################### @@ -22,28 +24,33 @@ class PhysicalAccelScalarBLSocket(base.BLSocket): description="Represents the unitless part of the acceleration", default=0.0, precision=6, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + #################### # - Default Value #################### @property - def default_value(self) -> None: + def value(self) -> SympyExpr: return self.raw_value * self.unit - @default_value.setter - def default_value(self, value: typ.Any) -> None: - self.raw_value = self.value_as_unit(value) + @value.setter + def value(self, value: SympyExpr) -> None: + self.raw_value = spu.convert_to(value, self.unit) / self.unit #################### # - Socket Configuration #################### class PhysicalAccelScalarSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalAccelScalar - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalAccelScalar - default_unit: typ.Any | None = None + default_unit: SympyExpr | None = None def init(self, bl_socket: PhysicalAccelScalarBLSocket) -> None: if self.default_unit: diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/angle_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/angle_socket.py index 83bb9bd..25706ac 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/angle_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/angle_socket.py @@ -1,17 +1,19 @@ import typing as typ import bpy +import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class PhysicalAngleBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalAngle - bl_label = "PhysicalAngle" +class PhysicalAngleBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalAngle + bl_label = "Physical Angle" use_units = True #################### @@ -22,28 +24,33 @@ class PhysicalAngleBLSocket(base.BLSocket): description="Represents the unitless part of the acceleration", default=0.0, precision=4, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + #################### # - Default Value #################### @property - def default_value(self) -> None: + def value(self) -> SympyExpr: return self.raw_value * self.unit - @default_value.setter - def default_value(self, value: typ.Any) -> None: - self.raw_value = self.value_as_unit(value) + @value.setter + def value(self, value: SympyExpr) -> None: + self.raw_value = spu.convert_to(value, self.unit) / self.unit #################### # - Socket Configuration #################### class PhysicalAngleSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalAngle - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalAngle - default_unit: typ.Any | None = None + default_unit: SympyExpr | None = None def init(self, bl_socket: PhysicalAngleBLSocket) -> None: if self.default_unit: diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/area_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/area_socket.py index 55f2387..44d91c2 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/area_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/area_socket.py @@ -5,25 +5,15 @@ import sympy as sp import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct -class PhysicalAreaBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalArea +class PhysicalAreaBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalArea bl_label = "Physical Area" use_units = True - compatible_types = { - sp.Expr: { - lambda self, v: v.is_real, - lambda self, v: len(v.free_symbols) == 0, - lambda self, v: any( - contracts.is_exactly_expressed_as_unit(v, unit) - for unit in self.units.values() - ) - }, - } - #################### # - Properties #################### @@ -32,19 +22,14 @@ class PhysicalAreaBLSocket(base.BLSocket): description="Represents the unitless part of the area", default=0.0, precision=6, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) #################### # - Socket UI #################### - def draw_label_row(self, label_col_row: bpy.types.UILayout, text: str) -> None: - label_col_row.label(text=text) - label_col_row.prop(self, "raw_unit", text="") - def draw_value(self, col: bpy.types.UILayout) -> None: - col_row = col.row(align=True) - col_row.prop(self, "raw_value", text="") + col.prop(self, "raw_value", text="") #################### # - Computation of Default Value @@ -73,8 +58,7 @@ class PhysicalAreaBLSocket(base.BLSocket): # - Socket Configuration #################### class PhysicalAreaSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalArea - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalArea default_unit: typ.Any | None = None diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/force_scalar_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/force_scalar_socket.py index a47deb1..d12947e 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/force_scalar_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/force_scalar_socket.py @@ -1,17 +1,19 @@ import typing as typ import bpy +import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class PhysicalForceScalarBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalForceScalar - bl_label = "PhysicalForceScalar" +class PhysicalForceScalarBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalForceScalar + bl_label = "Force Scalar" use_units = True #################### @@ -22,28 +24,33 @@ class PhysicalForceScalarBLSocket(base.BLSocket): description="Represents the unitless part of the force", default=0.0, precision=6, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + #################### # - Default Value #################### @property - def default_value(self) -> None: + def value(self) -> SympyExpr: return self.raw_value * self.unit - @default_value.setter - def default_value(self, value: typ.Any) -> None: - self.raw_value = self.value_as_unit(value) + @value.setter + def value(self, value: SympyExpr) -> None: + self.raw_value = spu.convert_to(value, self.unit) / self.unit #################### # - Socket Configuration #################### class PhysicalForceScalarSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalForceScalar - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalForceScalar - default_unit: typ.Any | None = None + default_unit: SympyExpr | None = None def init(self, bl_socket: PhysicalForceScalarBLSocket) -> None: if self.default_unit: diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/freq_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/freq_socket.py index 651472f..8bfb3cf 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/freq_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/freq_socket.py @@ -1,17 +1,19 @@ import typing as typ import bpy +import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class PhysicalFreqBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalFreq - bl_label = "PhysicalFreq" +class PhysicalFreqBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalFreq + bl_label = "Frequency" use_units = True #################### @@ -22,29 +24,40 @@ class PhysicalFreqBLSocket(base.BLSocket): description="Represents the unitless part of the frequency", default=0.0, precision=6, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + #################### # - Default Value #################### @property - def default_value(self) -> None: + def value(self) -> SympyExpr: return self.raw_value * self.unit - @default_value.setter - def default_value(self, value: typ.Any) -> None: - self.raw_value = self.value_as_unit(value) + @value.setter + def value(self, value: SympyExpr) -> None: + self.raw_value = spu.convert_to(value, self.unit) / self.unit #################### # - Socket Configuration #################### class PhysicalFreqSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalFreq - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalFreq + + default_value: SympyExpr | None = None + default_unit: SympyExpr | None = None def init(self, bl_socket: PhysicalFreqBLSocket) -> None: - pass + if self.default_value: + bl_socket.value = self.default_value + if self.default_unit: + bl_socket.unit = self.default_unit #################### # - Blender Registration diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/length_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/length_socket.py index 0f744af..bd1565a 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/length_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/length_socket.py @@ -1,16 +1,18 @@ import typing as typ import bpy +import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class PhysicalLengthBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalLength +class PhysicalLengthBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalLength bl_label = "PhysicalLength" use_units = True @@ -22,28 +24,33 @@ class PhysicalLengthBLSocket(base.BLSocket): description="Represents the unitless part of the force", default=0.0, precision=6, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + #################### # - Default Value #################### @property - def default_value(self) -> None: + def value(self) -> SympyExpr: return self.raw_value * self.unit - @default_value.setter - def default_value(self, value: typ.Any) -> None: - self.raw_value = self.value_as_unit(value) + @value.setter + def value(self, value: SympyExpr) -> None: + self.raw_value = spu.convert_to(value, self.unit) / self.unit #################### # - Socket Configuration #################### class PhysicalLengthSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalLength - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalLength - default_unit: typ.Any | None = None + default_unit: SympyExpr | None = None def init(self, bl_socket: PhysicalLengthBLSocket) -> None: if self.default_unit: diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/mass_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/mass_socket.py index f2a3619..fd16ba2 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/mass_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/mass_socket.py @@ -1,34 +1,57 @@ import typing as typ +import bpy +import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class PhysicalMassBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalMass - bl_label = "PhysicalMass" +class PhysicalMassBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalMass + bl_label = "Mass" + use_units = True + + #################### + # - Properties + #################### + raw_value: bpy.props.FloatProperty( + name="Unitless Mass", + description="Represents the unitless part of mass", + default=0.0, + precision=6, + update=(lambda self, context: self.sync_prop("raw_value", context)), + ) + + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") #################### # - Default Value #################### @property - def default_value(self) -> None: - pass + def value(self) -> SympyExpr: + return self.raw_value * self.unit - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass + @value.setter + def value(self, value: SympyExpr) -> None: + self.raw_value = spu.convert_to(value, self.unit) / self.unit + #################### # - Socket Configuration #################### class PhysicalMassSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalMass - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalMass + + default_unit: SympyExpr | None = None def init(self, bl_socket: PhysicalMassBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/point_3d_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/point_3d_socket.py index 20d310c..fd9d73d 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/point_3d_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/point_3d_socket.py @@ -5,25 +5,15 @@ import sympy as sp import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct -class PhysicalPoint3DBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalPoint3D - bl_label = "Physical Volume" +class PhysicalPoint3DBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalPoint3D + bl_label = "Volume" use_units = True - compatible_types = { - sp.Expr: { - lambda self, v: v.is_real, - lambda self, v: len(v.free_symbols) == 0, - lambda self, v: any( - contracts.is_exactly_expressed_as_unit(v, unit) - for unit in self.units.values() - ) - }, - } - #################### # - Properties #################### @@ -33,26 +23,31 @@ class PhysicalPoint3DBLSocket(base.BLSocket): size=3, default=(0.0, 0.0, 0.0), precision=4, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) #################### - # - Computation of Default Value + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + + #################### + # - Default Value #################### @property - def default_value(self) -> sp.Expr: + def value(self) -> sp.MatrixBase: return sp.Matrix(tuple(self.raw_value)) * self.unit - @default_value.setter - def default_value(self, value: typ.Any) -> None: - self.raw_value = self.value_as_unit(value) + @value.setter + def value(self, value: SympyExpr) -> None: + self.raw_value = tuple(spu.convert_to(value, self.unit) / self.unit) #################### # - Socket Configuration #################### class PhysicalPoint3DSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalPoint3D - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalPoint3D default_unit: typ.Any | None = None diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/pol_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/pol_socket.py index de0dba5..8dcb8a1 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/pol_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/pol_socket.py @@ -1,67 +1,223 @@ import typing as typ import bpy +import sympy as sp +import sympy.physics.units as spu +import sympy.physics.optics.polarization as spo_pol import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct -#################### -# - Blender Socket -#################### -class PhysicalPolBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalPol - bl_label = "PhysicalPol" +StokesVector = SympyExpr + +class PhysicalPolBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalPol + bl_label = "Polarization" + use_units = True + + + def radianize(self, ang): + return spu.convert_to( + ang * self.unit, + spu.radian, + ) / spu.radian #################### # - Properties #################### - default_choice: bpy.props.EnumProperty( - name="Bound Face", - description="A choice of default boundary face", + model: bpy.props.EnumProperty( + name="Polarization Model", + description="A choice of polarization representation", items=[ - ("EX", "Ex", "Linear x-pol of E field"), - ("EY", "Ey", "Linear y-pol of E field"), - ("EZ", "Ez", "Linear z-pol of E field"), - ("HX", "Hx", "Linear x-pol of H field"), - ("HY", "Hy", "Linear x-pol of H field"), - ("HZ", "Hz", "Linear x-pol of H field"), + ("UNPOL", "Unpolarized", "Unpolarized"), + ("LIN_ANG", "Linear", "Linearly polarized at angle"), + ("CIRC", "Circular", "Linearly polarized at angle"), + ("JONES", "Jones", "Polarized waves described by Jones vector"), + ("STOKES", "Stokes", "Linear x-pol of field"), ], - default="EX", - update=(lambda self, context: self.trigger_updates()), + default="UNPOL", + update=(lambda self, context: self.sync_prop("model", context)), ) + ## Lin Ang + lin_ang: bpy.props.FloatProperty( + name="Pol. Angle", + description="Angle to polarize linearly along", + default=0.0, + update=(lambda self, context: self.sync_prop("lin_ang", context)), + ) + ## Circ + circ: bpy.props.EnumProperty( + name="Pol. Orientation", + description="LCP or RCP", + items=[ + ("LCP", "LCP", "'Left Circular Polarization'"), + ("RCP", "RCP", "'Right Circular Polarization'"), + ], + default="LCP", + update=(lambda self, context: self.sync_prop("circ", context)), + ) + ## Jones + jones_psi: bpy.props.FloatProperty( + name="Jones X-Axis Angle", + description="Angle of the ellipse to the x-axis", + default=0.0, + precision=2, + update=(lambda self, context: self.sync_prop("jones_psi", context)), + ) + jones_chi: bpy.props.FloatProperty( + name="Jones Major-Axis-Adjacent Angle", + description="Angle of adjacent to the ellipse major axis", + default=0.0, + precision=2, + update=(lambda self, context: self.sync_prop("jones_chi", context)), + ) + + ## Stokes + stokes_psi: bpy.props.FloatProperty( + name="Stokes X-Axis Angle", + description="Angle of the ellipse to the x-axis", + default=0.0, + precision=2, + update=(lambda self, context: self.sync_prop("stokes_psi", context)), + ) + stokes_chi: bpy.props.FloatProperty( + name="Stokes Major-Axis-Adjacent Angle", + description="Angle of adjacent to the ellipse major axis", + default=0.0, + precision=2, + update=(lambda self, context: self.sync_prop("stokes_chi", context)), + ) + stokes_p: bpy.props.FloatProperty( + name="Stokes Polarization Degree", + description="The degree of polarization", + default=0.0, + precision=2, + update=(lambda self, context: self.sync_prop("stokes_p", context)), + ) + stokes_I: bpy.props.FloatProperty( + name="Stokes Field Intensity", + description="The intensity of the polarized field", + default=0.0, + precision=2, + update=(lambda self, context: self.sync_prop("stokes_I", context)), + ) ## TODO: Units? + #################### # - UI #################### def draw_value(self, col: bpy.types.UILayout) -> None: - col_row = col.row(align=True) - col_row.prop(self, "default_choice", text="") + col.prop(self, "model", text="") + + if self.model == "LIN_ANG": + col.prop(self, "lin_ang", text="") + + elif self.model == "CIRC": + col.prop(self, "circ", text="") + + elif self.model == "JONES": + split = col.split(factor=0.2, align=True) + + col = split.column(align=False) + col.label(text="ψ,χ") + + col = split.row(align=True) + col.prop(self, "jones_psi", text="") + col.prop(self, "jones_chi", text="") + + elif self.model == "STOKES": + split = col.split(factor=0.2, align=True) + # Split #1 + col = split.column(align=False) + col.label(text="ψ,χ") + col.label(text="p,I") + + # Split #2 + col = split.column(align=False) + row = col.row(align=True) + row.prop(self, "stokes_psi", text="") + row.prop(self, "stokes_chi", text="") + + row = col.row(align=True) + row.prop(self, "stokes_p", text="") + row.prop(self, "stokes_I", text="") + + ## TODO: Visualize stokes vector as oriented plane wave shape in plot (direct), in 3D (maybe in combination with a source), and on Poincare sphere. #################### # - Default Value #################### @property - def default_value(self) -> str: + def _value_unpol(self) -> StokesVector: + return spo_pol.stokes_vector(0, 0, 0) + @property + def _value_lin_ang(self) -> StokesVector: + return spo_pol.stokes_vector(self.radianize(self.lin_ang), 0, 0) + @property + def _value_circ(self) -> StokesVector: return { - "EX": "Ex", - "EY": "Ey", - "EZ": "Ez", - "HX": "Hx", - "HY": "Hy", - "HZ": "Hz", - }[self.default_choice] + "RCP": spo_pol.stokes_vector(0, sp.pi/4, 0), + "LCP": spo_pol.stokes_vector(0, -sp.pi/4, 0), + }[self.circ] + @property + def _value_jones(self) -> StokesVector: + return spo_pol.jones_2_stokes( + spo_pol.jones_vector( + self.radianize(self.jones_psi), + self.radianize(self.jones_chi), + ) + ) + @property + def _value_stokes(self) -> StokesVector: + return spo_pol.stokes_vector( + self.radianize(self.stokes_psi), + self.radianize(self.stokes_chi), + self.stokes_p, + self.stokes_I, + ) + + @property + def value(self) -> StokesVector: + return { + "UNPOL": self._value_unpol, + "LIN_ANG": self._value_lin_ang, + "CIRC": self._value_circ, + "JONES": self._value_jones, + "STOKES": self._value_stokes, + }[self.model] - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass + def sync_unit_change(self) -> None: + """We don't have a setter, so we need to manually implement the unit change operation.""" + self.lin_ang = spu.convert_to( + self.lin_ang * self.prev_unit, + self.unit, + ) / self.unit + self.jones_psi = spu.convert_to( + self.jones_psi * self.prev_unit, + self.unit, + ) / self.unit + self.jones_chi = spu.convert_to( + self.jones_chi * self.prev_unit, + self.unit, + ) / self.unit + self.stokes_psi = spu.convert_to( + self.stokes_psi * self.prev_unit, + self.unit, + ) / self.unit + self.stokes_chi = spu.convert_to( + self.stokes_chi * self.prev_unit, + self.unit, + ) / self.unit + + self.prev_active_unit = self.active_unit #################### # - Socket Configuration #################### class PhysicalPolSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalPol - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalPol def init(self, bl_socket: PhysicalPolBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/size_3d_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/size_3d_socket.py index 50db8ba..77c9bf5 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/size_3d_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/size_3d_socket.py @@ -5,12 +5,13 @@ import sympy as sp import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct -class PhysicalSize3DBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalSize3D - bl_label = "Physical Volume" +class PhysicalSize3DBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalSize3D + bl_label = "3D Size" use_units = True #################### @@ -22,28 +23,33 @@ class PhysicalSize3DBLSocket(base.BLSocket): size=3, default=(1.0, 1.0, 1.0), precision=4, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + #################### # - Computation of Default Value #################### @property - def default_value(self) -> sp.Expr: + def value(self) -> SympyExpr: return sp.Matrix(tuple(self.raw_value)) * self.unit - @default_value.setter - def default_value(self, value: typ.Any) -> None: - self.raw_value = self.value_as_unit(value) + @value.setter + def value(self, value: SympyExpr) -> None: + self.raw_value = tuple(spu.convert_to(value, self.unit) / self.unit) #################### # - Socket Configuration #################### class PhysicalSize3DSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalSize3D - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalSize3D - default_unit: typ.Any | None = None + default_unit: SympyExpr | None = None def init(self, bl_socket: PhysicalSize3DBLSocket) -> None: if self.default_unit: diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/spec_power_dist_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/spec_power_dist_socket.py deleted file mode 100644 index 8285d7e..0000000 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/spec_power_dist_socket.py +++ /dev/null @@ -1,41 +0,0 @@ -import typing as typ - -import pydantic as pyd - -from .. import base -from ... import contracts - -#################### -# - Blender Socket -#################### -class PhysicalSpecPowerDistBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalSpecPowerDist - bl_label = "PhysicalSpecPowerDist" - - #################### - # - Default Value - #################### - @property - def default_value(self) -> None: - pass - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass - -#################### -# - Socket Configuration -#################### -class PhysicalSpecPowerDistSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalSpecPowerDist - label: str - - def init(self, bl_socket: PhysicalSpecPowerDistBLSocket) -> None: - pass - -#################### -# - Blender Registration -#################### -BL_REGISTER = [ - PhysicalSpecPowerDistBLSocket, -] diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/spec_rel_permit_dist_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/spec_rel_permit_dist_socket.py deleted file mode 100644 index c54b26c..0000000 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/spec_rel_permit_dist_socket.py +++ /dev/null @@ -1,41 +0,0 @@ -import typing as typ - -import pydantic as pyd - -from .. import base -from ... import contracts - -#################### -# - Blender Socket -#################### -class PhysicalSpecRelPermDistBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalSpecRelPermDist - bl_label = "PhysicalSpecRelPermDist" - - #################### - # - Default Value - #################### - @property - def default_value(self) -> None: - pass - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass - -#################### -# - Socket Configuration -#################### -class PhysicalSpecRelPermDistSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalSpecRelPermDist - label: str - - def init(self, bl_socket: PhysicalSpecRelPermDistBLSocket) -> None: - pass - -#################### -# - Blender Registration -#################### -BL_REGISTER = [ - PhysicalSpecRelPermDistBLSocket, -] diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/speed_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/speed_socket.py index 4d3e8fb..1e5d2b7 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/speed_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/speed_socket.py @@ -1,37 +1,60 @@ import typing as typ +import bpy +import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class PhysicalSpeedBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalSpeed - bl_label = "PhysicalSpeed" +class PhysicalSpeedBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalSpeed + bl_label = "Speed" + use_units = True + + #################### + # - Properties + #################### + raw_value: bpy.props.FloatProperty( + name="Unitless Speed", + description="Represents the unitless part of the speed", + default=0.0, + precision=6, + update=(lambda self, context: self.sync_prop("raw_value", context)), + ) + + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") #################### # - Default Value #################### @property - def default_value(self) -> None: - pass + def value(self) -> SympyExpr: + return self.raw_value * self.unit - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass + @value.setter + def value(self, value: SympyExpr) -> None: + self.raw_value = spu.convert_to(value, self.unit) / self.unit #################### # - Socket Configuration #################### class PhysicalSpeedSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalSpeed - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalSpeed + + default_unit: SympyExpr | None = None def init(self, bl_socket: PhysicalSpeedBLSocket) -> None: - pass + if self.default_unit: + bl_socket.unit = self.default_unit #################### # - Blender Registration diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/time_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/time_socket.py index e429721..25820b6 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/time_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/time_socket.py @@ -1,51 +1,61 @@ import typing as typ import bpy +import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class PhysicalTimeBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalTime - bl_label = "PhysicalTime" +class PhysicalTimeBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalTime + bl_label = "Time" use_units = True #################### # - Properties #################### raw_value: bpy.props.FloatProperty( - name="Unitless Force", - description="Represents the unitless part of the force", + name="Unitless Time", + description="Represents the unitless part of time", default=0.0, - precision=6, - update=(lambda self, context: self.trigger_updates()), + precision=4, + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + #################### # - Default Value #################### @property - def default_value(self) -> None: + def value(self) -> SympyExpr: return self.raw_value * self.unit - @default_value.setter - def default_value(self, value: typ.Any) -> None: - self.raw_value = self.value_as_unit(value) + @value.setter + def value(self, value: SympyExpr) -> None: + self.raw_value = spu.convert_to(value, self.unit) / self.unit #################### # - Socket Configuration #################### class PhysicalTimeSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalTime - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalTime + default_value: SympyExpr | None = None default_unit: typ.Any | None = None def init(self, bl_socket: PhysicalTimeBLSocket) -> None: + if self.default_value: + bl_socket.value = self.default_value if self.default_unit: bl_socket.unit = self.default_unit diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/unit_system_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/unit_system_socket.py index 0b6b7f5..dc985c0 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/unit_system_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/unit_system_socket.py @@ -2,33 +2,48 @@ import typing as typ import bpy import sympy as sp +import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct -def contract_units_to_items(socket_type): +ST = ct.SocketType +SU = lambda socket_type: ct.SOCKET_UNITS[ + socket_type +]["values"] + +#################### +# - Utilities +#################### +def contract_units_to_items( + socket_type: ST +) -> list[tuple[str, str, str]]: + """For a given socket type, get a bpy.props.EnumProperty-compatible list-tuple of items to display in the enum dropdown menu. + """ return [ ( unit_key, str(unit), f"{socket_type}-compatible unit", ) - for unit_key, unit in contracts.SocketType_to_units[ - socket_type - ]["values"].items() + for unit_key, unit in SU(socket_type).items() ] -def default_unit_key_for(socket_type): - return contracts.SocketType_to_units[ + +def default_unit_key_for(socket_type: ST) -> str: + """For a given socket type, return the default key corresponding to the default unit. + """ + return ct.SOCKET_UNITS[ socket_type ]["default"] #################### # - Blender Socket #################### -class PhysicalUnitSystemBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalUnitSystem - bl_label = "PhysicalUnitSystem" +class PhysicalUnitSystemBLSocket(base.MaxwellSimSocket): + socket_type = ST.PhysicalUnitSystem + bl_label = "Unit System" #################### # - Properties @@ -37,151 +52,147 @@ class PhysicalUnitSystemBLSocket(base.BLSocket): name="Show Unit System Definition", description="Toggle to show unit system definition", default=False, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("show_definition", context)), ) unit_time: bpy.props.EnumProperty( name="Time Unit", description="Unit of time", - items=contract_units_to_items(contracts.SocketType.PhysicalTime), - default=default_unit_key_for(contracts.SocketType.PhysicalTime), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalTime), + default=default_unit_key_for(ST.PhysicalTime), + update=(lambda self, context: self.sync_prop("unit_time", context)), ) unit_angle: bpy.props.EnumProperty( name="Angle Unit", description="Unit of angle", - items=contract_units_to_items(contracts.SocketType.PhysicalAngle), - default=default_unit_key_for(contracts.SocketType.PhysicalAngle), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalAngle), + default=default_unit_key_for(ST.PhysicalAngle), + update=(lambda self, context: self.sync_prop("unit_angle", context)), ) unit_length: bpy.props.EnumProperty( name="Length Unit", description="Unit of length", - items=contract_units_to_items(contracts.SocketType.PhysicalLength), - default=default_unit_key_for(contracts.SocketType.PhysicalLength), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalLength), + default=default_unit_key_for(ST.PhysicalLength), + update=(lambda self, context: self.sync_prop("unit_length", context)), ) unit_area: bpy.props.EnumProperty( name="Area Unit", description="Unit of area", - items=contract_units_to_items(contracts.SocketType.PhysicalArea), - default=default_unit_key_for(contracts.SocketType.PhysicalArea), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalArea), + default=default_unit_key_for(ST.PhysicalArea), + update=(lambda self, context: self.sync_prop("unit_area", context)), ) unit_volume: bpy.props.EnumProperty( name="Volume Unit", description="Unit of time", - items=contract_units_to_items(contracts.SocketType.PhysicalVolume), - default=default_unit_key_for(contracts.SocketType.PhysicalVolume), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalVolume), + default=default_unit_key_for(ST.PhysicalVolume), + update=(lambda self, context: self.sync_prop("unit_volume", context)), ) unit_point_2d: bpy.props.EnumProperty( name="Point2D Unit", description="Unit of 2D points", - items=contract_units_to_items(contracts.SocketType.PhysicalPoint2D), - default=default_unit_key_for(contracts.SocketType.PhysicalPoint2D), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalPoint2D), + default=default_unit_key_for(ST.PhysicalPoint2D), + update=(lambda self, context: self.sync_prop("unit_point_2d", context)), ) unit_point_3d: bpy.props.EnumProperty( name="Point3D Unit", description="Unit of 3D points", - items=contract_units_to_items(contracts.SocketType.PhysicalPoint3D), - default=default_unit_key_for(contracts.SocketType.PhysicalPoint3D), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalPoint3D), + default=default_unit_key_for(ST.PhysicalPoint3D), + update=(lambda self, context: self.sync_prop("unit_point_3d", context)), ) unit_size_2d: bpy.props.EnumProperty( name="Size2D Unit", description="Unit of 2D sizes", - items=contract_units_to_items(contracts.SocketType.PhysicalSize2D), - default=default_unit_key_for(contracts.SocketType.PhysicalSize2D), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalSize2D), + default=default_unit_key_for(ST.PhysicalSize2D), + update=(lambda self, context: self.sync_prop("unit_size_2d", context)), ) unit_size_3d: bpy.props.EnumProperty( name="Size3D Unit", description="Unit of 3D sizes", - items=contract_units_to_items(contracts.SocketType.PhysicalSize3D), - default=default_unit_key_for(contracts.SocketType.PhysicalSize3D), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalSize3D), + default=default_unit_key_for(ST.PhysicalSize3D), + update=(lambda self, context: self.sync_prop("unit_size_3d", context)), ) unit_mass: bpy.props.EnumProperty( name="Mass Unit", description="Unit of mass", - items=contract_units_to_items(contracts.SocketType.PhysicalMass), - default=default_unit_key_for(contracts.SocketType.PhysicalMass), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalMass), + default=default_unit_key_for(ST.PhysicalMass), + update=(lambda self, context: self.sync_prop("unit_mass", context)), ) unit_speed: bpy.props.EnumProperty( name="Speed Unit", description="Unit of speed", - items=contract_units_to_items(contracts.SocketType.PhysicalSpeed), - default=default_unit_key_for(contracts.SocketType.PhysicalSpeed), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalSpeed), + default=default_unit_key_for(ST.PhysicalSpeed), + update=(lambda self, context: self.sync_prop("unit_speed", context)), ) unit_accel_scalar: bpy.props.EnumProperty( name="Accel Unit", description="Unit of acceleration", - items=contract_units_to_items(contracts.SocketType.PhysicalAccelScalar), - default=default_unit_key_for(contracts.SocketType.PhysicalAccelScalar), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalAccelScalar), + default=default_unit_key_for(ST.PhysicalAccelScalar), + update=(lambda self, context: self.sync_prop("unit_accel_scalar", context)), ) unit_force_scalar: bpy.props.EnumProperty( name="Force Scalar Unit", description="Unit of scalar force", - items=contract_units_to_items(contracts.SocketType.PhysicalForceScalar), - default=default_unit_key_for(contracts.SocketType.PhysicalForceScalar), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalForceScalar), + default=default_unit_key_for(ST.PhysicalForceScalar), + update=(lambda self, context: self.sync_prop("unit_force_scalar", context)), ) unit_accel_3d_vector: bpy.props.EnumProperty( name="Accel3D Unit", description="Unit of 3D vector acceleration", - items=contract_units_to_items(contracts.SocketType.PhysicalAccel3DVector), - default=default_unit_key_for(contracts.SocketType.PhysicalAccel3DVector), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalAccel3DVector), + default=default_unit_key_for(ST.PhysicalAccel3DVector), + update=(lambda self, context: self.sync_prop("unit_accel_3d_vector", context)), ) unit_force_3d_vector: bpy.props.EnumProperty( name="Force3D Unit", description="Unit of 3D vector force", - items=contract_units_to_items(contracts.SocketType.PhysicalForce3DVector), - default=default_unit_key_for(contracts.SocketType.PhysicalForce3DVector), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalForce3DVector), + default=default_unit_key_for(ST.PhysicalForce3DVector), + update=(lambda self, context: self.sync_prop("unit_force_3d_vector", context)), ) unit_freq: bpy.props.EnumProperty( name="Freq Unit", description="Unit of frequency", - items=contract_units_to_items(contracts.SocketType.PhysicalFreq), - default=default_unit_key_for(contracts.SocketType.PhysicalFreq), - update=(lambda self, context: self.trigger_updates()), - ) - unit_vac_wl: bpy.props.EnumProperty( - name="VacWL Unit", - description="Unit of vacuum wavelength", - items=contract_units_to_items(contracts.SocketType.PhysicalVacWL), - default=default_unit_key_for(contracts.SocketType.PhysicalVacWL), - update=(lambda self, context: self.trigger_updates()), + items=contract_units_to_items(ST.PhysicalFreq), + default=default_unit_key_for(ST.PhysicalFreq), + update=(lambda self, context: self.sync_prop("unit_freq", context)), ) #################### # - UI #################### - def draw_label_row(self, label_col_row: bpy.types.UILayout, text) -> None: - label_col_row.label(text=text) - label_col_row.prop(self, "show_definition", toggle=True, text="", icon="MOD_LENGTH") + def draw_label_row(self, row: bpy.types.UILayout, text) -> None: + row.label(text=text) + row.prop( + self, "show_definition", toggle=True, text="", icon="MOD_LENGTH" + ) def draw_value(self, col: bpy.types.UILayout) -> None: if self.show_definition: - col_row=col.row(align=True) + # TODO: We need panels instead of rows!! + col_row=col.row(align=False) col_row.alignment = "EXPAND" col_row.prop(self, "unit_time", text="") col_row.prop(self, "unit_angle", text="") - col_row=col.row(align=True) + col_row=col.row(align=False) col_row.alignment = "EXPAND" col_row.prop(self, "unit_length", text="") col_row.prop(self, "unit_area", text="") @@ -237,14 +248,9 @@ class PhysicalUnitSystemBLSocket(base.BLSocket): # - Default Value #################### @property - def default_value(self) -> sp.Expr: - ST = contracts.SocketType - SM = lambda socket_type: contracts.SocketType_to_units[ - socket_type - ]["values"] - + def value(self) -> dict[ST, SympyExpr]: return { - socket_type: SM(socket_type)[socket_unit_prop] + socket_type: SU(socket_type)[socket_unit_prop] for socket_type, socket_unit_prop in [ (ST.PhysicalTime, self.unit_time), (ST.PhysicalAngle, self.unit_angle), @@ -271,17 +277,12 @@ class PhysicalUnitSystemBLSocket(base.BLSocket): (ST.PhysicalVacWL, self.unit_vac_wl), ] } - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass #################### # - Socket Configuration #################### class PhysicalUnitSystemSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalUnitSystem - label: str + socket_type: ST = ST.PhysicalUnitSystem show_by_default: bool = False diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/vac_wl_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/vac_wl_socket.py deleted file mode 100644 index bdda73b..0000000 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/vac_wl_socket.py +++ /dev/null @@ -1,54 +0,0 @@ -import typing as typ - -import bpy -import pydantic as pyd - -from .. import base -from ... import contracts - -#################### -# - Blender Socket -#################### -class PhysicalVacWLBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalVacWL - bl_label = "PhysicalVacWL" - use_units = True - - #################### - # - Properties - #################### - raw_value: bpy.props.FloatProperty( - name="Unitless Vacuum Wavelength", - description="Represents the unitless part of the vacuum wavelength", - default=0.0, - precision=6, - update=(lambda self, context: self.trigger_updates()), - ) - - #################### - # - Default Value - #################### - @property - def default_value(self) -> None: - return self.raw_value * self.unit - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - self.raw_value = self.value_as_unit(value) - -#################### -# - Socket Configuration -#################### -class PhysicalVacWLSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalVacWL - label: str - - def init(self, bl_socket: PhysicalVacWLBLSocket) -> None: - pass - -#################### -# - Blender Registration -#################### -BL_REGISTER = [ - PhysicalVacWLBLSocket, -] diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/volume_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/volume_socket.py index 97b5d6b..2d2f300 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/volume_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/volume_socket.py @@ -5,25 +5,15 @@ import sympy as sp import sympy.physics.units as spu import pydantic as pyd +from .....utils.pydantic_sympy import SympyExpr from .. import base -from ... import contracts +from ... import contracts as ct -class PhysicalVolumeBLSocket(base.BLSocket): - socket_type = contracts.SocketType.PhysicalVolume - bl_label = "Physical Volume" +class PhysicalVolumeBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.PhysicalVolume + bl_label = "Volume" use_units = True - compatible_types = { - sp.Expr: { - lambda self, v: v.is_real, - lambda self, v: len(v.free_symbols) == 0, - lambda self, v: any( - contracts.is_exactly_expressed_as_unit(v, unit) - for unit in self.units.values() - ) - }, - } - #################### # - Properties #################### @@ -32,56 +22,32 @@ class PhysicalVolumeBLSocket(base.BLSocket): description="Represents the unitless part of the area", default=0.0, precision=6, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) #################### # - Socket UI #################### - def draw_label_row(self, label_col_row: bpy.types.UILayout, text: str) -> None: - """Draw the value of the area, including a toggle for - specifying the active unit. - """ - label_col_row.label(text=text) - #label_col_row.prop(self, "raw_value", text="") - label_col_row.prop(self, "raw_unit", text="") - def draw_value(self, col: bpy.types.UILayout) -> None: - col_row = col.row(align=True) - col_row.prop(self, "raw_value", text="") - #col_row.prop(self, "unit", text="") - + col.prop(self, "raw_value", text="") #################### - # - Computation of Default Value + # - Default Value #################### @property - def default_value(self) -> sp.Expr: - """Return the area as a sympy expression, which is a pure real - number perfectly expressed as the active unit. - - Returns: - The area as a sympy expression (with units). - """ - + def value(self) -> SympyExpr: return self.raw_value * self.unit - @default_value.setter - def default_value(self, value: typ.Any) -> None: - """Set the area from a sympy expression, including any required - unit conversions to normalize the input value to the selected - units. - """ - - self.raw_value = self.value_as_unit(value) + @value.setter + def value(self, value: SympyExpr) -> None: + self.raw_value = spu.convert_to(value, self.unit) / self.unit #################### # - Socket Configuration #################### class PhysicalVolumeSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.PhysicalVolume - label: str + socket_type: ct.SocketType = ct.SocketType.PhysicalVolume - default_unit: typ.Any | None = None + default_unit: SympyExpr | None = None def init(self, bl_socket: PhysicalVolumeBLSocket) -> None: if self.default_unit: diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/__init__.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/__init__.py new file mode 100644 index 0000000..4183e17 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/__init__.py @@ -0,0 +1,7 @@ +from . import cloud_task +Tidy3DCloudTaskSocketDef = cloud_task.Tidy3DCloudTaskSocketDef + + +BL_REGISTER = [ + *cloud_task.BL_REGISTER, +] diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py new file mode 100644 index 0000000..cc57077 --- /dev/null +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py @@ -0,0 +1,315 @@ +import typing as typ +import tempfile + +import bpy +import pydantic as pyd +import tidy3d as td +import tidy3d.web as _td_web + +from .....utils.auth_td_web import g_td_web, is_td_web_authed +from .. import base +from ... import contracts as ct + +#################### +# - Tidy3D Folder/Task Management +#################### +TD_FOLDERS = None +## TODO: Keep this data serialized in each node, so it works offline and saves/loads correctly (then we can try/except when the network fails). +## - We should consider adding some kind of serialization-backed instance data to the node base class... +## - We could guard it behind a feature, 'use_node_data_store' for example. + +def g_td_folders(): + global TD_FOLDERS + + if TD_FOLDERS is not None: return TD_FOLDERS + + # Populate Folders Cache & Return + TD_FOLDERS = { + cloud_folder.folder_name: None + for cloud_folder in _td_web.core.task_core.Folder.list() + } + return TD_FOLDERS + +def g_td_tasks(cloud_folder_name: str): + global TD_FOLDERS + + # Retrieve Cached Tasks + if (_tasks := TD_FOLDERS.get(cloud_folder_name)) is not None: + return _tasks + + # Retrieve Cloud Folder (if exists) + try: + cloud_folder = _td_web.core.task_core.Folder.get(cloud_folder_name) + except AttributeError as err: + # Folder Doesn't Exist + TD_FOLDERS = None + return [] + + # Return Tasks as List (also empty) + if (tasks := cloud_folder.list_tasks()) is None: + tasks = [] + + # Populate Cloud-Folder Cache & Return + TD_FOLDERS[cloud_folder_name] = [ + task + for task in tasks + ] + return TD_FOLDERS[cloud_folder_name] + +class BlenderMaxwellRefreshTDFolderList(bpy.types.Operator): + bl_idname = "blender_maxwell.refresh_td_folder_list" + bl_label = "Refresh Tidy3D Folder List" + bl_description = "Refresh the cached Tidy3D folder list" + bl_options = {'REGISTER'} + + @classmethod + def poll(cls, context): + space = context.space_data + return ( + space.type == 'NODE_EDITOR' + and space.node_tree is not None + and space.node_tree.bl_idname == "MaxwellSimTreeType" + and is_td_web_authed() + ) + + def execute(self, context): + global TD_FOLDERS + + TD_FOLDERS = None + return {'FINISHED'} + +class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.Tidy3DCloudTask + bl_label = "Tidy3D Cloud Sim" + + #################### + # - Properties + #################### + task_exists: bpy.props.BoolProperty( + name="Cloud Task Should Exist", + description="Whether or not the cloud task referred to should exist", + default=False, + ) + + api_key: bpy.props.StringProperty( + name="API Key", + description="API Key for the Tidy3D Cloud", + default="", + options={"SKIP_SAVE"}, + subtype="PASSWORD", + ) + + existing_folder_name: bpy.props.EnumProperty( + name="Folder of Cloud Tasks", + description="An existing folder on the Tidy3D Cloud", + items=lambda self, context: self.retrieve_folders(context), + update=(lambda self, context: self.sync_prop("existing_folder_name", context)), + ) + existing_task_id: bpy.props.EnumProperty( + name="Existing Cloud Task", + description="An existing task on the Tidy3D Cloud, within the given folder", + items=lambda self, context: self.retrieve_tasks(context), + update=(lambda self, context: self.sync_prop("existing_task_id", context)), + ) + new_task_name: bpy.props.StringProperty( + name="New Cloud Task Name", + description="Name of a new task to submit to the Tidy3D Cloud", + default="", + update=(lambda self, context: self.sync_new_task(context)), + ) + + lock_nonauth_interface: bpy.props.BoolProperty( + name="Lock the non-Auth Interface", + description="Declares that the non-auth interface should be locked", + default=False, + ) + + def retrieve_folders(self, context) -> list[tuple]: + if not is_td_web_authed: return [] + ## What if there are no folders? + + return [ + ( + folder_name, + folder_name, + folder_name, + ) + for folder_name in g_td_folders() + ] + + def retrieve_tasks(self, context) -> list[tuple]: + if not is_td_web_authed: return [] + if not (cloud_tasks := g_td_tasks(self.existing_folder_name)): + return [("NONE", "None", "No tasks in folder")] + + return [ + ( + ## Task ID + task.task_id, + + ## Task Dropdown Names + " ".join([ + task.taskName, + "(" + task.created_at.astimezone().strftime( + '%y-%m-%d @ %H:%M %Z' + ) + ")", + ]), + + ## Task Description + { + "draft": "Task has been uploaded, but not run", + "initialized": "Task is initializing", + "queued": "Task is queued for simulation", + "preprocessing": "Task is pre-processing", + "running": "Task is currently running", + "postprocessing": "Task is post-processing", + "success": "Task ran successfully, costing {task.real_flex_unit} credits", + "error": "Task ran, but an error occurred", + }[task.status], + + ## Status Icon + { + "draft": "SEQUENCE_COLOR_08", + "initialized": "SHADING_SOLID", + "queued": "SEQUENCE_COLOR_03", + "preprocessing": "SEQUENCE_COLOR_02", + "running": "SEQUENCE_COLOR_05", + "postprocessing": "SEQUENCE_COLOR_06", + "success": "SEQUENCE_COLOR_04", + "error": "SEQUENCE_COLOR_01", + }[task.status], + + ## Unique Number + i, + ) + for i, task in enumerate( + sorted(cloud_tasks, key=lambda el: el.created_at, reverse=True) + ) + ] + + #################### + # - Task Sync Methods + #################### + def sync_new_task(self, context): + if self.new_task_name == "": return + + if self.new_task_name in { + task.taskName + for task in g_td_tasks(self.existing_folder_name) + }: + self.new_task_name = "" + + self.sync_prop("new_task_name", context) + + def sync_task_loaded(self, loaded_task_id: str | None): + """Called whenever a particular task has been loaded. + + This resets the 'new_task_name' (if any), sets the dropdown to the new loaded task (which must be in the already-selected folder) (or, if input is None, leaves the selection alone), locks the socket UI (though NEVER the API authentication interface), and declares that the specified task exists. + """ + global TD_FOLDERS + ## TODO: This doesn't work with a linked socket. It should. + + if not (TD_FOLDERS is None): + TD_FOLDERS[self.existing_folder_name] = None + + if loaded_task_id is not None: + self.existing_task_id = loaded_task_id + + self.new_task_name = "" + self.lock_nonauth_interface = True + self.task_exists = True + + def sync_task_status_change(self, running_task_id: str): + global TD_FOLDERS + ## TODO: This doesn't work with a linked socket. It should. + + if not (TD_FOLDERS is None): + TD_FOLDERS[self.existing_folder_name] = None + + def sync_task_released(self, specify_new_task: bool = False): + ## TODO: This doesn't work with a linked socket. It should. + self.new_task_name = "" + self.lock_nonauth_interface = False + self.task_exists = not specify_new_task + + #################### + # - Socket UI + #################### + def draw_label_row(self, row: bpy.types.UILayout, text: str): + row.label(text=text) + + auth_icon = "CHECKBOX_HLT" if is_td_web_authed() else "CHECKBOX_DEHLT" + row.operator( + "blender_maxwell.refresh_td_auth", + text="", + icon=auth_icon, + ) + + def draw_value(self, col: bpy.types.UILayout) -> None: + if is_td_web_authed(): + if self.lock_nonauth_interface: col.enabled = False + else: col.enabled = True + + row = col.row() + row.label(icon="FILE_FOLDER") + row.prop(self, "existing_folder_name", text="") + row.operator( + BlenderMaxwellRefreshTDFolderList.bl_idname, + text="", + icon="FILE_REFRESH", + ) + + if not self.task_exists: + row = col.row() + row.label(icon="SEQUENCE_COLOR_04") + row.prop(self, "new_task_name", text="") + + if self.task_exists: + row = col.row() + else: + col.separator(factor=1.0) + box = col.box() + row = box.row() + + row.label(icon="NETWORK_DRIVE") + row.prop(self, "existing_task_id", text="") + + else: + col.enabled = True + row = col.row() + row.alignment="CENTER" + row.label(text="Tidy3D API Key") + + row = col.row() + row.prop(self, "api_key", text="") + + @property + def value(self) -> str | None: + if self.task_exists: + if self.existing_task_id == "NONE": return None + return self.existing_task_id + + return dict( + task_name=self.new_task_name, + folder_name=self.existing_folder_name, + ) + +#################### +# - Socket Configuration +#################### +class Tidy3DCloudTaskSocketDef(pyd.BaseModel): + socket_type: ct.SocketType = ct.SocketType.Tidy3DCloudTask + + task_exists: bool + + def init(self, bl_socket: Tidy3DCloudTaskBLSocket) -> None: + bl_socket.task_exists = self.task_exists + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + BlenderMaxwellRefreshTDFolderList, + Tidy3DCloudTaskBLSocket, +] + diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/complex_2d_vector_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/complex_2d_vector_socket.py index 537d6e4..6d60fac 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/complex_2d_vector_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/complex_2d_vector_socket.py @@ -3,32 +3,20 @@ import typing as typ import pydantic as pyd from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class Complex2DVectorBLSocket(base.BLSocket): - socket_type = contracts.SocketType.Complex2DVector - bl_label = "Complex2DVector" - - #################### - # - Default Value - #################### - @property - def default_value(self) -> None: - pass - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass +class Complex2DVectorBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.Complex2DVector + bl_label = "Complex 2D Vector" #################### # - Socket Configuration #################### class Complex2DVectorSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.Complex2DVector - label: str + socket_type: ct.SocketType = ct.SocketType.Complex2DVector def init(self, bl_socket: Complex2DVectorBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/complex_3d_vector_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/complex_3d_vector_socket.py index 6d70e99..15ae9ef 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/complex_3d_vector_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/complex_3d_vector_socket.py @@ -3,32 +3,20 @@ import typing as typ import pydantic as pyd from .. import base -from ... import contracts +from ... import contracts as ct #################### # - Blender Socket #################### -class Complex3DVectorBLSocket(base.BLSocket): - socket_type = contracts.SocketType.Complex3DVector - bl_label = "Complex3DVector" - - #################### - # - Default Value - #################### - @property - def default_value(self) -> None: - pass - - @default_value.setter - def default_value(self, value: typ.Any) -> None: - pass +class Complex3DVectorBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.Complex3DVector + bl_label = "Complex 3D Vector" #################### # - Socket Configuration #################### class Complex3DVectorSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.Complex3DVector - label: str + socket_type: ct.SocketType = ct.SocketType.Complex3DVector def init(self, bl_socket: Complex3DVectorBLSocket) -> None: pass diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/real_2d_vector_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/real_2d_vector_socket.py index 3d92f78..25df158 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/real_2d_vector_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/real_2d_vector_socket.py @@ -4,14 +4,22 @@ import bpy import sympy as sp import pydantic as pyd +from .....utils.pydantic_sympy import ConstrSympyExpr from .. import base -from ... import contracts +from ... import contracts as ct +Real2DVector = ConstrSympyExpr( + allow_variables=False, + allow_units=False, + allowed_sets={"integer", "rational", "real"}, + allowed_structures={"matrix"}, + allowed_matrix_shapes={(2, 1)}, +) #################### # - Blender Socket #################### -class Real2DVectorBLSocket(base.BLSocket): - socket_type = contracts.SocketType.Real2DVector +class Real2DVectorBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.Real2DVector bl_label = "Real2DVector" #################### @@ -23,29 +31,36 @@ class Real2DVectorBLSocket(base.BLSocket): size=2, default=(0.0, 0.0), precision=4, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + #################### # - Computation of Default Value #################### @property - def default_value(self) -> sp.Expr: - return tuple(self.raw_value) + def value(self) -> Real2DVector: + return sp.Matrix(tuple(self.raw_value)) - @default_value.setter - def default_value(self, value: typ.Any) -> None: + @value.setter + def value(self, value: Real2DVector) -> None: self.raw_value = tuple(value) #################### # - Socket Configuration #################### class Real2DVectorSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.Real2DVector - label: str + socket_type: ct.SocketType = ct.SocketType.Real2DVector + + default_value: Real2DVector = sp.Matrix([0.0, 0.0]) def init(self, bl_socket: Real2DVectorBLSocket) -> None: - pass + bl_socket.value = self.default_value #################### # - Blender Registration diff --git a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/real_3d_vector_socket.py b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/real_3d_vector_socket.py index d36dcde..1bdced1 100644 --- a/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/real_3d_vector_socket.py +++ b/code/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/vector/real_3d_vector_socket.py @@ -4,48 +4,64 @@ import bpy import sympy as sp import pydantic as pyd +from .....utils.pydantic_sympy import ConstrSympyExpr from .. import base -from ... import contracts +from ... import contracts as ct + +Real3DVector = ConstrSympyExpr( + allow_variables=False, + allow_units=False, + allowed_sets={"integer", "rational", "real"}, + allowed_structures={"matrix"}, + allowed_matrix_shapes={(3, 1)}, +) #################### # - Blender Socket #################### -class Real3DVectorBLSocket(base.BLSocket): - socket_type = contracts.SocketType.Real3DVector - bl_label = "Real3DVector" +class Real3DVectorBLSocket(base.MaxwellSimSocket): + socket_type = ct.SocketType.Real3DVector + bl_label = "Real 3D Vector" #################### # - Properties #################### raw_value: bpy.props.FloatVectorProperty( - name="Unitless 3D Vector (global coordinate system)", + name="Real 3D Vector", description="Represents a real 3D (coordinate) vector", size=3, default=(0.0, 0.0, 0.0), precision=4, - update=(lambda self, context: self.trigger_updates()), + update=(lambda self, context: self.sync_prop("raw_value", context)), ) + #################### + # - Socket UI + #################### + def draw_value(self, col: bpy.types.UILayout) -> None: + col.prop(self, "raw_value", text="") + #################### # - Computation of Default Value #################### @property - def default_value(self) -> sp.Expr: - return tuple(self.raw_value) + def value(self) -> Real3DVector: + return sp.Matrix(tuple(self.raw_value)) - @default_value.setter - def default_value(self, value: typ.Any) -> None: + @value.setter + def value(self, value: Real3DVector) -> None: self.raw_value = tuple(value) #################### # - Socket Configuration #################### class Real3DVectorSocketDef(pyd.BaseModel): - socket_type: contracts.SocketType = contracts.SocketType.Real3DVector - label: str + socket_type: ct.SocketType = ct.SocketType.Real3DVector + + default_value: Real3DVector = sp.Matrix([0.0, 0.0, 0.0]) def init(self, bl_socket: Real3DVectorBLSocket) -> None: - pass + bl_socket.value = self.default_value #################### # - Blender Registration diff --git a/code/blender_maxwell/node_trees/maxwell_viz_nodes/__init__.py b/code/blender_maxwell/node_trees/maxwell_viz_nodes/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/code/blender_maxwell/operators/__init__.py b/code/blender_maxwell/operators/__init__.py index 76a0da7..416f268 100644 --- a/code/blender_maxwell/operators/__init__.py +++ b/code/blender_maxwell/operators/__init__.py @@ -1,7 +1,14 @@ from . import install_deps from . import uninstall_deps +from . import connect_viewer +from . import refresh_td_auth BL_REGISTER = [ *install_deps.BL_REGISTER, *uninstall_deps.BL_REGISTER, + *connect_viewer.BL_REGISTER, + *refresh_td_auth.BL_REGISTER, +] +BL_KMI_REGISTER = [ + *connect_viewer.BL_KMI_REGISTER, ] diff --git a/code/blender_maxwell/operators/connect_viewer.py b/code/blender_maxwell/operators/connect_viewer.py new file mode 100644 index 0000000..b76de95 --- /dev/null +++ b/code/blender_maxwell/operators/connect_viewer.py @@ -0,0 +1,67 @@ +import bpy + +class BlenderMaxwellConnectViewer(bpy.types.Operator): + bl_idname = "blender_maxwell.connect_viewer" + bl_label = "Connect Viewer to Active" + bl_description = "Connect active node to Viewer Node" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + space = context.space_data + return ( + space.type == 'NODE_EDITOR' + and space.node_tree is not None + and space.node_tree.bl_idname == "MaxwellSimTreeType" + ) + + def invoke(self, context, event): + node_tree = context.space_data.node_tree + mlocx = event.mouse_region_x + mlocy = event.mouse_region_y + bpy.ops.node.select( + extend=False, + location=(mlocx, mlocy), + ) + select_node = context.selected_nodes[0] + + for node in node_tree.nodes: + if node.bl_idname == "ViewerNodeType": + viewer_node = node + break + else: + viewer_node = node_tree.nodes.new("ViewerNodeType") + viewer_node.location.x = select_node.location.x + 250 + viewer_node.location.y = select_node.location.y + select_node.select = False + + new_link = True + for link in viewer_node.inputs[0].links: + if link.from_node.name == select_node.name: + new_link = False + continue + node_tree.links.remove(link) + + if new_link: + node_tree.links.new(select_node.outputs[0], viewer_node.inputs[0]) + return {'FINISHED'} + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + BlenderMaxwellConnectViewer, +] + +BL_KMI_REGISTER = [ + dict( + _=( + BlenderMaxwellConnectViewer.bl_idname, + "LEFTMOUSE", + "PRESS", + ), + ctrl=True, ## CTRL + shift=True, ## Shift + alt=False, ## Alt + ), +] diff --git a/code/blender_maxwell/operators/install_deps.py b/code/blender_maxwell/operators/install_deps.py index 47c913b..85f5209 100644 --- a/code/blender_maxwell/operators/install_deps.py +++ b/code/blender_maxwell/operators/install_deps.py @@ -9,7 +9,7 @@ from . import types class BlenderMaxwellInstallDependenciesOperator(bpy.types.Operator): bl_idname = types.BlenderMaxwellInstallDependencies bl_label = "Install Dependencies for Blender Maxwell Addon" - + def execute(self, context): addon_dir = Path(__file__).parent.parent requirements_path = addon_dir / 'requirements.txt' @@ -58,3 +58,5 @@ class BlenderMaxwellInstallDependenciesOperator(bpy.types.Operator): BL_REGISTER = [ BlenderMaxwellInstallDependenciesOperator, ] + +BL_KMI_REGISTER = [] diff --git a/code/blender_maxwell/operators/refresh_td_auth.py b/code/blender_maxwell/operators/refresh_td_auth.py new file mode 100644 index 0000000..38d487e --- /dev/null +++ b/code/blender_maxwell/operators/refresh_td_auth.py @@ -0,0 +1,30 @@ +import bpy +from ..utils.auth_td_web import is_td_web_authed + +class BlenderMaxwellRefreshTDAuth(bpy.types.Operator): + bl_idname = "blender_maxwell.refresh_td_auth" + bl_label = "Refresh Tidy3D Auth" + bl_description = "Refresh the authentication of Tidy3D Web API" + bl_options = {'REGISTER'} + + @classmethod + def poll(cls, context): + space = context.space_data + return ( + space.type == 'NODE_EDITOR' + and space.node_tree is not None + and space.node_tree.bl_idname == "MaxwellSimTreeType" + ) + + def invoke(self, context, event): + is_td_web_authed(force_check=True) + return {'FINISHED'} + +#################### +# - Blender Registration +#################### +BL_REGISTER = [ + BlenderMaxwellRefreshTDAuth, +] + +BL_KMI_REGISTER = [] diff --git a/code/blender_maxwell/operators/types.py b/code/blender_maxwell/operators/types.py index 1ad0607..1ea1d70 100644 --- a/code/blender_maxwell/operators/types.py +++ b/code/blender_maxwell/operators/types.py @@ -5,3 +5,5 @@ import bpy #################### BlenderMaxwellInstallDependencies = "blender_maxwell.install_dependencies" BlenderMaxwellUninstallDependencies = "blender_maxwell.uninstall_dependencies" +BlenderMaxwellConnectViewer = "blender_maxwell.connect_viewer" +BlenderMaxwellRefreshRDAuth = "blender_maxwell.refresh_td_auth" diff --git a/code/blender_maxwell/operators/uninstall_deps.py b/code/blender_maxwell/operators/uninstall_deps.py index 5f8043b..fc26dd0 100644 --- a/code/blender_maxwell/operators/uninstall_deps.py +++ b/code/blender_maxwell/operators/uninstall_deps.py @@ -28,3 +28,4 @@ class BlenderMaxwellUninstallDependenciesOperator(bpy.types.Operator): BL_REGISTER = [ BlenderMaxwellUninstallDependenciesOperator, ] +BL_KMI_REGISTER = [] diff --git a/code/blender_maxwell/utils/auth_td_web.py b/code/blender_maxwell/utils/auth_td_web.py new file mode 100644 index 0000000..34ac704 --- /dev/null +++ b/code/blender_maxwell/utils/auth_td_web.py @@ -0,0 +1,57 @@ +import types +import tidy3d.web as td_web + +AUTHENTICATED = False + +def td_auth(api_key: str): + # Check for API Key + if api_key: + msg = "API Key must be defined to authenticate" + raise ValueError(msg) + + # Perform Authentication + td_web.configure(api_key) + try: + td_web.test() + except: + msg = "Tidy3D Cloud Authentication Failed" + raise ValueError(msg) + + AUTHENTICATED = True + +def is_td_web_authed(force_check: bool = False) -> bool: + """Checks whether `td_web` is authenticated, using the cache. + The result is heuristically accurate. + + If accuracy must be guaranteed, an aliveness-check can be performed by setting `force_check=True`. + This comes at a performance penalty, as a web request must be made; thus, `force_check` is not appropriate for hot-paths like `draw` functions. + + If a check is performed + """ + global AUTHENTICATED + + # Return Cached Authentication + if not force_check: + return AUTHENTICATED + + # Re-Check Authentication + try: + td_web.test() + AUTHENTICATED = True ## Guarantee cache value to True. + return True + except: + AUTHENTICATED = False ## Guarantee cache value to False. + return False + +def g_td_web(api_key: str, force_check: bool = False) -> types.ModuleType: + """Returns a `tidy3d.web` module object that is already authenticated using the given API key. + + The authentication status is cached using a global module-level variable, `AUTHENTICATED`. + """ + global AUTHENTICATED + + # Check Cached Authentication + if not is_td_web_authed(force_check=force_check): + td_auth(api_key) + + return td_web diff --git a/code/blender_maxwell/utils/extra_sympy_units.py b/code/blender_maxwell/utils/extra_sympy_units.py index 372632c..c7225b6 100644 --- a/code/blender_maxwell/utils/extra_sympy_units.py +++ b/code/blender_maxwell/utils/extra_sympy_units.py @@ -1,6 +1,27 @@ import sympy as sp import sympy.physics.units as spu +#################### +# - Useful Methods +#################### +def uses_units(expression: sp.Expr) -> bool: + """Checks if an expression uses any units (`Quantity`).""" + + for arg in sp.preorder_traversal(expression): + if isinstance(arg, spu.Quantity): + return True + return False + +# Function to return a set containing all units used in the expression +def get_units(expression: sp.Expr): + """Gets all the units of an expression (as `Quantity`).""" + + return { + arg + for arg in sp.preorder_traversal(expression) + if isinstance(arg, spu.Quantity) + } + #################### # - Force #################### @@ -35,3 +56,25 @@ petahertz.set_global_relative_scale_factor(spu.peta, spu.hertz) exahertz = EHz = spu.Quantity("exahertz", abbrev="EHz") exahertz.set_global_relative_scale_factor(spu.exa, spu.hertz) + +#################### +# - Sympy Expression Typing +#################### +#ALL_UNIT_SYMBOLS = { +# unit +# for unit in spu.__dict__.values() +# if isinstance(unit, spu.Quantity) +#} +#def has_units(expr: sp.Expr): +# return any( +# symbol in ALL_UNIT_SYMBOLS +# for symbol in expr.atoms(sp.Symbol) +# ) +#def is_exactly_expressed_as_unit(expr: sp.Expr, unit) -> bool: +# #try: +# converted_expr = expr / unit +# +# return ( +# converted_expr.is_number +# and not converted_expr.has(spu.Quantity) +# ) diff --git a/code/blender_maxwell/utils/pydantic_sympy.py b/code/blender_maxwell/utils/pydantic_sympy.py new file mode 100644 index 0000000..a71017f --- /dev/null +++ b/code/blender_maxwell/utils/pydantic_sympy.py @@ -0,0 +1,161 @@ +import typing as typ +import typing_extensions as typx + +import pydantic as pyd +from pydantic_core import core_schema as pyd_core_schema +import sympy as sp +import sympy.physics.units as spu + +from . import extra_sympy_units as spuex + +#################### +# - Missing Basics +#################### +AllowedSympyExprs = sp.Expr | sp.MatrixBase +Complex = typx.Annotated[ + complex, + pyd.GetPydanticSchema( + lambda tp, handler: pyd_core_schema.no_info_after_validator_function( + lambda x: x, handler(tp) + ) + ), +] + +#################### +# - Custom Pydantic Type for sp.Expr +#################### +class _SympyExpr: + @classmethod + def __get_pydantic_core_schema__( + cls, + _source_type: AllowedSympyExprs, + _handler: pyd.GetCoreSchemaHandler, + ) -> pyd_core_schema.CoreSchema: + def validate_from_str(value: str) -> AllowedSympyExprs: + if not isinstance(value, str): + msg = f"Value {value} is not a string" + raise ValueError(msg) + + try: + return sp.sympify(value) + except ValueError as ex: + msg = f"Value {value} is not a `sympify`able string" + raise ValueError(msg) from ex + + def validate_from_expr(value: AllowedSympyExprs) -> AllowedSympyExprs: + if not ( + isinstance(value, sp.Expr) + or isinstance(value, sp.MatrixBase) + ): + msg = f"Value {value} is not a `sympy` expression" + raise ValueError(msg) + + return value + + from_expr_schema = pyd_core_schema.chain_schema([ + pyd_core_schema.no_info_plain_validator_function(validate_from_expr), + pyd_core_schema.no_info_plain_validator_function(validate_from_str), + ]) + return pyd_core_schema.json_or_python_schema( + json_schema=from_expr_schema, + python_schema=pyd_core_schema.union_schema( + [ + # check if it's an instance first before doing any further work + pyd_core_schema.is_instance_schema(AllowedSympyExprs), + from_expr_schema, + ] + ), + serialization=pyd_core_schema.plain_serializer_function_ser_schema( + lambda instance: str(instance) + ), + ) + +#################### +# - Configurable Expression Validation +#################### +SympyExpr = typx.Annotated[ + AllowedSympyExprs, + _SympyExpr, +] + +def ConstrSympyExpr( + # Feature Class + allow_variables: bool = True, + allow_units: bool = True, + + # Structure Class + allowed_sets: set[typx.Literal[ + "integer", "rational", "real", "complex" + ]] | None = None, + allowed_structures: set[typx.Literal[ + "scalar", "matrix" + ]] | None = None, + + # Element Class + allowed_symbols: set[sp.Symbol] | None = None, + allowed_units: set[spu.Quantity] | None = None, + + # Shape Class + allowed_matrix_shapes: set[tuple[int, int]] | None = None, +): + ## See `sympy` predicates: + ## - + def validate_expr(expr: AllowedSympyExprs): + if not ( + isinstance(expr, sp.Expr) + or isinstance(expr, sp.MatrixBase), + ): + ## NOTE: Must match AllowedSympyExprs union elements. + msg = f"expr '{expr}' is not an allowed Sympy expression ({AllowedSympyExprs})" + raise ValueError(msg) + + msgs = set() + + # Validate Feature Class + if (not allow_variables) and (len(expr.free_symbols) > 0): + msgs.add(f"allow_variables={allow_variables} does not match expression {expr}.") + if (not allow_units) and spuex.uses_units(expr): + msgs.add(f"allow_units={allow_units} does not match expression {expr}.") + + # Validate Structure Class + if allowed_sets and isinstance(expr, sp.Expr) and not any([ + { + "integer": expr.is_integer, + "rational": expr.is_rational, + "real": expr.is_real, + "complex": expr.is_complex, + }[allowed_set] + for allowed_set in allowed_sets + ]): + msgs.add(f"allowed_sets={allowed_sets} does not match expression {expr} (remember to add assumptions to symbols, ex. `x = sp.Symbol('x', real=True))") + if allowed_structures and not any([ + { + "matrix": isinstance(expr, sp.MatrixBase), + }[allowed_set] + for allowed_set in allowed_structures + if allowed_structures != "scalar" + ]): + msgs.add(f"allowed_structures={allowed_structures} does not match expression {expr} (remember to add assumptions to symbols, ex. `x = sp.Symbol('x', real=True))") + + # Validate Element Class + if allowed_symbols and expr.free_symbols.issubset(allowed_symbols): + msgs.add(f"allowed_symbols={allowed_symbols} does not match expression {expr}") + if allowed_units and spuex.get_units(expr).issubset(allowed_units): + msgs.add(f"allowed_units={allowed_units} does not match expression {expr}") + + # Validate Shape Class + if ( + allowed_matrix_shapes + and isinstance(expr, sp.MatrixBase) + ) and not (expr.shape in allowed_matrix_shapes): + msgs.add(f"allowed_matrix_shapes={allowed_matrix_shapes} does not match expression {expr} with shape {expr.shape}") + + # Error or Return + if msgs: raise ValueError(str(msgs)) + return expr + + return typx.Annotated[ + AllowedSympyExprs, + _SympyExpr, + pyd.AfterValidator(validate_expr), + ]