Compare commits

...

13 Commits

Author SHA1 Message Date
Sofus Albert Høgsbro Rose 1e420376fc
feat: Continue to add features.
See README.md.
2024-03-12 09:01:50 +01:00
Sofus Albert Høgsbro Rose 134bf0c358
refactor: Continuing large-scale alterations.
The big news is that GeoNodes Structure is now implemented,
under the new and vastly more robust chaining system.

Upload to Tidy3D cloud is tested. Next is Monitors!
2024-03-11 16:35:41 +01:00
Sofus Albert Høgsbro Rose 1ebb57cff7
refactor: Massive architectural changes.
See README.md for new, semi-finalized TODO list.
2024-03-10 11:56:37 +01:00
Sofus Albert Høgsbro Rose d95210dc34
feat: Various features (some very prototype).
It's very prototype-y. Cleanup pending.
2024-02-26 16:16:06 +01:00
Sofus Albert Høgsbro Rose 74d5a5daf8
feat: We did it, GeoNodes node w/live update!
We also implemented the TriMesh node, and established a strong
convention for updating nodes from sockets via. socket superclass
method. `trigger_updates`. It should be triggered as the
`update=` callback on **ALL PROPERTIES IN ALL SOCKETS**. This
method in turn calls the nodal `update()` function, which in turn
causes the node to chain-update all nodes linked to any output socket.

By default, `update()` is `pass`, so performance shouldn't be a concern,
but we should think about this deeper at some point.

Because update-chaining is done, we're ready for preview toggles on
node outputs. A lot of exciting things to do now!
2024-02-20 13:16:23 +01:00
Sofus Albert Høgsbro Rose 7344913c0e
feat: More sockets, nodes, fixes.
We're starting to have a very advanced socket-based language
for defining nodes. It's very practical already.

The priorities are
1. All the nodes!
2. Sockets, including default values, as needed.
3. Library constants, mediums.
4. Output socket previews w/geonodes, geonodes structures.
5. Utilities including arrays (and selected array multi-input)

The code is still very spaghetti. This is very much still the
"first, make it run" part of the system design.
2024-02-19 18:36:16 +01:00
Sofus Albert Høgsbro Rose 586d6fa74b
feat: Added accel socket, fixed default units. 2024-02-19 16:03:32 +01:00
Sofus Albert Høgsbro Rose de8d64b5b3
feat: Custom units, def. all SocketType units. 2024-02-19 15:58:39 +01:00
Sofus Albert Høgsbro Rose 3793175011
feat: Registered all nodes.
Also added several features including dynamic sockets
in nodes, abstracted units for sockets, and more.
2024-02-19 14:28:35 +01:00
Sofus Albert Høgsbro Rose 0bf6100e19
docs: Revised the node/socket plan. 2024-02-16 13:17:09 +01:00
Sofus Albert Høgsbro Rose b78dd8dd56
refactor: Far more well-functioning baseline. 2024-02-14 12:33:40 +01:00
Sofus Albert Høgsbro Rose b592ea4b10
refactor: Big categories, structure change.
We're back down to a single working node, but this was a very practical
refactor.
In general, very good progress towards making #2 easy to fulfill in
its entirety.

Bugs remain:

- Category discovery has big code smells and needs smoothing. Blender
  complains especially about wanting `_MT_` prefix/suffix on node
category submenu types. There's also a piece of registration logic in
category.py (big no no).
- I'd love to pass a `ruff`/`mypy` run before doubling down on node
  creation, especially to help manage the complex pieces of MP logic.
- Still needs socket-bound unit-awareness feat. sympy units, before
  doubling down on a data flow convention.
- Dependency management should also be smoothed out wrt. the user
  experience, with cached directories exposed in addon preferences.
2024-02-10 17:59:16 +01:00
Sofus Albert Høgsbro Rose a7fc66376b
feat: Somewhat working addon.
Solved a lot of problems related to bundled Python environment flushing
for reloading. However, we have a really solid framework for computing
node trees, and we can now both construct Tidy3D objects and noodle
them into the "Debug Printer". Next step is rote implementation of
relevant nodes, then live-visualization of the simulation setup.

See #2 for progress tracking.
2024-02-06 21:44:43 +01:00
191 changed files with 12425 additions and 0 deletions

5
.gitignore vendored
View File

@ -2,6 +2,11 @@
# - Standard Ignores
####################
dev
.cached-dependencies
__pycache__
*.blend1
*.blend2
*.blend3
####################

52
code/FUTURE.md 100644
View File

@ -0,0 +1,52 @@
# Projects / Plugins
## Larger Architectural Changes
[ ] Dedicated way of generating properties for sockets and nodes, incl. helping make better (less boilerplatey) use of callbacks
- Perhaps, we should also go 100% custom `PropertyGroup`, to the point that we have `nodes`, `sockets` and `props`.
- This would allow far simplified sockets (the more complex kinds), and help standardize use of ex. units in node properties.
- Having a dedicated base class for custom props would help avoid issues like forgetting to run `self.sync_prop` on every goddamn update method in every goddamn socket.
[ ] Dedicated way of handling node-specific operators without all the boilerplate.
## 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 <https://docs.flexcompute.com/projects/tidy3d/en/latest/notebooks/Symmetry.html>
[ ] Dispersive Model Fitting
[ ] Scattering Matrix Calculator
[ ] Resonance Finder
[ ] Adjoint Optimization
[ ] Design Space Exploration / Parameterization
## Preview Semantics
[ ] Node tree header toggle that toggles a modal operator on and off, which constantly checks the context of the selected nodes, and tries to `bl_select` them (which in turn, should cause the node base class to `bl_select` shit inside).
- Shouldn't survive a file save; always startup with this thing off.
[ ] Custom gizmos attached to preview toggles!
- There is a WIP for GN-driven gizmos: <https://projects.blender.org/blender/blender/pulls/112677>
- 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
[ ] Heat Solver
[ ] MEEP integration (<https://meep.readthedocs.io/en/latest/>)
- The main boost would be if we could setup a MEEP simulation entirely from a td.Simulation object.

399
code/README.md 100644
View File

@ -0,0 +1,399 @@
# Nodes
**LEGEND**:
- [-] Exists but doesn't quite work good enough.
- [x] Done to working degree (the standard is "good enough for the demo").
- See check marks underneath
- [?] Unsure whether we should do this.
## Inputs
[x] Wave Constant
- [ ] Implement export of frequency / wavelength array/range.
[-] Unit System
- [ ] Implement presets, including "Tidy3D" and "Blender", shown in the label row.
[ ] Constants / Scientific Constant
[x] Constants / Number Constant
[ ] Constants / Physical Constant
- [ ] Pol: Elliptical plot viz
- [ ] Pol: Poincare sphere viz
[x] Constants / Blender Constant
[x] Web / Tidy3D Web Importer
[ ] 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.
## Outputs
[x] Viewer
- [ ] **BIG ONE**: Remove image preview when disabling plots.
- [ ] Either enforce singleton, or find a way to have several viewers at the same time.
- [ ] A setting that live-previews just a value.
- [ ] Pop-up multiline string print as alternative to console print.
- [x] Toggleable auto-plot, auto-3D-preview, auto-value-view, (?)auto-text-view.
[x] Web Export / Tidy3D Web Exporter
- [ ] We need better ways of doing checks before uploading, like for monitor data size. Maybe a SimInfo node?
- [ ] We need to be able to "delete and re-upload" (or maybe just delete from the interface).
[x] 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
[ ] Sim Info
- [ ] Implement estimation of monitor storage
- [ ] Implement cost estimation
[ ] 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 (<https://docs.flexcompute.com/projects/tidy3d/en/latest/api/output_data.html>)
- [ ] Project field values onto a plane object (managed)
## Sources
[x] Temporal Shapes / Gaussian Pulse Temporal Shape
[x] 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
[x] Point Dipole Source
- [ ] Consider a "real" mesh - the empty kind of gets stuck inside of the sim domain.
[-] Plane Wave Source
- [ ] **IMPORTANT**: Fix the math so that an actually valid construction emerges!!
- [x] 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
[ ] 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
[x] GeoNodes Structure
- [x] Rewrite the `bl_socket_map.py`
- [x] 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: <https://docs.flexcompute.com/projects/tidy3d/en/latest/notebooks/Metalens.html>
[ ] 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
[x] Sim Domain
- [ ] By-Medium batching of Structures when building the td.Simulation object, which can have significant performance implications.
[x] Boundary Conds
- [x] 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
# GeoNodes
[ ] Tests / Monkey (suzanne deserves to be simulated, she may need manifolding up though :))
[ ] Tests / Wood Pile
[ ] 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
# Benchmark / Example Sims
[ ] Research-Grade Experiment
- Membrane 15nm thickness suspended in air
- Square lattice of holes period 900nm (900nm between each hole, air inside holes)
- Holes square radius 100nm
- Square lattice
- Analysis of transmission
- Guided mode resonance
[ ] Tunable Chiral Metasurface <https://docs.flexcompute.com/projects/tidy3d/en/latest/notebooks/TunableChiralMetasurface.html>
# Sockets
## Basic
[x] Any
[x] Bool
[x] String
- [ ] Rename from "Text"
[x] File Path
[x] Color
## Number
[x] Integer
[x] Rational
- [ ] Implement constrained SympyExpr check for Rational.
[x] Real
- [ ] Implement min/max for ex. 0..1 factor support.
- [ ] Implement constrained SympyExpr check for Rational.
[x] Complex
## Blender
[x] Object
- [ ] Implement default SocketDef object name
[x] Collection
- [ ] Implement default SocketDef collection name
[x] Image
- [ ] Implement default SocketDef image name
[x] GeoNodes
- [ ] Implement default SocketDef geonodes name
[x] Text
- [ ] Implement default SocketDef object name
## Maxwell
[x] Bound Conds
[ ] Bound Cond
[x] Medium
[ ] Medium Non-Linearity
[x] Source
[ ] Temporal Shape
- [ ] Sane-default pulses for easy access.
[ ] 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
[x] Cloud Task
- [ ] Implement switcher for API-key-having config filconfig file vs. direct entry of API key. It should be auto-filled with the config file when such a thing exists.
## Physical
[x] Unit System
- [ ] Implement more comprehensible UI; honestly, probably with the new panels (<https://developer.blender.org/docs/release_notes/4.1/python_api/>)
[x] Time
[x] Angle
[ ] Solid Angle (steradian)
[x] Frequency (hertz)
[ ] Angular Frequency (`rad*hertz`)
### Cartesian
[x] Length
[x] Area
[x] Volume
[ ] Point 1D
[ ] Point 2D
[x] Point 3D
[ ] Size 2D
[x] Size 3D
[ ] Rotation 3D
- [ ] Implement Euler methods
- [ ] Implement Quaternion methods
### Mechanical
[ ] Mass
[x] Speed
[ ] Velocity 3D
[x] Acceleration Scalar
[ ] Acceleration 3D
[x] Force Scalar
[ ] Force 3D
[ ] Pressure
### Energy
[ ] 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 (Stokes)
# 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.
# Architecture
## Registration and Contracts
[x] 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.
[?] Would be nice with some kind of indicator somewhere to help set good socket descriptions when using geonodes and wanting units.
## Managed Objects
[x] Implement modifier support on the managed BL object, with special attention paid to the needs of the GeoNodes socket.
- [x] 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).
## Utils or Services
[ ] Dedicated module for managing the interaction with the tidy3d cloud, to help nuke all the random caches out of existance.
## 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.
[ ] Custom callbacks when deleting a node (in `free()`), to ex. delete all previews with the viewer node.
## 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 (<https://developer.blender.org/docs/release_notes/4.1/python_api/>)
[ ] 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 <https://projects.blender.org/blender/blender/commit/14106150797a6ce35e006ffde18e78ea7ae67598> (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

View File

@ -0,0 +1,84 @@
bl_info = {
"name": "Maxwell Simulation and Visualization",
"blender": (4, 0, 2),
"category": "Node",
"description": "Custom node trees for defining and visualizing Maxwell simulation.",
"author": "Sofus Albert Høgsbro Rose",
"version": (0, 1),
"wiki_url": "https://git.sofus.io/dtu-courses/bsc_thesis",
"tracker_url": "https://git.sofus.io/dtu-courses/bsc_thesis/issues",
}
####################
# - sys.path Library Inclusion
####################
import sys
sys.path.insert(0, "/home/sofus/src/college/bsc_ge/thesis/code/.cached-dependencies")
## ^^ Placeholder
####################
# - Module Import
####################
if "bpy" not in locals():
import bpy
import nodeitems_utils
try:
from . import node_trees
from . import operators
from . import preferences
except ImportError:
import sys
sys.path.insert(0, "/home/sofus/src/college/bsc_ge/thesis/code/blender-maxwell")
import node_trees
import operators
import preferences
else:
import importlib
importlib.reload(node_trees)
####################
# - Registration
####################
BL_REGISTER = [
*node_trees.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()

View File

@ -0,0 +1,9 @@
from . import maxwell_sim_nodes
BL_REGISTER = [
*maxwell_sim_nodes.BL_REGISTER,
]
BL_NODE_CATEGORIES = [
*maxwell_sim_nodes.BL_NODE_CATEGORIES,
]

View File

@ -0,0 +1,21 @@
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
from . import categories
BL_REGISTER = [
*sockets.BL_REGISTER,
*node_tree.BL_REGISTER,
*nodes.BL_REGISTER,
*categories.BL_REGISTER,
]
BL_NODE_CATEGORIES = [
*categories.BL_NODE_CATEGORIES,
]

View File

@ -0,0 +1,251 @@
import typing as typ
import typing_extensions as typx
import pydantic as pyd
import sympy as sp
import sympy.physics.units as spu
import bpy
from ...utils import extra_sympy_units as spuex
from . import contracts as ct
from .contracts import SocketType as ST
from . import sockets as sck
# TODO: Caching?
# TODO: Move the manual labor stuff to contracts
BLSocketType = str ## A Blender-Defined Socket Type
BLSocketSize = int
DescType = str
Unit = typ.Any ## Type of a valid unit
####################
# - Socket to SocketDef
####################
SOCKET_DEFS = {
socket_type: getattr(
sck,
socket_type.value.removesuffix("SocketType") + "SocketDef",
)
for socket_type in ST
if hasattr(
sck,
socket_type.value.removesuffix("SocketType") + "SocketDef"
)
}
## TODO: Bit of a hack. Is it robust enough?
for socket_type in ST:
if not hasattr(
sck,
socket_type.value.removesuffix("SocketType") + "SocketDef",
):
print("Missing SocketDef for", socket_type.value)
####################
# - BL Socket Size Parser
####################
BL_SOCKET_3D_TYPE_PREFIXES = {
"NodeSocketVector",
"NodeSocketRotation",
}
BL_SOCKET_4D_TYPE_PREFIXES = {
"NodeSocketColor",
}
def size_from_bl_interface_socket(
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket
) -> typx.Literal[1, 2, 3, 4]:
"""Parses the `size`, aka. number of elements, contained within the `default_value` of a Blender interface socket.
Since there are no 2D sockets in Blender, the user can specify "2D" in the Blender socket's description to "promise" that only the first two values will be used.
When this is done, the third value is left entirely untouched by this entire system.
A hard-coded set of NodeSocket<Type> prefixes are used to determine which interface sockets are, in fact, 3D.
- For 3D sockets, a hard-coded list of Blender node socket types is used.
- Else, it is a 1D socket type.
"""
if bl_interface_socket.description.startswith("2D"): return 2
if any(
bl_interface_socket.socket_type.startswith(bl_socket_3d_type_prefix)
for bl_socket_3d_type_prefix in BL_SOCKET_3D_TYPE_PREFIXES
):
return 3
if any(
bl_interface_socket.socket_type.startswith(bl_socket_4d_type_prefix)
for bl_socket_4d_type_prefix in BL_SOCKET_4D_TYPE_PREFIXES
):
return 4
return 1
####################
# - BL Socket Type / Unit Parser
####################
def parse_bl_interface_socket(
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
) -> tuple[ST, sp.Expr | None]:
"""Parse a Blender interface socket by parsing its description, falling back to any direct type links.
Arguments:
bl_interface_socket: An interface socket associated with the global input to a node tree.
Returns:
The type of a corresponding MaxwellSimSocket, as well as a unit (if a particular unit was requested by the Blender interface socket).
"""
size = size_from_bl_interface_socket(bl_interface_socket)
# Determine Direct Socket Type
if (
direct_socket_type := ct.BL_SOCKET_DIRECT_TYPE_MAP.get(
(bl_interface_socket.socket_type, size)
)
) is None:
msg = "Blender interface socket has no mapping among 'MaxwellSimSocket's."
raise ValueError(msg)
# (Maybe) Return Direct Socket Type
## When there's no description, that's it; return.
if not ct.BL_SOCKET_DESCR_ANNOT_STRING in bl_interface_socket.description:
return (direct_socket_type, None)
# Parse Description for Socket Type
tokens = (
_tokens
if (_tokens := bl_interface_socket.description.split(" "))[0] != "2D"
else _tokens[1:]
) ## Don't include the "2D" token, if defined.
if (
socket_type := ct.BL_SOCKET_DESCR_TYPE_MAP.get(
(tokens[0], bl_interface_socket.socket_type, size)
)
) is None:
return (direct_socket_type, None) ## Description doesn't map to anything
# Determine Socket Unit (to use instead of "unit system")
## This is entirely OPTIONAL
socket_unit = None
if socket_type in ct.SOCKET_UNITS:
## Case: Unit is User-Defined
if len(tokens) > 1 and "(" in tokens[1] and ")" in tokens[1]:
# Compute (<unit_str>) as Unit Token
unit_token = tokens[1].removeprefix("(").removesuffix(")")
# Compare Unit Token to Valid Sympy-Printed Units
socket_unit = _socket_unit if (_socket_unit := [
unit
for unit in ct.SOCKET_UNITS[socket_type]["values"].values()
if str(unit) == unit_token
]) else ct.SOCKET_UNITS[socket_type]["values"][
ct.SOCKET_UNITS[socket_type]["default"]
]
## TODO: Enforce abbreviated sympy printing here, not globally
return (socket_type, socket_unit)
####################
# - BL Socket Interface Definition
####################
def socket_def_from_bl_interface_socket(
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
):
"""Computes an appropriate (no-arg) SocketDef from the given `bl_interface_socket`, by parsing it.
"""
return SOCKET_DEFS[
parse_bl_interface_socket(bl_interface_socket)[0]
]
####################
# - Extract Default Interface Socket Value
####################
def value_from_bl(
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
unit_system: dict | None = None,
) -> typ.Any:
"""Reads the value of any Blender socket, and writes its `default_value` to the `value` of any `MaxwellSimSocket`.
- If the size of the Blender socket is >1, then `value` is written to as a `sympy.Matrix`.
- If a unit system is given, then the Blender socket is matched to a `MaxwellSimSocket`, which is used to lookup an appropriate unit in the given `unit_system`.
"""
## TODO: Consider sympy.S()'ing the default_value
parsed_bl_socket_value = {
1: lambda: bl_interface_socket.default_value,
2: lambda: sp.Matrix(tuple(bl_interface_socket.default_value)[:2]),
3: lambda: sp.Matrix(tuple(bl_interface_socket.default_value)),
4: lambda: sp.Matrix(tuple(bl_interface_socket.default_value)),
}[size_from_bl_interface_socket(bl_interface_socket)]()
## The 'lambda' delays construction until size is determined
socket_type, unit = parse_bl_interface_socket(bl_interface_socket)
# Add Unit to Parsed (if relevant)
if unit is not None:
parsed_bl_socket_value *= unit
elif unit_system is not None:
parsed_bl_socket_value *= unit_system[socket_type]
return parsed_bl_socket_value
####################
# - Convert to Blender-Compatible Value
####################
def make_scalar_bl_compat(scalar: typ.Any) -> typ.Any:
"""Blender doesn't accept ex. Sympy numbers as values.
Therefore, we need to do some conforming.
Currently hard-coded; this is probably best.
"""
if isinstance(scalar, sp.Integer):
return int(scalar)
elif isinstance(scalar, sp.Float):
return float(scalar)
elif isinstance(scalar, sp.Rational):
return float(scalar)
elif isinstance(scalar, sp.Expr):
return float(scalar.n())
## TODO: More?
return scalar
def value_to_bl(
bl_interface_socket: bpy.types.NodeSocket,
value: typ.Any,
unit_system: dict | None = None,
) -> typ.Any:
socket_type, unit = parse_bl_interface_socket(bl_interface_socket)
# Set Socket
if unit is not None:
bl_socket_value = spu.convert_to(value, unit) / unit
elif (
unit_system is not None
and socket_type in unit_system
):
bl_socket_value = spu.convert_to(
value, unit_system[socket_type]
) / unit_system[socket_type]
else:
bl_socket_value = value
return {
1: lambda: make_scalar_bl_compat(bl_socket_value),
2: lambda: tuple([
make_scalar_bl_compat(bl_socket_value[0]),
make_scalar_bl_compat(bl_socket_value[1]),
bl_interface_socket.default_value[2]
## Don't touch (unused) 3rd bl_socket coordinate
]),
3: lambda: tuple([
make_scalar_bl_compat(el)
for el in bl_socket_value
]),
4: lambda: tuple([
make_scalar_bl_compat(el)
for el in bl_socket_value
]),
}[size_from_bl_interface_socket(bl_interface_socket)]()
## The 'lambda' delays construction until size is determined

View File

@ -0,0 +1,91 @@
## TODO: Refactor this whole horrible module.
import bpy
import nodeitems_utils
from . import contracts as ct
from .nodes import BL_NODES
DYNAMIC_SUBMENU_REGISTRATIONS = []
def mk_node_categories(
tree,
syllable_prefix = [],
#root = True,
):
global DYNAMIC_SUBMENU_REGISTRATIONS
items = []
# Add Node Items
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))
# Add Node Sub-Menus
for syllable, sub_tree in tree.items():
current_syllable_path = syllable_prefix + [syllable]
current_category = ct.NodeCategory[
"_".join(current_syllable_path)
]
# Build Items for Sub-Categories
subitems = mk_node_categories(
sub_tree,
current_syllable_path,
)
if len(subitems) == 0: continue
# Define Dynamic Node Submenu
def draw_factory(items):
def draw(self, context):
for nodeitem_or_submenu in items:
if isinstance(
nodeitem_or_submenu,
nodeitems_utils.NodeItem,
):
nodeitem = nodeitem_or_submenu
op_add_node_cfg = self.layout.operator(
"node.add_node",
text=nodeitem.label,
)
op_add_node_cfg.type = nodeitem.nodetype
op_add_node_cfg.use_transform = True
elif isinstance(nodeitem_or_submenu, str):
submenu_id = nodeitem_or_submenu
self.layout.menu(submenu_id)
return draw
menu_class = type(str(current_category.value), (bpy.types.Menu,), {
'bl_idname': current_category.value,
'bl_label': ct.NODE_CAT_LABELS[current_category],
'draw': draw_factory(tuple(subitems)),
})
# Report to Items and Registration List
items.append(current_category.value)
DYNAMIC_SUBMENU_REGISTRATIONS.append(menu_class)
return items
####################
# - Blender Registration
####################
BL_NODE_CATEGORIES = mk_node_categories(
ct.NodeCategory.get_tree()["MAXWELLSIM"],
syllable_prefix = ["MAXWELLSIM"],
)
## TODO: refactor, this has a big code smell
BL_REGISTER = [
*DYNAMIC_SUBMENU_REGISTRATIONS
] ## Must be run after, right now.
## TEST - TODO this is a big code smell
def menu_draw(self, context):
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
self.layout.menu(submenu_id)
bpy.types.NODE_MT_add.append(menu_draw)

View File

@ -0,0 +1,56 @@
####################
# - 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_from_bl_desc import BL_SOCKET_DESCR_TYPE_MAP
from .socket_from_bl_direct import BL_SOCKET_DIRECT_TYPE_MAP
from .socket_from_bl_desc import BL_SOCKET_DESCR_ANNOT_STRING
####################
# - 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

View File

@ -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_]+$',
)]

View File

@ -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()

View File

@ -0,0 +1,4 @@
from ....utils.blender_type_enum import BlenderTypeEnum
class Icon(BlenderTypeEnum):
SimNodeEditor = "MOD_SIMPLEDEFORM"

View File

@ -0,0 +1,9 @@
import enum
from ....utils.blender_type_enum import (
BlenderTypeEnum
)
class ManagedObjType(BlenderTypeEnum):
ManagedBLObject = enum.auto()
ManagedBLImage = enum.auto()

View File

@ -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_BOUNDCONDS: "Bound Conds",
# 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",
}

View File

@ -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_BOUNDCONDS = 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

View File

@ -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
BoundConds = enum.auto()
## Bounds / Bound Faces
PMLBoundCond = enum.auto()
PECBoundCond = enum.auto()
PMCBoundCond = enum.auto()
BlochBoundCond = enum.auto()
PeriodicBoundCond = enum.auto()
AbsorbingBoundCond = 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()

View File

@ -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

View File

@ -0,0 +1,33 @@
import typing as typ
import typing as typx
import pydantic as pyd
import bpy
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

View File

@ -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 = ""

View File

@ -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):

View File

@ -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]

View File

@ -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:
...

View File

@ -0,0 +1,72 @@
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.String: (0.7, 0.7, 0.7, 1.0), # Medium Light Grey
ST.FilePath: (0.6, 0.6, 0.6, 1.0), # Medium Grey
# 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.Integer2DVector: (0.5, 1.0, 0.5, 1.0), # Light Green
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.Integer3DVector: (0.3, 0.8, 0.3, 1.0), # Medium 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.PhysicalAccel3D: (0.7, 0.5, 0.3, 1.0), # Medium Orange
ST.PhysicalForce3D: (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.BlenderGeoNodes: (0.3, 0.3, 0.6, 1.0), # Dark Purple
ST.BlenderText: (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.MaxwellBoundConds: (0.9, 0.8, 0.5, 1.0), # Light Gold
ST.MaxwellBoundCond: (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
}

View File

@ -0,0 +1,78 @@
from .socket_types import SocketType as ST
BL_SOCKET_DESCR_ANNOT_STRING = ":: "
BL_SOCKET_DESCR_TYPE_MAP = {
("Time", "NodeSocketFloat", 1): ST.PhysicalTime,
("Angle", "NodeSocketFloat", 1): ST.PhysicalAngle,
("SolidAngle", "NodeSocketFloat", 1): ST.PhysicalSolidAngle,
("Rotation", "NodeSocketVector", 2): ST.PhysicalRot2D,
("Rotation", "NodeSocketVector", 3): ST.PhysicalRot3D,
("Freq", "NodeSocketFloat", 1): ST.PhysicalFreq,
("AngFreq", "NodeSocketFloat", 1): ST.PhysicalAngFreq,
## Cartesian
("Length", "NodeSocketFloat", 1): ST.PhysicalLength,
("Area", "NodeSocketFloat", 1): ST.PhysicalArea,
("Volume", "NodeSocketFloat", 1): ST.PhysicalVolume,
("Disp", "NodeSocketVector", 2): ST.PhysicalDisp2D,
("Disp", "NodeSocketVector", 3): ST.PhysicalDisp3D,
("Point", "NodeSocketFloat", 1): ST.PhysicalPoint1D,
("Point", "NodeSocketVector", 2): ST.PhysicalPoint2D,
("Point", "NodeSocketVector", 3): ST.PhysicalPoint3D,
("Size", "NodeSocketVector", 2): ST.PhysicalSize2D,
("Size", "NodeSocketVector", 3): ST.PhysicalSize3D,
## Mechanical
("Mass", "NodeSocketFloat", 1): ST.PhysicalMass,
("Speed", "NodeSocketFloat", 1): ST.PhysicalSpeed,
("Vel", "NodeSocketVector", 2): ST.PhysicalVel2D,
("Vel", "NodeSocketVector", 3): ST.PhysicalVel3D,
("Accel", "NodeSocketFloat", 1): ST.PhysicalAccelScalar,
("Accel", "NodeSocketVector", 2): ST.PhysicalAccel2D,
("Accel", "NodeSocketVector", 3): ST.PhysicalAccel3D,
("Force", "NodeSocketFloat", 1): ST.PhysicalForceScalar,
("Force", "NodeSocketVector", 2): ST.PhysicalForce2D,
("Force", "NodeSocketVector", 3): ST.PhysicalForce3D,
("Pressure", "NodeSocketFloat", 1): ST.PhysicalPressure,
## Energetic
("Energy", "NodeSocketFloat", 1): ST.PhysicalEnergy,
("Power", "NodeSocketFloat", 1): ST.PhysicalPower,
("Temp", "NodeSocketFloat", 1): ST.PhysicalTemp,
## ELectrodynamical
("Curr", "NodeSocketFloat", 1): ST.PhysicalCurr,
("CurrDens", "NodeSocketVector", 2): ST.PhysicalCurrDens2D,
("CurrDens", "NodeSocketVector", 3): ST.PhysicalCurrDens3D,
("Charge", "NodeSocketFloat", 1): ST.PhysicalCharge,
("Voltage", "NodeSocketFloat", 1): ST.PhysicalVoltage,
("Capacitance", "NodeSocketFloat", 1): ST.PhysicalCapacitance,
("Resistance", "NodeSocketFloat", 1): ST.PhysicalResistance,
("Conductance", "NodeSocketFloat", 1): ST.PhysicalConductance,
("MagFlux", "NodeSocketFloat", 1): ST.PhysicalMagFlux,
("MagFluxDens", "NodeSocketFloat", 1): ST.PhysicalMagFluxDens,
("Inductance", "NodeSocketFloat", 1): ST.PhysicalInductance,
("EField", "NodeSocketFloat", 2): ST.PhysicalEField3D,
("EField", "NodeSocketFloat", 3): ST.PhysicalEField2D,
("HField", "NodeSocketFloat", 2): ST.PhysicalHField3D,
("HField", "NodeSocketFloat", 3): ST.PhysicalHField2D,
## Luminal
("LumIntensity", "NodeSocketFloat", 1): ST.PhysicalLumIntensity,
("LumFlux", "NodeSocketFloat", 1): ST.PhysicalLumFlux,
("Illuminance", "NodeSocketFloat", 1): ST.PhysicalIlluminance,
## Optical
("PolJones", "NodeSocketFloat", 2): ST.PhysicalPolJones,
("Pol", "NodeSocketFloat", 4): ST.PhysicalPol,
}

View File

@ -0,0 +1,36 @@
from .socket_types import SocketType as ST
BL_SOCKET_DIRECT_TYPE_MAP = {
("NodeSocketString", 1): ST.String,
("NodeSocketBool", 1): ST.Bool,
("NodeSocketCollection", 1): ST.BlenderCollection,
("NodeSocketImage", 1): ST.BlenderImage,
("NodeSocketObject", 1): ST.BlenderObject,
("NodeSocketFloat", 1): ST.RealNumber,
#("NodeSocketFloatAngle", 1): ST.PhysicalAngle,
#("NodeSocketFloatDistance", 1): ST.PhysicalLength,
("NodeSocketFloatFactor", 1): ST.RealNumber,
("NodeSocketFloatPercentage", 1): ST.RealNumber,
#("NodeSocketFloatTime", 1): ST.PhysicalTime,
#("NodeSocketFloatTimeAbsolute", 1): ST.PhysicalTime,
("NodeSocketInt", 1): ST.IntegerNumber,
("NodeSocketIntFactor", 1): ST.IntegerNumber,
("NodeSocketIntPercentage", 1): ST.IntegerNumber,
("NodeSocketIntUnsigned", 1): ST.IntegerNumber,
("NodeSocketRotation", 2): ST.PhysicalRot2D,
("NodeSocketColor", 3): ST.Color,
("NodeSocketVector", 2): ST.Real2DVector,
("NodeSocketVector", 3): ST.Real3DVector,
#("NodeSocketVectorAcceleration", 2): ST.PhysicalAccel2D,
#("NodeSocketVectorAcceleration", 3): ST.PhysicalAccel3D,
#("NodeSocketVectorDirection", 2): ST.Real2DVectorDir,
#("NodeSocketVectorDirection", 3): ST.Real3DVectorDir,
("NodeSocketVectorEuler", 2): ST.PhysicalRot2D,
("NodeSocketVectorEuler", 3): ST.PhysicalRot3D,
#("NodeSocketVectorTranslation", 3): ST.PhysicalDisp3D,
#("NodeSocketVectorVelocity", 3): ST.PhysicalVel3D,
#("NodeSocketVectorXYZ", 3): ST.PhysicalPoint3D,
}

View File

@ -0,0 +1,67 @@
from .socket_types import SocketType as ST
SOCKET_SHAPES = {
# Basic
ST.Any: "CIRCLE",
ST.Bool: "CIRCLE",
ST.String: "SQUARE",
ST.FilePath: "SQUARE",
# Number
ST.IntegerNumber: "CIRCLE",
ST.RationalNumber: "CIRCLE",
ST.RealNumber: "CIRCLE",
ST.ComplexNumber: "CIRCLE_DOT",
# Vector
ST.Integer2DVector: "SQUARE_DOT",
ST.Real2DVector: "SQUARE_DOT",
ST.Complex2DVector: "DIAMOND_DOT",
ST.Integer3DVector: "SQUARE_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.PhysicalAccel3D: "SQUARE_DOT",
ST.PhysicalForce3D: "SQUARE_DOT",
ST.PhysicalPol: "DIAMOND",
ST.PhysicalFreq: "CIRCLE",
# Blender
ST.BlenderObject: "SQUARE",
ST.BlenderCollection: "SQUARE",
ST.BlenderImage: "DIAMOND",
ST.BlenderGeoNodes: "DIAMOND",
ST.BlenderText: "SQUARE",
# Maxwell
ST.MaxwellSource: "CIRCLE",
ST.MaxwellTemporalShape: "CIRCLE",
ST.MaxwellMedium: "CIRCLE",
ST.MaxwellMediumNonLinearity: "CIRCLE",
ST.MaxwellStructure: "SQUARE",
ST.MaxwellBoundConds: "SQUARE",
ST.MaxwellBoundCond: "DIAMOND",
ST.MaxwellMonitor: "CIRCLE",
ST.MaxwellFDTDSim: "SQUARE",
ST.MaxwellSimGrid: "SQUARE",
ST.MaxwellSimGridAxis: "DIAMOND",
ST.MaxwellSimDomain: "SQUARE",
# Tidy3D
ST.Tidy3DCloudTask: "CIRCLE",
}

View File

@ -0,0 +1,138 @@
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()
String = enum.auto()
FilePath = enum.auto()
Color = enum.auto()
# Number
IntegerNumber = enum.auto()
RationalNumber = enum.auto()
RealNumber = enum.auto()
ComplexNumber = enum.auto()
# Vector
Integer2DVector = enum.auto()
Real2DVector = enum.auto()
Real2DVectorDir = enum.auto()
Complex2DVector = enum.auto()
Integer3DVector = enum.auto()
Real3DVector = enum.auto()
Real3DVectorDir = enum.auto()
Complex3DVector = enum.auto()
# Blender
BlenderObject = enum.auto()
BlenderCollection = enum.auto()
BlenderImage = enum.auto()
BlenderGeoNodes = enum.auto()
BlenderText = enum.auto()
# Maxwell
MaxwellBoundConds = enum.auto()
MaxwellBoundCond = enum.auto()
MaxwellMedium = enum.auto()
MaxwellMediumNonLinearity = enum.auto()
MaxwellSource = enum.auto()
MaxwellTemporalShape = enum.auto()
MaxwellStructure = enum.auto()
MaxwellMonitor = enum.auto()
MaxwellFDTDSim = enum.auto()
MaxwellSimDomain = enum.auto()
MaxwellSimGrid = enum.auto()
MaxwellSimGridAxis = enum.auto()
# Tidy3D
Tidy3DCloudTask = enum.auto()
# Physical
PhysicalUnitSystem = enum.auto()
PhysicalTime = enum.auto()
PhysicalAngle = enum.auto()
PhysicalSolidAngle = enum.auto()
PhysicalRot2D = enum.auto()
PhysicalRot3D = enum.auto()
PhysicalFreq = enum.auto()
PhysicalAngFreq = enum.auto()
## Cartesian
PhysicalLength = enum.auto()
PhysicalArea = enum.auto()
PhysicalVolume = enum.auto()
PhysicalDisp2D = enum.auto()
PhysicalDisp3D = enum.auto()
PhysicalPoint1D = enum.auto()
PhysicalPoint2D = enum.auto()
PhysicalPoint3D = enum.auto()
PhysicalSize2D = enum.auto()
PhysicalSize3D = enum.auto()
## Mechanical
PhysicalMass = enum.auto()
PhysicalSpeed = enum.auto()
PhysicalVel2D = enum.auto()
PhysicalVel3D = enum.auto()
PhysicalAccelScalar = enum.auto()
PhysicalAccel2D = enum.auto()
PhysicalAccel3D = enum.auto()
PhysicalForceScalar = enum.auto()
PhysicalForce2D = enum.auto()
PhysicalForce3D = enum.auto()
PhysicalPressure = enum.auto()
## Energetic
PhysicalEnergy = enum.auto()
PhysicalPower = enum.auto()
PhysicalTemp = enum.auto()
## Electrodynamical
PhysicalCurr = enum.auto()
PhysicalCurrDens2D = enum.auto()
PhysicalCurrDens3D = enum.auto()
PhysicalCharge = enum.auto()
PhysicalVoltage = enum.auto()
PhysicalCapacitance = enum.auto()
PhysicalResistance = enum.auto()
PhysicalConductance = enum.auto()
PhysicalMagFlux = enum.auto()
PhysicalMagFluxDens = enum.auto()
PhysicalInductance = enum.auto()
PhysicalEField2D = enum.auto()
PhysicalEField3D = enum.auto()
PhysicalHField2D = enum.auto()
PhysicalHField3D = enum.auto()
## Luminal
PhysicalLumIntensity = enum.auto()
PhysicalLumFlux = enum.auto()
PhysicalIlluminance = enum.auto()
## Optical
PhysicalPolJones = enum.auto()
PhysicalPol = enum.auto()

View File

@ -0,0 +1,266 @@
import sympy.physics.units as spu
from ....utils import extra_sympy_units as spux
from .socket_types import SocketType as ST
SOCKET_UNITS = {
ST.PhysicalTime: {
"default": "PS",
"values": {
"FS": spux.femtosecond,
"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": spux.nanonewton,
"UNEWT": spux.micronewton,
"MNEWT": spux.millinewton,
"NEWT": spu.newton,
},
},
ST.PhysicalAccel3D: {
"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.PhysicalForce3D: {
"default": "UNEWT",
"values": {
"KG_M_S_SQ": spu.kg * spu.m/spu.second**2,
"NNEWT": spux.nanonewton,
"UNEWT": spux.micronewton,
"MNEWT": spux.millinewton,
"NEWT": spu.newton,
},
},
ST.PhysicalFreq: {
"default": "THZ",
"values": {
"HZ": spu.hertz,
"KHZ": spux.kilohertz,
"MHZ": spux.megahertz,
"GHZ": spux.gigahertz,
"THZ": spux.terahertz,
"PHZ": spux.petahertz,
"EHZ": spux.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,
},
},
}

View File

@ -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()

View File

@ -0,0 +1,2 @@
from .managed_bl_image import ManagedBLImage
from .managed_bl_object import ManagedBLObject

View File

@ -0,0 +1,205 @@
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):
# Image Doesn't Exist
if not (bl_image := bpy.data.images.get(self._bl_image_name)):
# ...AND Desired Image Name is Not Taken
if not bpy.data.objects.get(value):
self._bl_image_name = value
return
# ...AND Desired Image Name is Taken
else:
msg = f"Desired name {value} for BL image is taken"
raise ValueError(msg)
# Object DOES Exist
bl_image.name = value
self._bl_image_name = bl_image.name
## - When name exists, Blender adds .### to prevent overlap.
## - `set_name` is allowed to change the name; nodes account for this.
def free(self):
if not (bl_image := bpy.data.images.get(self.name)):
msg = "Can't free BL image that doesn't exist"
raise ValueError(msg)
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()

View File

@ -0,0 +1,392 @@
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
ModifierType = typx.Literal["NODES", "ARRAY"]
MODIFIER_NAMES = {
"NODES": "BLMaxwell_GeoNodes",
"ARRAY": "BLMaxwell_Array",
}
MANAGED_COLLECTION_NAME = "BLMaxwell"
PREVIEW_COLLECTION_NAME = "BLMaxwell Visible"
def bl_collection(
collection_name: str, view_layer_exclude: bool
) -> bpy.types.Collection:
# Init the "Managed Collection"
# Ensure Collection exists (and is in the Scene collection)
if collection_name not in bpy.data.collections:
collection = bpy.data.collections.new(collection_name)
bpy.context.scene.collection.children.link(collection)
else:
collection = bpy.data.collections[collection_name]
## Ensure synced View Layer exclusion
if (layer_collection := bpy.context.view_layer.layer_collection.children[
collection_name
]).exclude != view_layer_exclude:
layer_collection.exclude = view_layer_exclude
return collection
class ManagedBLObject(ct.schemas.ManagedObj):
managed_obj_type = ct.ManagedObjType.ManagedBLObject
_bl_object_name: str
def __init__(self, name: str):
self._bl_object_name = name
# Object Name
@property
def name(self):
return self._bl_object_name
@name.setter
def set_name(self, value: str) -> None:
# Object Doesn't Exist
if not (bl_object := bpy.data.objects.get(self._bl_object_name)):
# ...AND Desired Object Name is Not Taken
if not bpy.data.objects.get(value):
self._bl_object_name = value
return
# ...AND Desired Object Name is Taken
else:
msg = f"Desired name {value} for BL object is taken"
raise ValueError(msg)
# Object DOES Exist
bl_object.name = value
self._bl_object_name = bl_object.name
## - When name exists, Blender adds .### to prevent overlap.
## - `set_name` is allowed to change the name; nodes account for this.
# Object Datablock Name
@property
def bl_mesh_name(self):
return self.name
@property
def bl_volume_name(self):
return self.name
# Deallocation
def free(self):
if not (bl_object := bpy.data.objects.get(self.name)):
return ## Nothing to do
# Delete the Underlying Datablock
## This automatically deletes the object too
if bl_object.type == "MESH":
bpy.data.meshes.remove(bl_object.data)
elif bl_object.type == "EMPTY":
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 show_preview(
self,
kind: typx.Literal["MESH", "EMPTY", "VOLUME"],
empty_display_type: typx.Literal[
"PLAIN_AXES", "ARROWS", "SINGLE_ARROW", "CIRCLE", "CUBE",
"SPHERE", "CONE", "IMAGE",
] | None = None,
) -> None:
"""Moves the managed Blender object to the preview collection.
If it's already included, do nothing.
"""
bl_object = self.bl_object(kind)
if bl_object.name not in (preview_collection := bl_collection(
PREVIEW_COLLECTION_NAME, view_layer_exclude=False
)).objects:
preview_collection.objects.link(bl_object)
if kind == "EMPTY" and empty_display_type is not None:
bl_object.empty_display_type = empty_display_type
def hide_preview(
self,
kind: typx.Literal["MESH", "EMPTY", "VOLUME"],
) -> None:
"""Removes the managed Blender object from the preview collection.
If it's already removed, do nothing.
"""
bl_object = self.bl_object(kind)
if bl_object.name not in (preview_collection := bl_collection(
PREVIEW_COLLECTION_NAME, view_layer_exclude=False
)).objects:
preview_collection.objects.unlink(bl_object)
def bl_select(self) -> None:
"""Selects the managed Blender object globally, causing it to be ex.
outlined in the 3D viewport.
"""
if not (bl_object := bpy.data.objects.get(self.name)):
msg = "Managed BLObject does not exist"
raise ValueError(msg)
bpy.ops.object.select_all(action='DESELECT')
bl_object.select_set(True)
####################
# - Managed Object Management
####################
def bl_object(
self,
kind: typx.Literal["MESH", "EMPTY", "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.objects.get(self.name))
and bl_object.type != kind
):
self.free()
# Create Object w/Appropriate Data Block
if not (bl_object := bpy.data.objects.get(self.name)):
if kind == "MESH":
bl_data = bpy.data.meshes.new(self.bl_mesh_name)
elif kind == "EMPTY":
bl_data = None
elif kind == "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.name, bl_data)
bl_collection(
MANAGED_COLLECTION_NAME, view_layer_exclude=True
).objects.link(bl_object)
return bl_object
####################
# - Mesh 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.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 mesh_as_bmesh(
self,
evaluate: bool = True,
triangulate: bool = False,
) -> bpy.types.Mesh:
if (
(bl_object := bpy.data.objects.get(self.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()
else:
msg = f"Requested BMesh from `bl_object` of type {bl_object.type}"
raise ValueError(msg)
@property
def mesh_as_arrays(self) -> dict:
## TODO: Cached
# Ensure Updated Geometry
bpy.context.view_layer.update()
## TODO: Must we?
# Compute Evaluted + Triangulated Mesh
_mesh = bpy.data.meshes.new(name="TemporaryMesh")
with self.mesh_as_bmesh(evaluate=True, triangulate=True) as bmesh_mesh:
bmesh_mesh.to_mesh(_mesh)
# Optimized Vertex Copy
## See <https://blog.michelanders.nl/2016/02/copying-vertices-to-numpy-arrays-in_4.html>
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,
}
####################
# - Modifier Methods
####################
def bl_modifier(
self,
modifier_type: ModifierType,
):
"""Creates a new modifier for the current `bl_object`.
For all Blender modifier type names, see: <https://docs.blender.org/api/current/bpy_types_enum_items/object_modifier_type_items.html#rna-enum-object-modifier-type-items>
"""
if not (bl_object := bpy.data.objects.get(self.name)):
msg = "Can't add modifier to BL object that doesn't exist"
raise ValueError(msg)
# (Create and) Return Modifier
bl_modifier_name = MODIFIER_NAMES[modifier_type]
if bl_modifier_name not in bl_object.modifiers:
return bl_object.modifiers.new(
name=bl_modifier_name,
type=modifier_type,
)
return bl_object.modifiers[bl_modifier_name]
def modifier_attrs(self, modifier_type: ModifierType) -> dict:
"""Based on the modifier type, retrieve a representative dictionary of modifier attributes.
The attributes can then easily be set using `setattr`.
"""
bl_modifier = self.bl_modifier(modifier_type)
if modifier_type == "NODES":
return {
"node_group": bl_modifier.node_group,
}
elif modifier_type == "ARRAY":
raise NotImplementedError
def s_modifier_attrs(
self,
modifier_type: ModifierType,
modifier_attrs: dict,
):
bl_modifier = self.bl_modifier(modifier_type)
if modifier_type == "NODES":
if bl_modifier.node_group != modifier_attrs["node_group"]:
bl_modifier.node_group = modifier_attrs["node_group"]
elif modifier_type == "ARRAY":
raise NotImplementedError
####################
# - GeoNodes Modifier
####################
def sync_geonodes_modifier(
self,
geonodes_node_group,
geonodes_identifier_to_value: dict,
):
"""Push the given GeoNodes Interface values to a GeoNodes modifier attached to a managed MESH object.
The values must be compatible with the `default_value`s of the interface sockets.
If there is no object, it is created.
If the object isn't a MESH object, it is made so.
If the GeoNodes modifier doesn't exist, it is created.
If the GeoNodes node group doesn't match, it is changed.
Only differing interface values are actually changed.
"""
bl_object = self.bl_object("MESH")
# Get (/make) a GeoModes Modifier
bl_modifier = self.bl_modifier("NODES")
# Set GeoNodes Modifier Attributes (specifically, the 'node_group')
self.s_modifier_attrs("NODES", {"node_group": geonodes_node_group})
# Set GeoNodes Values
modifier_altered = False
for interface_identifier, value in (
geonodes_identifier_to_value.items()
):
if bl_modifier[interface_identifier] != value:
# Quickly Determine if IDPropertyArray is Equal
if hasattr(
bl_modifier[interface_identifier],
"to_list"
) and tuple(
bl_modifier[interface_identifier].to_list()
) == value:
continue
# Quickly Determine int/float Mismatch
if isinstance(
bl_modifier[interface_identifier],
float,
) and isinstance(value, int):
value = float(value)
bl_modifier[interface_identifier] = value
modifier_altered = True
# Update DepGraph (if anything changed)
if modifier_altered:
bl_object.data.update()
#@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)

View File

@ -0,0 +1,185 @@
import typing as typ
import bpy
from . import contracts as ct
####################
# - Cache Management
####################
MemAddr = int
class DeltaNodeLinkCache(typ.TypedDict):
added: set[MemAddr]
removed: set[MemAddr]
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 = ct.TreeType.MaxwellSim.value
bl_label = "Maxwell Sim Editor"
bl_icon = ct.Icon.SimNodeEditor.value
####################
# - 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
####################
# - Init Methods
####################
def on_load(self):
"""Run by Blender when loading the NodeSimTree, ex. on file load, on creation, etc. .
It's a bit of a "fake" function - in practicality, it's triggered on the first update() function.
"""
## TODO: Consider tying this to an "on_load" handler
self._node_link_cache = NodeLinkCache(self)
####################
# - 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.on_load()
## 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,
]

View File

@ -0,0 +1,36 @@
#from . import kitchen_sink
from . import inputs
from . import outputs
from . import sources
from . import mediums
from . import structures
#from . import bounds
from . import monitors
from . import simulations
#from . import utilities
BL_REGISTER = [
#*kitchen_sink.BL_REGISTER,
*inputs.BL_REGISTER,
*outputs.BL_REGISTER,
*sources.BL_REGISTER,
*mediums.BL_REGISTER,
*structures.BL_REGISTER,
# *bounds.BL_REGISTER,
*monitors.BL_REGISTER,
*simulations.BL_REGISTER,
# *utilities.BL_REGISTER,
]
BL_NODES = {
#**kitchen_sink.BL_NODES,
**inputs.BL_NODES,
**outputs.BL_NODES,
**sources.BL_NODES,
**mediums.BL_NODES,
**structures.BL_NODES,
# **bounds.BL_NODES,
**monitors.BL_NODES,
**simulations.BL_NODES,
# **utilities.BL_NODES,
}

View File

@ -0,0 +1,940 @@
import uuid
import typing as typ
import typing_extensions as typx
import json
import inspect
import bpy
import pydantic as pyd
from .. import contracts as ct
from .. import sockets
CACHE: dict[str, typ.Any] = {} ## By Instance UUID
## NOTE: CACHE does not persist between file loads.
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
# Style
bl_description: str = ""
#bl_width_default: float = 0.0
#bl_width_min: float = 0.0
#bl_width_max: float = 0.0
# Sockets
_output_socket_methods: dict
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]] = {}
# Presets
presets = {}
# Managed Objects
managed_obj_defs: dict[ct.ManagedObjName, ct.schemas.ManagedObjDef] = {}
####################
# - Initialization
####################
def __init_subclass__(cls, **kwargs: typ.Any):
super().__init_subclass__(**kwargs)
# 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)
# 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.
## 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_name,
socket_set_name,
socket_set_name,
)
for socket_set_id, socket_set_name in zip(
socket_set_ids,
socket_set_names,
)
],
default=socket_set_names[0],
update=(lambda self, _: self.sync_sockets()),
)
# 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,
preset_def.label,
preset_def.description,
)
for preset_name, preset_def in cls.presets.items()
],
default=list(cls.presets.keys())[0],
update=lambda self, context: (
self.sync_active_preset()()
),
)
####################
# - Generic Properties
####################
def sync_sim_node_name(self, context):
if (mobjs := CACHE[self.instance_id].get("managed_objs")) is None:
return
for mobj_id, mobj in mobjs.items():
# Retrieve Managed Obj Definition
mobj_def = self.managed_obj_defs[mobj_id]
# Set Managed Obj Name
mobj.name = mobj_def.name_prefix + self.sim_node_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"] = {}
# 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)
)
return CACHE[self.instance_id]["managed_objs"]
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": [
model.model_dump()
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
# 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.
- 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
####################
def draw_buttons(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
) -> None:
if self.locked: layout.enabled = False
if self.active_preset:
layout.prop(self, "active_preset", text="")
if self.active_socket_set:
layout.prop(self, "active_socket_set", text="")
# Draw Name
col = layout.column(align=False)
if self.use_sim_node_name:
row = col.row(align=True)
row.label(text="", icon="EVENT_N")
row.prop(self, "sim_node_name", text="")
# 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
####################
# - Data Flow
####################
def _compute_input(
self,
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`.
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)
return bl_socket.compute_data(kind=kind)
def compute_output(
self,
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 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,
for which this method computes the output.
Returns:
The value of the output socket, as computed by the dedicated method
registered using the `@computes_output_socket` decorator.
"""
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_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 in method._extra_data.get("changed_sockets")
) or (
prop_name
and prop_name in method._extra_data.get("changed_props")
) or (
socket_name
and method._extra_data["changed_loose_input"]
and socket_name in self.loose_input_sockets
):
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
loose_input_sockets: bool = False,
loose_output_sockets: bool = False,
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 Loose Sockets
if loose_input_sockets:
_loose_input_sockets = {
input_socket_name: node._compute_input(input_socket_name, kind)
for input_socket_name in node.loose_input_sockets
}
method_kw_args |= dict(
loose_input_sockets=_loose_input_sockets
)
if loose_output_sockets:
_loose_output_sockets = {
output_socket_name: node.compute_output(output_socket_name, kind)
for output_socket_name in node.loose_output_sockets
}
method_kw_args |= dict(
loose_output_sockets=_loose_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: set[ct.SocketName] | ct.SocketName | None = None,
prop_name: set[str] | str | None = None,
any_loose_input_socket: bool = False,
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
input_sockets: set[str] = set(),
props: set[str] = set(),
managed_objs: set[str] = set(),
):
if sum([
int(socket_name is not None),
int(prop_name is not None),
int(any_loose_input_socket),
]) > 1:
msg = "Define only one of socket_name, prop_name or any_loose_input_socket"
raise ValueError(msg)
req_params = {"self"} | (
{"input_sockets"} if input_sockets else set()
) | (
{"loose_input_sockets"} if any_loose_input_socket else set()
) | (
{"props"} if props else set()
) | (
{"managed_objs"} if managed_objs else set()
)
return chain_event_decorator(
callback_type="on_value_changed",
extra_data={
"changed_sockets": (
socket_name if isinstance(socket_name, set) else {socket_name}
),
"changed_props": (
prop_name if isinstance(prop_name, set) else {prop_name}
),
"changed_loose_input": any_loose_input_socket,
},
kind=kind,
input_sockets=input_sockets,
loose_input_sockets=any_loose_input_socket,
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 only same kind
output_sockets: set[str] = set(), ## For now, presume only same kind
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,
)

View File

@ -0,0 +1,11 @@
from . import bound_box
from . import bound_faces
BL_REGISTER = [
*bound_box.BL_REGISTER,
*bound_faces.BL_REGISTER,
]
BL_NODES = {
**bound_box.BL_NODES,
**bound_faces.BL_NODES,
}

View File

@ -0,0 +1,71 @@
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from ... import contracts as ct
from ... import sockets
from .. import base
class BoundCondsNode(base.MaxwellSimNode):
node_type = ct.NodeType.BoundConds
bl_label = "Bound Box"
#bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
"+X": sockets.MaxwellBoundCondSocketDef(),
"-X": sockets.MaxwellBoundCondSocketDef(),
"+Y": sockets.MaxwellBoundCondSocketDef(),
"-Y": sockets.MaxwellBoundCondSocketDef(),
"+Z": sockets.MaxwellBoundCondSocketDef(),
"-Z": sockets.MaxwellBoundCondSocketDef(),
}
output_sockets = {
"BCs": sockets.MaxwellBoundCondsSocketDef(),
}
####################
# - Output Socket Computation
####################
@base.computes_output_socket(
"BCs",
input_sockets={"+X", "-X", "+Y", "-Y", "+Z", "-Z"}
)
def compute_simulation(self, input_sockets) -> td.BoundarySpec:
x_pos = input_sockets["+X"]
x_neg = input_sockets["-X"]
y_pos = input_sockets["+Y"]
y_neg = input_sockets["-Y"]
z_pos = input_sockets["+Z"]
z_neg = input_sockets["-Z"]
return td.BoundarySpec(
x=td.Boundary(
plus=x_pos,
minus=x_neg,
),
y=td.Boundary(
plus=y_pos,
minus=y_neg,
),
z=td.Boundary(
plus=z_pos,
minus=z_neg,
),
)
####################
# - Blender Registration
####################
BL_REGISTER = [
BoundCondsNode,
]
BL_NODES = {
ct.NodeType.BoundConds: (
ct.NodeCategory.MAXWELLSIM_BOUNDS
)
}

View File

@ -0,0 +1,26 @@
from . import pml_bound_face
from . import pec_bound_face
from . import pmc_bound_face
from . import bloch_bound_face
from . import periodic_bound_face
from . import absorbing_bound_face
BL_REGISTER = [
*pml_bound_face.BL_REGISTER,
*pec_bound_face.BL_REGISTER,
*pmc_bound_face.BL_REGISTER,
*bloch_bound_face.BL_REGISTER,
*periodic_bound_face.BL_REGISTER,
*absorbing_bound_face.BL_REGISTER,
]
BL_NODES = {
**pml_bound_face.BL_NODES,
**pec_bound_face.BL_NODES,
**pmc_bound_face.BL_NODES,
**bloch_bound_face.BL_NODES,
**periodic_bound_face.BL_NODES,
**absorbing_bound_face.BL_NODES,
}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,26 @@
from . import wave_constant
#from . import unit_system
from . import constants
from . import web_importers
#from . import file_importers
BL_REGISTER = [
*wave_constant.BL_REGISTER,
# *unit_system.BL_REGISTER,
*constants.BL_REGISTER,
*web_importers.BL_REGISTER,
# *file_importers.BL_REGISTER,
]
BL_NODES = {
**wave_constant.BL_NODES,
# **unit_system.BL_NODES,
**constants.BL_NODES,
**web_importers.BL_NODES,
# *file_importers.BL_REGISTER,
}

View File

@ -0,0 +1,17 @@
#from . import scientific_constant
from . import number_constant
#from . import physical_constant
from . import blender_constant
BL_REGISTER = [
# *scientific_constant.BL_REGISTER,
*number_constant.BL_REGISTER,
# *physical_constant.BL_REGISTER,
*blender_constant.BL_REGISTER,
]
BL_NODES = {
# **scientific_constant.BL_NODES,
**number_constant.BL_NODES,
# **physical_constant.BL_NODES,
**blender_constant.BL_NODES,
}

View File

@ -0,0 +1,52 @@
import typing as typ
from .... import contracts as ct
from .... import sockets
from ... import base
class BlenderConstantNode(base.MaxwellSimNode):
node_type = ct.NodeType.BlenderConstant
bl_label = "Blender Constant"
input_socket_sets = {
"Object": {
"Value": sockets.BlenderObjectSocketDef(),
},
"Collection": {
"Value": sockets.BlenderCollectionSocketDef(),
},
"Text": {
"Value": sockets.BlenderTextSocketDef(),
},
"Image": {
"Value": sockets.BlenderImageSocketDef(),
},
"GeoNode Tree": {
"Value": sockets.BlenderGeoNodesSocketDef(),
},
}
output_socket_sets = input_socket_sets
####################
# - Callbacks
####################
@base.computes_output_socket(
"Value",
input_sockets={"Value"}
)
def compute_value(self, input_sockets) -> typ.Any:
return input_sockets["Value"]
####################
# - Blender Registration
####################
BL_REGISTER = [
BlenderConstantNode,
]
BL_NODES = {
ct.NodeType.BlenderConstant: (
ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS
)
}

View File

@ -0,0 +1,52 @@
import typing as typ
import bpy
import sympy as sp
from .... import contracts as ct
from .... import sockets
from ... import base
class NumberConstantNode(base.MaxwellSimNode):
node_type = ct.NodeType.NumberConstant
bl_label = "Numerical Constant"
input_socket_sets = {
"Integer": {
"Value": sockets.IntegerNumberSocketDef(),
},
"Rational": {
"Value": sockets.RationalNumberSocketDef(),
},
"Real": {
"Value": sockets.RealNumberSocketDef(),
},
"Complex": {
"Value": sockets.ComplexNumberSocketDef(),
},
}
output_socket_sets = input_socket_sets
####################
# - Callbacks
####################
@base.computes_output_socket(
"Value",
input_sockets={"Value"}
)
def compute_value(self, input_sockets) -> typ.Any:
return input_sockets["Value"]
####################
# - Blender Registration
####################
BL_REGISTER = [
NumberConstantNode,
]
BL_NODES = {
ct.NodeType.NumberConstant: (
ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS
)
}

View File

@ -0,0 +1,75 @@
import bpy
import sympy as sp
from .... import contracts
from .... import sockets
from ... import base
class PhysicalConstantNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.PhysicalConstant
bl_label = "Physical Constant"
#bl_icon = constants.ICON_SIM_INPUT
input_sockets = {}
input_socket_sets = {
"time": {
"value": sockets.PhysicalTimeSocketDef(
label="Time",
),
},
"angle": {
"value": sockets.PhysicalAngleSocketDef(
label="Angle",
),
},
"length": {
"value": sockets.PhysicalLengthSocketDef(
label="Length",
),
},
"area": {
"value": sockets.PhysicalAreaSocketDef(
label="Area",
),
},
"volume": {
"value": sockets.PhysicalVolumeSocketDef(
label="Volume",
),
},
"point_3d": {
"value": sockets.PhysicalPoint3DSocketDef(
label="3D Point",
),
},
"size_3d": {
"value": sockets.PhysicalSize3DSocketDef(
label="3D Size",
),
},
## I got bored so maybe the rest later
}
output_sockets = {}
output_socket_sets = input_socket_sets
####################
# - Callbacks
####################
@base.computes_output_socket("value")
def compute_value(self: contracts.NodeTypeProtocol) -> sp.Expr:
return self.compute_input("value")
####################
# - Blender Registration
####################
BL_REGISTER = [
PhysicalConstantNode,
]
BL_NODES = {
contracts.NodeType.PhysicalConstant: (
contracts.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS
)
}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,43 @@
import bpy
import sympy as sp
from ... import contracts as ct
from ... import sockets
from .. import base
class PhysicalUnitSystemNode(base.MaxwellSimNode):
node_type = ct.NodeType.UnitSystem
bl_label = "Unit System"
input_sockets = {
"Unit System": sockets.PhysicalUnitSystemSocketDef(
show_by_default=True,
),
}
output_sockets = {
"Unit System": sockets.PhysicalUnitSystemSocketDef(),
}
####################
# - Callbacks
####################
@base.computes_output_socket(
"Unit System",
input_sockets = {"Unit System"},
)
def compute_value(self, input_sockets) -> dict:
return input_sockets["Unit System"]
####################
# - Blender Registration
####################
BL_REGISTER = [
PhysicalUnitSystemNode,
]
BL_NODES = {
ct.NodeType.UnitSystem: (
ct.NodeCategory.MAXWELLSIM_INPUTS
)
}

View File

@ -0,0 +1,75 @@
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
VAC_SPEED_OF_LIGHT = (
sc.constants.speed_of_light
* spu.meter/spu.second
)
class WaveConstantNode(base.MaxwellSimNode):
node_type = ct.NodeType.WaveConstant
bl_label = "Wave Constant"
input_socket_sets = {
"Vacuum WL": {
"WL": sockets.PhysicalLengthSocketDef(),
},
"Frequency": {
"Freq": sockets.PhysicalFreqSocketDef(),
},
}
output_sockets = {
"WL": sockets.PhysicalLengthSocketDef(),
"Freq": sockets.PhysicalFreqSocketDef(),
}
####################
# - Callbacks
####################
@base.computes_output_socket(
"WL",
kind=ct.DataFlowKind.Value,
input_sockets={"WL", "Freq"},
)
def compute_vac_wl(self, input_sockets: dict) -> sp.Expr:
if (vac_wl := input_sockets["WL"]):
return vac_wl
elif (freq := input_sockets["Freq"]):
return spu.convert_to(
VAC_SPEED_OF_LIGHT / freq,
spu.meter,
)
raise RuntimeError("Vac WL and Freq are both non-truthy")
@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,
spu.hertz,
)
elif (freq := input_sockets["Freq"]):
return freq
####################
# - Blender Registration
####################
BL_REGISTER = [
WaveConstantNode,
]
BL_NODES = {
ct.NodeType.WaveConstant: (
ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS
)
}

View File

@ -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,
}

View File

@ -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
)
}

View File

@ -0,0 +1,102 @@
from pathlib import Path
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from .. import contracts as ct
from .. import sockets
from . import base
class KitchenSinkNode(base.MaxwellSimNode):
node_type = ct.NodeType.KitchenSink
bl_label = "Kitchen Sink"
#bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
"Static Data": sockets.AnySocketDef(),
}
input_socket_sets = {
"Basic": {
"Any": sockets.AnySocketDef(),
"Bool": sockets.BoolSocketDef(),
"FilePath": sockets.FilePathSocketDef(),
"Text": sockets.TextSocketDef(),
},
"Number": {
"Integer": sockets.IntegerNumberSocketDef(),
"Rational": sockets.RationalNumberSocketDef(),
"Real": sockets.RealNumberSocketDef(),
"Complex": sockets.ComplexNumberSocketDef(),
},
"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": {
"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": {
"Object": sockets.BlenderObjectSocketDef(),
"Collection": sockets.BlenderCollectionSocketDef(),
"Image": sockets.BlenderImageSocketDef(),
"GeoNodes": sockets.BlenderGeoNodesSocketDef(),
"Text": sockets.BlenderTextSocketDef(),
},
"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 = {
"Static Data": sockets.AnySocketDef(),
}
output_socket_sets = input_socket_sets
####################
# - Blender Registration
####################
BL_REGISTER = [
KitchenSinkNode,
]
BL_NODES = {
ct.NodeType.KitchenSink: (
ct.NodeCategory.MAXWELLSIM_INPUTS
)
}

View File

@ -0,0 +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
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,
]
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,
}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,80 @@
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from ... import contracts
from ... import sockets
from .. import base
class DrudeLorentzMediumNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.DrudeLorentzMedium
bl_label = "Drude-Lorentz Medium"
#bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
"eps_inf": sockets.RealNumberSocketDef(
label=f"εr_∞",
),
} | {
f"del_eps{i}": sockets.RealNumberSocketDef(
label=f"Δεr_{i}",
)
for i in [1, 2, 3]
} | {
f"f{i}": sockets.PhysicalFreqSocketDef(
label=f"f_{i}",
)
for i in [1, 2, 3]
} | {
f"delta{i}": sockets.PhysicalFreqSocketDef(
label=f"δ_{i}",
)
for i in [1, 2, 3]
}
output_sockets = {
"medium": sockets.MaxwellMediumSocketDef(
label="Medium"
),
}
####################
# - Output Socket Computation
####################
@base.computes_output_socket("medium")
def compute_medium(self: contracts.NodeTypeProtocol) -> td.Sellmeier:
## Retrieval
return td.Lorentz(
eps_inf=self.compute_input(f"eps_inf"),
coeffs = [
(
self.compute_input(f"del_eps{i}"),
spu.convert_to(
self.compute_input(f"f{i}"),
spu.hertz,
) / spu.hertz,
spu.convert_to(
self.compute_input(f"delta{i}"),
spu.hertz,
) / spu.hertz,
)
for i in [1, 2, 3]
]
)
####################
# - Blender Registration
####################
BL_REGISTER = [
DrudeLorentzMediumNode,
]
BL_NODES = {
contracts.NodeType.DrudeLorentzMedium: (
contracts.NodeCategory.MAXWELLSIM_MEDIUMS
)
}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,162 @@
import typing as typ
import functools
import bpy
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
import numpy as np
import scipy as sc
from .....utils import extra_sympy_units as spuex
from ... import contracts as ct
from ... import sockets
from ... import managed_objs
from .. import base
VAC_SPEED_OF_LIGHT = (
sc.constants.speed_of_light
* spu.meter/spu.second
)
class LibraryMediumNode(base.MaxwellSimNode):
node_type = ct.NodeType.LibraryMedium
bl_label = "Library Medium"
####################
# - Sockets
####################
input_sockets = {}
output_sockets = {
"Medium": sockets.MaxwellMediumSocketDef(),
}
managed_obj_defs = {
"nk_plot": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLImage(name),
name_prefix="nkplot_",
)
}
####################
# - Properties
####################
material: bpy.props.EnumProperty(
name="",
description="",
#icon="NODE_MATERIAL",
items=[
(
mat_key,
td.material_library[mat_key].name,
", ".join([
ref.journal
for ref in td.material_library[mat_key].variants[
td.material_library[mat_key].default
].reference
])
)
for mat_key in td.material_library
if mat_key != "graphene" ## For some reason, it's unique...
],
default="Au",
update=(lambda self, context: self.sync_prop("material", context)),
)
@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(
val * spu.hertz,
spuex.terahertz,
) / 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),
spu.nanometer,
) / spu.nanometer
for val in reversed(mat.medium.frequency_range)
]
return sp.pretty(
[nm_range[0].n(4), nm_range[1].n(4)],
use_unicode=True
)
####################
# - UI
####################
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
####################
# - Event Callbacks
####################
@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
]
managed_objs["nk_plot"].mpl_plot_to_image(
lambda ax: medium.plot(medium.frequency_range, ax=ax),
bl_select=True,
)
####################
# - Blender Registration
####################
BL_REGISTER = [
LibraryMediumNode,
]
BL_NODES = {
ct.NodeType.LibraryMedium: (
ct.NodeCategory.MAXWELLSIM_MEDIUMS
)
}

View File

@ -0,0 +1,17 @@
from . import add_non_linearity
from . import chi_3_susceptibility_non_linearity
from . import kerr_non_linearity
from . import two_photon_absorption_non_linearity
BL_REGISTER = [
*add_non_linearity.BL_REGISTER,
*chi_3_susceptibility_non_linearity.BL_REGISTER,
*kerr_non_linearity.BL_REGISTER,
*two_photon_absorption_non_linearity.BL_REGISTER,
]
BL_NODES = {
**add_non_linearity.BL_NODES,
**chi_3_susceptibility_non_linearity.BL_NODES,
**kerr_non_linearity.BL_NODES,
**two_photon_absorption_non_linearity.BL_NODES,
}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,101 @@
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from ... import contracts
from ... import sockets
from .. import base
class TripleSellmeierMediumNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.TripleSellmeierMedium
bl_label = "Three-Parameter Sellmeier Medium"
#bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
f"B{i}": sockets.RealNumberSocketDef(
label=f"B{i}",
)
for i in [1, 2, 3]
} | {
f"C{i}": sockets.PhysicalAreaSocketDef(
label=f"C{i}",
default_unit=spu.um**2
)
for i in [1, 2, 3]
}
output_sockets = {
"medium": sockets.MaxwellMediumSocketDef(
label="Medium"
),
}
####################
# - Presets
####################
presets = {
"BK7": contracts.PresetDef(
label="BK7 Glass",
description="Borosilicate crown glass (known as BK7)",
values={
"B1": 1.03961212,
"B2": 0.231792344,
"B3": 1.01046945,
"C1": 6.00069867e-3 * spu.um**2,
"C2": 2.00179144e-2 * spu.um**2,
"C3": 103.560653 * spu.um**2,
}
),
"FUSED_SILICA": contracts.PresetDef(
label="Fused Silica",
description="Fused silica aka. SiO2",
values={
"B1": 0.696166300,
"B2": 0.407942600,
"B3": 0.897479400,
"C1": 4.67914826e-3 * spu.um**2,
"C2": 1.35120631e-2 * spu.um**2,
"C3": 97.9340025 * spu.um**2,
}
),
}
####################
# - Output Socket Computation
####################
@base.computes_output_socket("medium")
def compute_medium(self: contracts.NodeTypeProtocol) -> td.Sellmeier:
## Retrieval
#B1 = self.compute_input("B1")
#C1_with_units = self.compute_input("C1")
#
## Processing
#C1 = spu.convert_to(C1_with_units, spu.um**2) / spu.um**2
return td.Sellmeier(coeffs = [
(
self.compute_input(f"B{i}"),
spu.convert_to(
self.compute_input(f"C{i}"),
spu.um**2,
) / spu.um**2
)
for i in [1, 2, 3]
])
####################
# - Blender Registration
####################
BL_REGISTER = [
TripleSellmeierMediumNode,
]
BL_NODES = {
contracts.NodeType.TripleSellmeierMedium: (
contracts.NodeCategory.MAXWELLSIM_MEDIUMS
)
}

View File

@ -0,0 +1,17 @@
from . import eh_field_monitor
#from . import field_power_flux_monitor
#from . import epsilon_tensor_monitor
#from . import diffraction_monitor
BL_REGISTER = [
*eh_field_monitor.BL_REGISTER,
# *field_power_flux_monitor.BL_REGISTER,
# *epsilon_tensor_monitor.BL_REGISTER,
# *diffraction_monitor.BL_REGISTER,
]
BL_NODES = {
**eh_field_monitor.BL_NODES,
# **field_power_flux_monitor.BL_NODES,
# **epsilon_tensor_monitor.BL_NODES,
# **diffraction_monitor.BL_NODES,
}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,171 @@
import typing as typ
import functools
import bpy
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
import numpy as np
import scipy as sc
from .....utils import analyze_geonodes
from .....utils import extra_sympy_units as spux
from ... import contracts as ct
from ... import sockets
from ... import managed_objs
from .. import base
GEONODES_MONITOR_BOX = "monitor_box"
class EHFieldMonitorNode(base.MaxwellSimNode):
node_type = ct.NodeType.EHFieldMonitor
bl_label = "E/H Field Monitor"
use_sim_node_name = True
####################
# - Sockets
####################
input_sockets = {
"Rec Start": sockets.PhysicalTimeSocketDef(),
"Rec Stop": sockets.PhysicalTimeSocketDef(
default_value=200*spux.fs
),
"Center": sockets.PhysicalPoint3DSocketDef(),
"Size": sockets.PhysicalSize3DSocketDef(),
"Samples/Space": sockets.Integer3DVectorSocketDef(
default_value=sp.Matrix([10, 10, 10])
),
"Samples/Time": sockets.IntegerNumberSocketDef(
default_value=100,
),
}
output_sockets = {
"Monitor": sockets.MaxwellMonitorSocketDef(),
}
managed_obj_defs = {
"monitor_box": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix="",
)
}
####################
# - Properties
####################
####################
# - UI
####################
def draw_props(self, context, layout):
pass
def draw_info(self, context, col):
pass
####################
# - Output Sockets
####################
@base.computes_output_socket(
"Monitor",
input_sockets={
"Rec Start", "Rec Stop", "Center", "Size", "Samples/Space",
"Samples/Time",
},
props={"sim_node_name"}
)
def compute_monitor(self, input_sockets: dict, props: dict) -> td.FieldTimeMonitor:
_rec_start = input_sockets["Rec Start"]
_rec_stop = input_sockets["Rec Stop"]
_center = input_sockets["Center"]
_size = input_sockets["Size"]
_samples_space = input_sockets["Samples/Space"]
samples_time = input_sockets["Samples/Time"]
rec_start = spu.convert_to(_rec_start, spu.second) / spu.second
rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
size = tuple(spu.convert_to(_size, spu.um) / spu.um)
samples_space = tuple(_samples_space)
return td.FieldTimeMonitor(
center=center,
size=size,
name=props["sim_node_name"],
start=rec_start,
stop=rec_stop,
interval=samples_time,
interval_space=samples_space,
)
####################
# - Preview - Changes to Input Sockets
####################
@base.on_value_changed(
socket_name={"Center", "Size"},
input_sockets={"Center", "Size"},
managed_objs={"monitor_box"},
)
def on_value_changed__center_size(
self,
input_sockets: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
_center = input_sockets["Center"]
center = tuple([
float(el)
for el in spu.convert_to(_center, spu.um) / spu.um
])
_size = input_sockets["Size"]
size = tuple([
float(el)
for el in spu.convert_to(_size, spu.um) / spu.um
])
## TODO: Preview unit system?? Presume um for now
# Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_MONITOR_BOX]
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc="INPUT"
)
# Sync Modifier Inputs
managed_objs["monitor_box"].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface["Size"].identifier: size,
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}
)
# Sync Object Position
managed_objs["monitor_box"].bl_object("MESH").location = center
####################
# - Preview - Show Preview
####################
@base.on_show_preview(
managed_objs={"monitor_box"},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs["monitor_box"].show_preview("MESH")
self.on_value_changed__center_size()
####################
# - Blender Registration
####################
BL_REGISTER = [
EHFieldMonitorNode,
]
BL_NODES = {
ct.NodeType.EHFieldMonitor: (
ct.NodeCategory.MAXWELLSIM_MONITORS
)
}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,11 @@
from . import viewer
from . import exporters
BL_REGISTER = [
*viewer.BL_REGISTER,
*exporters.BL_REGISTER,
]
BL_NODES = {
**viewer.BL_NODES,
**exporters.BL_NODES,
}

View File

@ -0,0 +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,
}

View File

@ -0,0 +1,106 @@
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
####################
# - Operators
####################
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."
@classmethod
def poll(cls, context):
return True
def execute(self, context):
node = context.node
node.export_data_as_json()
return {'FINISHED'}
####################
# - Node
####################
class JSONFileExporterNode(base.MaxwellSimNode):
node_type = ct.NodeType.JSONFileExporter
bl_label = "JSON File Exporter"
#bl_icon = constants.ICON_SIM_INPUT
input_sockets = {
"Data": sockets.AnySocketDef(),
"JSON Path": sockets.FilePathSocketDef(
default_path=Path("simulation.json")
),
"JSON Indent": sockets.IntegerNumberSocketDef(
default_value=4,
),
}
output_sockets = {
"JSON String": sockets.StringSocketDef(),
}
####################
# - UI Layout
####################
def draw_operators(
self,
context: bpy.types.Context,
layout: bpy.types.UILayout,
) -> None:
layout.operator(JSONFileExporterSaveJSON.bl_idname, text="Save JSON")
####################
# - Methods
####################
def export_data_as_json(self) -> None:
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 = [
JSONFileExporterSaveJSON,
JSONFileExporterNode,
]
BL_NODES = {
ct.NodeType.JSONFileExporter: (
ct.NodeCategory.MAXWELLSIM_OUTPUTS_EXPORTERS
)
}

View File

@ -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(2.0, 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..."),
"postprocess": (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
)
}

View File

@ -0,0 +1,169 @@
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
from ...managed_objs import managed_bl_object
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(),
}
####################
# - Properties
####################
auto_plot: bpy.props.BoolProperty(
name="Auto-Plot",
description="Whether to auto-plot anything plugged into the viewer node",
default=False,
update=lambda self, context: self.sync_prop("auto_plot", context),
)
auto_3d_preview: bpy.props.BoolProperty(
name="Auto 3D Preview",
description="Whether to auto-preview anything 3D, that's plugged into the viewer node",
default=False,
update=lambda self, context: self.sync_prop("auto_3d_preview", context),
)
####################
# - UI
####################
def draw_operators(self, context, layout):
split = layout.split(factor=0.4)
# Split LHS
col = split.column(align=False)
col.label(text="Console")
col.label(text="Plot")
col.label(text="3D")
# Split RHS
col = split.column(align=False)
## Console Options
col.operator(ConsoleViewOperator.bl_idname, text="Print")
## Plot Options
row = col.row(align=True)
row.prop(self, "auto_plot", text="Plot", toggle=True)
row.operator(
RefreshPlotViewOperator.bl_idname,
text="",
icon="FILE_REFRESH",
)
## 3D Preview Options
row = col.row(align=True)
row.prop(self, "auto_3d_preview", text="3D Preview", toggle=True)
####################
# - 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))
####################
# - Updates
####################
@base.on_value_changed(
socket_name="Data",
props={"auto_3d_preview"},
)
def on_value_changed__data(self, props):
# Show Plot
## Don't have to un-show other plots.
if self.auto_plot:
self.trigger_action("show_plot")
# Remove Anything Previewed
preview_collection = managed_bl_object.bl_collection(
managed_bl_object.PREVIEW_COLLECTION_NAME,
view_layer_exclude=False,
)
for bl_object in preview_collection.objects.values():
preview_collection.objects.unlink(bl_object)
# Preview Anything that Should be Previewed (maybe)
if props["auto_3d_preview"]:
self.trigger_action("show_preview")
@base.on_value_changed(
prop_name="auto_3d_preview",
props={"auto_3d_preview"},
)
def on_value_changed__auto_3d_preview(self, props):
# Remove Anything Previewed
preview_collection = managed_bl_object.bl_collection(
managed_bl_object.PREVIEW_COLLECTION_NAME,
view_layer_exclude=False,
)
for bl_object in preview_collection.objects.values():
preview_collection.objects.unlink(bl_object)
# Preview Anything that Should be Previewed (maybe)
if props["auto_3d_preview"]:
self.trigger_action("show_preview")
####################
# - Blender Registration
####################
BL_REGISTER = [
ConsoleViewOperator,
RefreshPlotViewOperator,
ViewerNode,
]
BL_NODES = {
ct.NodeType.Viewer: (
ct.NodeCategory.MAXWELLSIM_OUTPUTS
)
}

View File

@ -0,0 +1,19 @@
from . import sim_domain
#from . import sim_grid
#from . import sim_grid_axes
from . import fdtd_sim
BL_REGISTER = [
*sim_domain.BL_REGISTER,
# *sim_grid.BL_REGISTER,
# *sim_grid_axes.BL_REGISTER,
*fdtd_sim.BL_REGISTER,
]
BL_NODES = {
**sim_domain.BL_NODES,
# **sim_grid.BL_NODES,
# **sim_grid_axes.BL_NODES,
**fdtd_sim.BL_NODES,
}

View File

@ -0,0 +1,69 @@
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from ... import contracts as ct
from ... import sockets
from .. import base
class FDTDSimNode(base.MaxwellSimNode):
node_type = ct.NodeType.FDTDSim
bl_label = "FDTD Simulation"
####################
# - Sockets
####################
input_sockets = {
"Domain": sockets.MaxwellSimDomainSocketDef(),
"BCs": sockets.MaxwellBoundCondsSocketDef(),
"Sources": sockets.MaxwellSourceSocketDef(),
"Structures": sockets.MaxwellStructureSocketDef(),
"Monitors": sockets.MaxwellMonitorSocketDef(),
}
output_sockets = {
"FDTD Sim": sockets.MaxwellFDTDSimSocketDef(),
}
####################
# - Output Socket Computation
####################
@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"]
if not isinstance(sources, list):
sources = [sources]
if not isinstance(structures, list):
structures = [structures]
if not isinstance(monitors, list):
monitors = [monitors]
return td.Simulation(
**sim_domain, ## run_time=, size=, grid=, medium=
structures=structures,
sources=sources,
monitors=monitors,
boundary_spec=bounds,
)
####################
# - Blender Registration
####################
BL_REGISTER = [
FDTDSimNode,
]
BL_NODES = {
ct.NodeType.FDTDSim: (
ct.NodeCategory.MAXWELLSIM_SIMS
)
}

View File

@ -0,0 +1,132 @@
import bpy
import sympy as sp
import sympy.physics.units as spu
import scipy as sc
from .....utils import analyze_geonodes
from ... import contracts as ct
from ... import sockets
from .. import base
from ... import managed_objs
GEONODES_DOMAIN_BOX = "simdomain_box"
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,
),
"Center": sockets.PhysicalSize3DSocketDef(),
"Size": sockets.PhysicalSize3DSocketDef(),
"Grid": sockets.MaxwellSimGridSocketDef(),
"Ambient Medium": sockets.MaxwellMediumSocketDef(),
}
output_sockets = {
"Domain": sockets.MaxwellSimDomainSocketDef(),
}
managed_obj_defs = {
"domain_box": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix="domain_box_",
)
}
####################
# - Callbacks
####################
@base.computes_output_socket(
"Domain",
input_sockets={"Duration", "Center", "Size", "Grid", "Ambient Medium"},
)
def compute_sim_domain(self, input_sockets: dict) -> sp.Expr:
if all([
(_duration := input_sockets["Duration"]),
(_center := input_sockets["Center"]),
(_size := input_sockets["Size"]),
(grid := input_sockets["Grid"]),
(medium := input_sockets["Ambient Medium"]),
]):
duration = spu.convert_to(_duration, spu.second) / spu.second
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
size = tuple(spu.convert_to(_size, spu.um) / spu.um)
return dict(
run_time=duration,
center=center,
size=size,
grid_spec=grid,
medium=medium,
)
####################
# - Preview
####################
@base.on_value_changed(
socket_name={"Center", "Size"},
input_sockets={"Center", "Size"},
managed_objs={"domain_box"},
)
def on_value_changed__center_size(
self,
input_sockets: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
_center = input_sockets["Center"]
center = tuple([
float(el)
for el in spu.convert_to(_center, spu.um) / spu.um
])
_size = input_sockets["Size"]
size = tuple([
float(el)
for el in spu.convert_to(_size, spu.um) / spu.um
])
## TODO: Preview unit system?? Presume um for now
# Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_DOMAIN_BOX]
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc="INPUT"
)
# Sync Modifier Inputs
managed_objs["domain_box"].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface["Size"].identifier: size,
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}
)
# Sync Object Position
managed_objs["domain_box"].bl_object("MESH").location = center
@base.on_show_preview(
managed_objs={"domain_box"},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs["domain_box"].show_preview("MESH")
self.on_value_changed__center_size()
####################
# - Blender Registration
####################
BL_REGISTER = [
SimDomainNode,
]
BL_NODES = {
ct.NodeType.SimDomain: (
ct.NodeCategory.MAXWELLSIM_SIMS
)
}

View File

@ -0,0 +1,5 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,17 @@
from . import automatic_sim_grid_axis
from . import manual_sim_grid_axis
from . import uniform_sim_grid_axis
from . import array_sim_grid_axis
BL_REGISTER = [
*automatic_sim_grid_axis.BL_REGISTER,
*manual_sim_grid_axis.BL_REGISTER,
*uniform_sim_grid_axis.BL_REGISTER,
*array_sim_grid_axis.BL_REGISTER,
]
BL_NODES = {
**automatic_sim_grid_axis.BL_NODES,
**manual_sim_grid_axis.BL_NODES,
**uniform_sim_grid_axis.BL_NODES,
**array_sim_grid_axis.BL_NODES,
}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,27 @@
from . import temporal_shapes
from . import point_dipole_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
BL_REGISTER = [
*temporal_shapes.BL_REGISTER,
*point_dipole_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,
]
BL_NODES = {
**temporal_shapes.BL_NODES,
**point_dipole_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,
}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,178 @@
import typing_extensions as typx
import math
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
import bpy
from .....utils import analyze_geonodes
from ... import managed_objs
from ... import contracts as ct
from ... import sockets
from .. import base
GEONODES_PLANE_WAVE = "source_plane_wave"
def convert_vector_to_spherical(
v: sp.MatrixBase,
) -> tuple[str, str, sp.Expr, sp.Expr]:
"""Converts a vector (maybe normalized) to spherical coordinates from an arbitrary choice of injection axis.
Injection axis is chosen to minimize `theta`
"""
x, y, z = v
injection_axis = max(
('x', abs(x)),
('y', abs(y)),
('z', abs(z)),
key=lambda item: item[1]
)[0]
## Select injection axis that minimizes 'theta'
if injection_axis == "x":
direction = "+" if x >= 0 else "-"
theta = sp.acos(x / sp.sqrt(x**2 + y**2 + z**2))
phi = sp.atan2(z, y)
elif injection_axis == "y":
direction = "+" if y >= 0 else "-"
theta = sp.acos(y / sp.sqrt(x**2 + y**2 + z**2))
phi = sp.atan2(x, z)
else:
direction = "+" if z >= 0 else "-"
theta = sp.acos(z / sp.sqrt(x**2 + y**2 + z**2))
phi = sp.atan2(y, x)
return injection_axis, direction, theta, phi
class PlaneWaveSourceNode(base.MaxwellSimNode):
node_type = ct.NodeType.PlaneWaveSource
bl_label = "Plane Wave Source"
####################
# - Sockets
####################
input_sockets = {
"Temporal Shape": sockets.MaxwellTemporalShapeSocketDef(),
"Center": sockets.PhysicalPoint3DSocketDef(),
"Direction": sockets.Real3DVectorSocketDef(
default_value=sp.Matrix([0, 0, -1])
),
"Pol Angle": sockets.PhysicalAngleSocketDef(),
}
output_sockets = {
"Source": sockets.MaxwellSourceSocketDef(),
}
managed_obj_defs = {
"plane_wave_source": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix="",
)
}
####################
# - Output Socket Computation
####################
@base.computes_output_socket(
"Source",
input_sockets={"Temporal Shape", "Center", "Direction", "Pol Angle"},
)
def compute_source(self, input_sockets: dict):
temporal_shape = input_sockets["Temporal Shape"]
_center = input_sockets["Center"]
direction = input_sockets["Direction"]
pol_angle = input_sockets["Pol Angle"]
injection_axis, dir_sgn, theta, phi = convert_vector_to_spherical(direction)
size = {
"x": (0, math.inf, math.inf),
"y": (math.inf, 0, math.inf),
"z": (math.inf, math.inf, 0),
}[injection_axis]
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
# Display the results
return td.PlaneWave(
center=center,
source_time=temporal_shape,
size=size,
direction=dir_sgn,
angle_theta=theta,
angle_phi=phi,
pol_angle=pol_angle,
)
####################
# - Preview
####################
@base.on_value_changed(
socket_name={"Center", "Direction"},
input_sockets={"Center", "Direction"},
managed_objs={"plane_wave_source"},
)
def on_value_changed__center_direction(
self,
input_sockets: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
_center = input_sockets["Center"]
center = tuple([
float(el)
for el in spu.convert_to(_center, spu.um) / spu.um
])
_direction = input_sockets["Direction"]
direction = tuple([
float(el)
for el in _direction
])
## TODO: Preview unit system?? Presume um for now
# Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_PLANE_WAVE]
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc="INPUT"
)
# Sync Modifier Inputs
managed_objs["plane_wave_source"].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface["Direction"].identifier: direction,
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}
)
# Sync Object Position
managed_objs["plane_wave_source"].bl_object("MESH").location = center
@base.on_show_preview(
managed_objs={"plane_wave_source"},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs["plane_wave_source"].show_preview("MESH")
self.on_value_changed__center_direction()
####################
# - Blender Registration
####################
BL_REGISTER = [
PlaneWaveSourceNode,
]
BL_NODES = {
ct.NodeType.PlaneWaveSource: (
ct.NodeCategory.MAXWELLSIM_SOURCES
)
}

View File

@ -0,0 +1,133 @@
import typing as typ
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
import bpy
from ... import contracts as ct
from ... import sockets
from .. import base
from ... import managed_objs
class PointDipoleSourceNode(base.MaxwellSimNode):
node_type = ct.NodeType.PointDipoleSource
bl_label = "Point Dipole Source"
####################
# - Sockets
####################
input_sockets = {
"Temporal Shape": sockets.MaxwellTemporalShapeSocketDef(),
"Center": sockets.PhysicalPoint3DSocketDef(),
"Interpolate": sockets.BoolSocketDef(
default_value=True,
),
}
output_sockets = {
"Source": sockets.MaxwellSourceSocketDef(),
}
managed_obj_defs = {
"sphere_empty": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix="point_dipole_",
)
}
####################
# - 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",
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)
_res = td.PointDipole(
center=center,
source_time=temporal_shape,
interpolate=interpolate,
polarization=pol_axis,
)
return _res
####################
# - Preview
####################
@base.on_value_changed(
socket_name="Center",
input_sockets={"Center"},
managed_objs={"sphere_empty"},
)
def on_value_changed__center(
self,
input_sockets: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
_center = input_sockets["Center"]
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
## TODO: Preview unit system?? Presume um for now
mobj = managed_objs["sphere_empty"]
bl_object = mobj.bl_object("EMPTY")
bl_object.location = center #tuple([float(el) for el in center])
@base.on_show_preview(
managed_objs={"sphere_empty"},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs["sphere_empty"].show_preview(
"EMPTY",
empty_display_type="SPHERE",
)
####################
# - Blender Registration
####################
BL_REGISTER = [
PointDipoleSourceNode,
]
BL_NODES = {
ct.NodeType.PointDipoleSource: (
ct.NodeCategory.MAXWELLSIM_SOURCES
)
}

View File

@ -0,0 +1,14 @@
from . import gaussian_pulse_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,
]
BL_NODES = {
**gaussian_pulse_temporal_shape.BL_NODES,
# **continuous_wave_temporal_shape.BL_NODES,
# **array_temporal_shape.BL_NODES,
}

View File

@ -0,0 +1,6 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -0,0 +1,77 @@
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
from .... import contracts
from .... import sockets
from ... import base
class ContinuousWaveTemporalShapeNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.ContinuousWaveTemporalShape
bl_label = "Continuous Wave Temporal Shape"
#bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
#"amplitude": sockets.RealNumberSocketDef(
# label="Temporal Shape",
#), ## Should have a unit of some kind...
"phase": sockets.PhysicalAngleSocketDef(
label="Phase",
),
"freq_center": sockets.PhysicalFreqSocketDef(
label="Freq Center",
),
"freq_std": sockets.PhysicalFreqSocketDef(
label="Freq STD",
),
"time_delay_rel_ang_freq": sockets.RealNumberSocketDef(
label="Time Delay rel. Ang. Freq",
default_value=5.0,
),
}
output_sockets = {
"temporal_shape": sockets.MaxwellTemporalShapeSocketDef(
label="Temporal Shape",
),
}
####################
# - 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")
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
return td.ContinuousWave(
amplitude=cheating_amplitude,
phase=phase,
freq0=freq_center,
fwidth=freq_std,
offset=time_delay_rel_ang_freq,
)
####################
# - Blender Registration
####################
BL_REGISTER = [
ContinuousWaveTemporalShapeNode,
]
BL_NODES = {
contracts.NodeType.ContinuousWaveTemporalShape: (
contracts.NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES
)
}

Some files were not shown because too many files have changed in this diff Show More