Compare commits

...

8 Commits

135 changed files with 4492 additions and 2253 deletions

View File

@ -9,3 +9,4 @@ trim_trailing_whitespace = false
[*.yml]
indent_style = space
indent_size = 2

View File

@ -36,7 +36,7 @@
- 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.
- Trigger: "Report" event 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.

View File

@ -1,10 +1,44 @@
####################
# - Project Config
####################
project:
type: website
output-dir: _site
# Website Configuration
format:
html:
toc: true
filters:
- interlinks
interlinks:
sources:
numpy:
url: https://numpy.org/doc/stable/
matplotlib:
url: https://matplotlib.org/stable/
python:
url: https://docs.python.org/3/
metadata-files:
# Sidebar for /pydocs Paths
- pydocs/_sidebar.yml
####################
# - Website Config
####################
website:
title: "Blender Maxwell"
description: "A Blender-based design and analysis tool for electromagnetic simulations"
page-footer: "Copyright 2024, Sofus Albert Høgsbro Rose"
repo-url: https://github.com/so-rose/blender_maxwell/
repo-actions: [issue]
page-navigation: true
navbar:
background: primary
pinned: true
search: true
left:
- file: index.qmd
text: Home
@ -18,19 +52,12 @@ website:
- text: Report a Bug
url: https://github.com/so-rose/blender_maxwell/issues/new/choose
# Auto-Generated Metadata
metadata-files:
# Sidebar for /pydocs Paths
- pydocs/_sidebar.yml
####################
# - quartodoc - Autogenerated Python Docs
# - Quartodoc Config
####################
quartodoc:
# Output
dir: pydocs
#out_index: _api_index.qmd
sidebar: pydocs/_sidebar.yml
# Python Package
@ -43,8 +70,13 @@ quartodoc:
title: "Blender Maxwell"
# Options
renderer:
style: markdown
#show_signature: true
show_signature_annotations: false
display_name: name
options:
include_private: true
#include_private: true
include_empty: true
include_attributes: true
signature_name: "short"
@ -57,16 +89,11 @@ quartodoc:
desc: Build/packaging scripts for developing and publishing the addon.
package: scripts
contents:
- name: info
children: embedded
- name: pack
children: embedded
- name: dev
children: embedded
- name: bl_delete_addon
children: embedded
- name: bl_install_addon
children: embedded
- info
- pack
- dev
- bl_delete_addon
- bl_install_addon
####################
# - bl_maxwell
@ -74,54 +101,39 @@ quartodoc:
- title: "`bl_maxwell`"
desc: Root package for the addon.
contents:
- name: info
children: embedded
- name: preferences
children: embedded
- name: registration
children: embedded
- preferences
- registration
- subtitle: "`bl_maxwell.assets`"
desc: Blender assets bundled w/Blender Maxwell
contents:
- name: assets
children: embedded
- name: assets.import_geonodes
children: embedded
- assets
- assets.import_geonodes
- subtitle: "`bl_maxwell.nodeps`"
desc: No-Dependency
contents:
- name: operators
children: embedded
- operators
- subtitle: "`bl_maxwell.utils`"
desc: Utilities wo/shared global state.
contents:
- name: utils.analyze_geonodes
children: embedded
- name: utils.blender_type_enum
children: embedded
- name: utils.extra_sympy_units
children: embedded
- name: utils.logger
children: embedded
- name: utils.pydantic_sympy
children: embedded
- utils.analyze_geonodes
- utils.blender_type_enum
- utils.extra_sympy_units
- utils.logger
- utils.pydantic_sympy
- subtitle: "`bl_maxwell.services`"
desc: Utilities w/shared global state.
contents:
- name: services.tdcloud
children: embedded
- services.tdcloud
- subtitle: "`bl_maxwell.operators`"
desc: General Blender operators.
contents:
- name: operators.bl_append
children: embedded
- name: operators.connect_viewer
children: embedded
- operators.bl_append
- operators.connect_viewer
####################
# - ..maxwell_sim_nodes
@ -130,73 +142,49 @@ quartodoc:
desc: Maxwell Simulation Design/Viz Node Tree.
package: blender_maxwell.node_trees.maxwell_sim_nodes
contents:
- name: bl_socket_map
children: embedded
- name: categories
children: embedded
- name: bl_cache
children: embedded
- name: node_tree
children: embedded
- bl_socket_map
- categories
- bl_cache
- node_tree
- subtitle: "`contracts`"
desc: Constants and interfaces for identifying resources.
package: blender_maxwell.node_trees.maxwell_sim_nodes.contracts
contents:
# General
- name: bl
children: embedded
- name: data_flows
children: embedded
- name: icons
children: embedded
- flow_kinds
- flow_kinds.FlowKind
- flow_kinds.LazyValueFuncFlow
- icons
- name: trees
children: embedded
- tree_types
# Managed Objects
- name: managed_obj_type
children: embedded
- mobj_types
# Nodes
- name: node_types
children: embedded
- name: node_cats
children: embedded
- name: node_cat_labels
children: embedded
- node_types
- category_types
- category_labels
# Sockets
- name: socket_types
children: embedded
- name: socket_colors
children: embedded
- name: socket_from_bl_desc
children: embedded
- name: socket_from_bl_direct
children: embedded
- name: socket_shapes
children: embedded
- name: socket_units
children: embedded
- socket_types
- socket_colors
- bl_socket_types
- bl_socket_desc_map
- socket_units
- name: unit_systems
children: embedded
- unit_systems
- subtitle: "`managed_objs`"
desc: Maxwell Simulation Design/Viz Node Tree
package: blender_maxwell.node_trees.maxwell_sim_nodes.managed_objs
contents:
- name: managed_bl_collection
children: embedded
- name: managed_bl_empty
children: embedded
- name: managed_bl_image
children: embedded
- name: managed_bl_mesh
children: embedded
- name: managed_bl_modifier
children: embedded
- managed_bl_collection
- managed_bl_empty
- managed_bl_image
- managed_bl_mesh
- managed_bl_modifier
####################
# - ..maxwell_sim_nodes.nodes
@ -205,10 +193,8 @@ quartodoc:
desc: Maxwell Simulation Node Sockets
package: blender_maxwell.node_trees.maxwell_sim_nodes.sockets
contents:
- name: base
children: embedded
- name: scan_socket_defs
children: embedded
- base
- scan_socket_defs
####################
# - ..maxwell_sim_nodes.nodes
@ -217,7 +203,5 @@ quartodoc:
desc: Maxwell Simulation Nodes
package: blender_maxwell.node_trees.maxwell_sim_nodes.nodes
contents:
- name: base
children: embedded
- name: events
children: embedded
- base
- events

View File

View File

@ -14,6 +14,10 @@ dependencies = [
"networkx==3.2.*",
"rich==12.5.*",
"rtree==1.2.*",
"jax[cpu]==0.4.26",
"msgspec[toml]==0.18.6",
"numba==0.59.1",
"jaxtyping==0.2.28",
# Pin Blender 4.1.0-Compatible Versions
## The dependency resolver will report if anything is wonky.
"urllib3==1.26.8",
@ -22,8 +26,6 @@ dependencies = [
"idna==3.3",
"charset-normalizer==2.0.10",
"certifi==2021.10.8",
"jax[cpu]>=0.4.26",
"msgspec[toml]>=0.18.6",
]
readme = "README.md"
requires-python = "~= 3.11"
@ -105,6 +107,7 @@ ignore = [
"B008", # FastAPI uses this for Depends(), Security(), etc. .
"E701", # class foo(Parent): pass or if simple: return are perfectly elegant
"ERA001", # 'Commented-out code' seems to be just about anything to ruff
"F722", # jaxtyping uses type annotations that ruff sees as "syntax error"
# Line Length - Controversy Incoming
## Hot Take: Let the Formatter Worry about Line Length

View File

@ -49,11 +49,14 @@ importlib-metadata==6.11.0
jax==0.4.26
jaxlib==0.4.26
# via jax
jaxtyping==0.2.28
jmespath==1.0.1
# via boto3
# via botocore
kiwisolver==1.4.5
# via matplotlib
llvmlite==0.42.0
# via numba
locket==1.0.0
# via partd
matplotlib==3.8.3
@ -65,13 +68,16 @@ mpmath==1.3.0
# via sympy
msgspec==0.18.6
networkx==3.2
numba==0.59.1
numpy==1.24.3
# via contourpy
# via h5py
# via jax
# via jaxlib
# via jaxtyping
# via matplotlib
# via ml-dtypes
# via numba
# via opt-einsum
# via scipy
# via shapely
@ -142,6 +148,8 @@ toolz==0.12.1
# via dask
# via partd
trimesh==4.2.0
typeguard==2.13.3
# via jaxtyping
types-pyyaml==6.0.12.20240311
# via responses
typing-extensions==4.10.0

View File

@ -48,11 +48,14 @@ importlib-metadata==6.11.0
jax==0.4.26
jaxlib==0.4.26
# via jax
jaxtyping==0.2.28
jmespath==1.0.1
# via boto3
# via botocore
kiwisolver==1.4.5
# via matplotlib
llvmlite==0.42.0
# via numba
locket==1.0.0
# via partd
matplotlib==3.8.3
@ -64,13 +67,16 @@ mpmath==1.3.0
# via sympy
msgspec==0.18.6
networkx==3.2
numba==0.59.1
numpy==1.24.3
# via contourpy
# via h5py
# via jax
# via jaxlib
# via jaxtyping
# via matplotlib
# via ml-dtypes
# via numba
# via opt-einsum
# via scipy
# via shapely
@ -140,6 +146,8 @@ toolz==0.12.1
# via dask
# via partd
trimesh==4.2.0
typeguard==2.13.3
# via jaxtyping
types-pyyaml==6.0.12.20240311
# via responses
typing-extensions==4.10.0

View File

@ -1,13 +1,44 @@
"""A Blender-based system for electromagnetic simulation design and analysis, with deep Tidy3D integration.
# `bl_info`
`bl_info` declares information about the addon to Blender.
However, it is not _dynamically_ read: Blender traverses it using `ast.parse`.
This makes it difficult to synchronize `bl_info` with the project's `pyproject.toml`.
As a workaround, **the addon zip-packer will replace `bl_info` entries**.
The following `bl_info` entries are currently replaced when the ZIP is built:
- `description`: To match the description in `pyproject.toml`.
- `version`: To match the version in `pyproject.toml`.
For more information, see `scripts.pack.BL_INFO_REPLACEMENTS`.
**NOTE**: The find/replace procedure is "dumb" (aka. no regex, no `ast` traversal, etc.).
This is surprisingly robust, so long as use of the deterministic code-formatter `ruff fmt` is enforced.
Still. Be careful around `bl_info`.
Attributes:
bl_info: Information about the addon declared to Blender.
BL_REGISTER_BEFORE_DEPS: Blender classes to register before dependencies are verified as installed.
BL_HOTKEYS: Blender keymap item defs to register before dependencies are verified as installed.
"""
from pathlib import Path
from . import info
import bpy
from . import contracts as ct
from .nodeps.utils import simple_logger
simple_logger.sync_bootstrap_logging(
console_level=info.BOOTSTRAP_LOG_LEVEL,
console_level=ct.addon.BOOTSTRAP_LOG_LEVEL,
)
from . import nodeps, preferences, registration # noqa: E402
from . import preferences, registration # noqa: E402
from .nodeps import operators as nodeps_operators # noqa: E402
from .nodeps.utils import pydeps # noqa: E402
log = simple_logger.get(__name__)
@ -15,9 +46,6 @@ log = simple_logger.get(__name__)
####################
# - Addon Information
####################
# The following parameters are replaced when packing the addon ZIP
## - description
## - version
bl_info = {
'name': 'Maxwell PDE Sim and Viz',
'blender': (4, 1, 0),
@ -28,24 +56,36 @@ bl_info = {
'wiki_url': 'https://git.sofus.io/dtu-courses/bsc_thesis',
'tracker_url': 'https://git.sofus.io/dtu-courses/bsc_thesis/issues',
}
## bl_info MUST readable via. ast.parse
## See scripts/pack.py::BL_INFO_REPLACEMENTS for active replacements
## The mechanism is a 'dumb' - output of 'ruff fmt' MUST be basis for replacing
####################
# - Load and Register Addon
####################
log.info('Loading Before-Deps BL_REGISTER')
BL_REGISTER__BEFORE_DEPS = [
*nodeps.operators.BL_REGISTER,
BL_REGISTER_BEFORE_DEPS: list[ct.BLClass] = [
*nodeps_operators.BL_REGISTER,
*preferences.BL_REGISTER,
]
## TODO: BL_HANDLERS and BL_SOCKET_DEFS
def BL_REGISTER__AFTER_DEPS(path_deps: Path):
log.info('Loading After-Deps BL_REGISTER')
with pydeps.importable_addon_deps(path_deps):
BL_HOTKEYS_BEFORE_DEPS: list[ct.KeymapItemDef] = [
*nodeps_operators.BL_HOTKEYS,
]
def load_main_blclasses(path_pydeps: Path) -> list[ct.BLClass]:
"""Imports all addon classes that rely on Python dependencies.
Notes:
`sys.path` is modified while executing this function.
Parameters:
path_pydeps: The path to the Python dependencies.
Returns:
An ordered list of Blender classes to register.
"""
with pydeps.importable_addon_deps(path_pydeps):
from . import assets, node_trees, operators
return [
*operators.BL_REGISTER,
@ -54,64 +94,114 @@ def BL_REGISTER__AFTER_DEPS(path_deps: Path):
]
log.info('Loading Before-Deps BL_KEYMAP_ITEM_DEFS')
BL_KEYMAP_ITEM_DEFS__BEFORE_DEPS = [
*nodeps.operators.BL_KEYMAP_ITEM_DEFS,
]
def load_main_blhotkeys(path_deps: Path) -> list[ct.KeymapItemDef]:
"""Imports all keymap item defs that rely on Python dependencies.
Notes:
`sys.path` is modified while executing this function.
def BL_KEYMAP_ITEM_DEFS__AFTER_DEPS(path_deps: Path):
log.info('Loading After-Deps BL_KEYMAP_ITEM_DEFS')
Parameters:
path_pydeps: The path to the Python dependencies.
Returns:
An ordered list of Blender keymap item defs to register.
"""
with pydeps.importable_addon_deps(path_deps):
from . import assets, operators
return [
*operators.BL_KEYMAP_ITEM_DEFS,
*assets.BL_KEYMAP_ITEM_DEFS,
*operators.BL_HOTKEYS,
*assets.BL_HOTKEYS,
]
####################
# - Registration
####################
def register():
"""Register the Blender addon."""
log.info('Starting %s Registration', info.ADDON_NAME)
@bpy.app.handlers.persistent
def manage_pydeps(*_):
# ct.addon.operator(
# ct.OperatorType.ManagePyDeps,
# 'INVOKE_DEFAULT',
# path_addon_pydeps='',
# path_addon_reqs='',
# )
ct.addon.prefs().on_addon_pydeps_changed(show_popup_if_deps_invalid=True)
# Register Barebones Addon (enough for PyDeps Installability)
registration.register_classes(BL_REGISTER__BEFORE_DEPS)
registration.register_keymap_items(BL_KEYMAP_ITEM_DEFS__BEFORE_DEPS)
# Retrieve PyDeps Path from Addon Preferences
if (addon_prefs := info.addon_prefs()) is None:
unregister()
msg = f'Addon preferences not found; aborting registration of {info.ADDON_NAME}'
raise RuntimeError(msg)
def register() -> None:
"""Implements a multi-stage addon registration, which accounts for Python dependency management.
# Retrieve PyDeps Path
path_pydeps = addon_prefs.pydeps_path
log.info('Loaded PyDeps Path from Addon Prefs: %s', path_pydeps)
# Multi-Stage Registration
The trouble is that many classes in our addon might require Python dependencies.
if pydeps.check_pydeps(path_pydeps):
log.info('PyDeps Satisfied: Loading Addon %s', info.ADDON_NAME)
addon_prefs.sync_addon_logging()
registration.register_classes(BL_REGISTER__AFTER_DEPS(path_pydeps))
registration.register_keymap_items(BL_KEYMAP_ITEM_DEFS__AFTER_DEPS(path_pydeps))
else:
log.info(
'PyDeps Invalid: Delaying Addon Registration of %s',
info.ADDON_NAME,
## Stage 1: Barebones Addon
Many classes in our addon might require Python dependencies.
However, they may not yet be installed.
To solve this bootstrapping problem in a streamlined manner, we only **guarantee** the registration of a few key classes, including:
- `AddonPreferences`: The addon preferences provide an interface for the user to fix Python dependency problems, thereby triggering subsequent stages.
- `InstallPyDeps`: An operator that installs missing Python dependencies, using Blender's embeded `pip`.
- `UninstallPyDeps`: An operator that uninstalls Python dependencies.
**These classes provide just enough interface to help the user install the missing Python dependencies**.
## Stage 2: Declare Delayed Registration
We may not be able to register any classes that rely on Python dependencies.
However, we can use `registration.delay_registration()` to **delay the registration until it is determined that the Python dependencies are satisfied**.`
For now, we just pass a callback that will import + return a list of classes to register (`load_main_blclasses()`) when the time comes.
## Stage 3: Trigger "PyDeps Changed"
The addon preferences is responsible for storing (and exposing to the user) the path to the Python dependencies.
Thus, the addon preferences method `on_addon_pydeps_changed()` has the responsibility for checking when the dependencies are valid, and running the delayed registrations (and any other delayed setup) in response.
In general, `on_addon_pydeps_changed()` runs whenever the PyDeps path is changed, but it can also be run manually.
As the last part of this process, that's exactly what `register()` does: Runs `on_addon_pydeps_changed()` manually.
Depending on the addon preferences (which persist), one of two things can happen:
1. **Deps Satisfied**: The addon will load without issue: The just-declared "delayed registrations" will run immediately, and all is well.
2. **Deps Not Satisfied**: The user must take action to fix the conflicts due to Python dependencies, before the addon can load. **A popup will show to help the user do so.
Notes:
Called by Blender when enabling the addon.
"""
log.info('Commencing Registration of Addon: %s', ct.addon.NAME)
bpy.app.handlers.load_post.append(manage_pydeps)
# Register Barebones Addon
## Contains all no-dependency BLClasses:
## - Contains AddonPreferences.
## Contains all BLClasses from 'nodeps'.
registration.register_classes(BL_REGISTER_BEFORE_DEPS)
registration.register_hotkeys(BL_HOTKEYS_BEFORE_DEPS)
# Delay Complete Registration until DEPS_SATISFIED
registration.delay_registration_until(
registration.BLRegisterEvent.DepsSatisfied,
then_register_classes=load_main_blclasses,
then_register_hotkeys=load_main_blhotkeys,
)
registration.delay_registration(
registration.EVENT__DEPS_SATISFIED,
classes_cb=BL_REGISTER__AFTER_DEPS,
keymap_item_defs_cb=BL_KEYMAP_ITEM_DEFS__AFTER_DEPS,
)
## TODO: bpy Popup to Deal w/Dependency Errors
# Trigger PyDeps Check
## Deps ARE OK: Delayed registration will trigger.
## Deps NOT OK: User must fix the pydeps, then trigger this method.
ct.addon.prefs().on_addon_pydeps_changed()
def unregister():
"""Unregister the Blender addon."""
log.info('Starting %s Unregister', info.ADDON_NAME)
def unregister() -> None:
"""Unregisters anything that was registered by the addon.
Notes:
Run by Blender when disabling the addon.
This doesn't clean `sys.modules`.
To fully revert to Blender's state before the addon was in use (especially various import-related caches in the Python process), Blender must be restarted.
"""
log.info('Starting %s Unregister', ct.addon.NAME)
registration.unregister_classes()
registration.unregister_keymap_items()
log.info('Finished %s Unregister', info.ADDON_NAME)
registration.unregister_hotkeys()
registration.clear_delayed_registrations()
log.info('Finished %s Unregister', ct.addon.NAME)

View File

@ -4,11 +4,11 @@ BL_REGISTER = [
*import_geonodes.BL_REGISTER,
]
BL_KEYMAP_ITEM_DEFS = [
*import_geonodes.BL_KEYMAP_ITEM_DEFS,
BL_HOTKEYS = [
*import_geonodes.BL_HOTKEYS,
]
__all__ = [
'BL_REGISTER',
'BL_KEYMAP_ITEM_DEFS',
'BL_HOTKEYS',
]

View File

@ -1,22 +1,15 @@
"""Provides for the linking and/or appending of geometry nodes trees from vendored libraries included in Blender maxwell."""
import enum
import typing as typ
from pathlib import Path
import bpy
import typing_extensions as typx
from .. import info
from ..utils import logger
from blender_maxwell import contracts as ct
from blender_maxwell.utils import logger
log = logger.get(__name__)
ImportMethod: typ.TypeAlias = typx.Literal['append', 'link']
BLOperatorStatus: typ.TypeAlias = set[
typx.Literal['RUNNING_MODAL', 'CANCELLED', 'FINISHED', 'PASS_THROUGH', 'INTERFACE']
]
####################
# - GeoNodes Specification
@ -26,6 +19,7 @@ class GeoNodes(enum.StrEnum):
The value of this StrEnum is both the name of the .blend file containing the GeoNodes group, and of the GeoNodes group itself.
"""
# Node Previews
## Input
InputConstantPhysicalPol = '_input_constant_physical_pol'
@ -79,7 +73,7 @@ class GeoNodes(enum.StrEnum):
# GeoNodes Paths
## Internal
GN_INTERNAL_PATH = info.PATH_ASSETS / 'internal' / 'primitives'
GN_INTERNAL_PATH = ct.addon.PATH_ASSETS / 'internal' / 'primitives'
GN_INTERNAL_INPUTS_PATH = GN_INTERNAL_PATH / 'input'
GN_INTERNAL_SOURCES_PATH = GN_INTERNAL_PATH / 'source'
GN_INTERNAL_STRUCTURES_PATH = GN_INTERNAL_PATH / 'structure'
@ -87,7 +81,7 @@ GN_INTERNAL_MONITORS_PATH = GN_INTERNAL_PATH / 'monitor'
GN_INTERNAL_SIMULATIONS_PATH = GN_INTERNAL_PATH / 'simulation'
## Structures
GN_STRUCTURES_PATH = info.PATH_ASSETS / 'structures'
GN_STRUCTURES_PATH = ct.addon.PATH_ASSETS / 'structures'
GN_STRUCTURES_PRIMITIVES_PATH = GN_STRUCTURES_PATH / 'primitives'
GN_PARENT_PATHS: dict[GeoNodes, Path] = {
@ -134,7 +128,6 @@ GN_PARENT_PATHS: dict[GeoNodes, Path] = {
GeoNodes.SimulationSimGridAxisManual: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationSimGridAxisUniform: GN_INTERNAL_SIMULATIONS_PATH,
GeoNodes.SimulationSimGridAxisArray: GN_INTERNAL_SIMULATIONS_PATH,
# Structures
GeoNodes.PrimitiveBox: GN_STRUCTURES_PRIMITIVES_PATH,
GeoNodes.PrimitiveRing: GN_STRUCTURES_PRIMITIVES_PATH,
@ -147,7 +140,7 @@ GN_PARENT_PATHS: dict[GeoNodes, Path] = {
####################
def import_geonodes(
geonodes: GeoNodes,
import_method: ImportMethod,
import_method: ct.BLImportMethod,
) -> bpy.types.GeometryNodeGroup:
"""Given a (name of a) GeoNodes group packaged with Blender Maxwell, link/append it to the current file, and return the node group.
@ -159,10 +152,7 @@ def import_geonodes(
Returns:
A GeoNodes group available in the current .blend file, which can ex. be attached to a 'GeoNodes Structure' node.
"""
if (
import_method == 'link'
and geonodes in bpy.data.node_groups
):
if import_method == 'link' and geonodes in bpy.data.node_groups:
return bpy.data.node_groups[geonodes]
filename = geonodes
@ -273,7 +263,7 @@ class AppendGeoNodes(bpy.types.Operator):
def invoke(self, context: bpy.types.Context, _):
return self.execute(context)
def execute(self, context: bpy.types.Context) -> BLOperatorStatus:
def execute(self, context: bpy.types.Context) -> ct.BLOperatorStatus:
"""Initializes the while-dragging modal handler, which executes custom logic when the mouse button is released.
Runs in response to drag_handler of a `UILayout.template_asset_view`.
@ -296,7 +286,7 @@ class AppendGeoNodes(bpy.types.Operator):
def modal(
self, context: bpy.types.Context, event: bpy.types.Event
) -> BLOperatorStatus:
) -> ct.BLOperatorStatus:
"""When LMB is released, creates a GeoNodes Structure node.
Runs in response to events in the node editor while dragging an asset from the side panel.
@ -385,14 +375,14 @@ class AppendGeoNodes(bpy.types.Operator):
asset_libraries = bpy.context.preferences.filepaths.asset_libraries
if (
asset_library_idx := asset_libraries.find('Blender Maxwell')
) != -1 and asset_libraries['Blender Maxwell'].path != str(info.PATH_ASSETS):
) != -1 and asset_libraries['Blender Maxwell'].path != str(ct.addon.PATH_ASSETS):
bpy.ops.preferences.asset_library_remove(asset_library_idx)
if 'Blender Maxwell' not in asset_libraries:
bpy.ops.preferences.asset_library_add()
asset_library = asset_libraries[-1] ## Since the operator adds to the end
asset_library.name = 'Blender Maxwell'
asset_library.path = str(info.PATH_ASSETS)
asset_library.path = str(ct.addon.PATH_ASSETS)
bpy.types.WindowManager.active_asset_list = bpy.props.CollectionProperty(
type=bpy.types.AssetHandle
@ -406,7 +396,7 @@ BL_REGISTER = [
AppendGeoNodes,
]
BL_KEYMAP_ITEM_DEFS = [
BL_HOTKEYS = [
# {
# '_': [
# AppendGeoNodes.bl_idname,

View File

@ -0,0 +1,45 @@
from . import addon
from .bl import (
BLClass,
BLColorRGBA,
BLEnumID,
BLIconSet,
BLImportMethod,
BLKeymapItem,
BLModifierType,
BLNodeTreeInterfaceID,
BLOperatorStatus,
BLRegionType,
BLSpaceType,
KeymapItemDef,
ManagedObjName,
PresetName,
SocketName,
)
from .operator_types import (
OperatorType,
)
from .panel_types import (
PanelType,
)
__all__ = [
'addon',
'BLClass',
'BLColorRGBA',
'BLEnumID',
'BLIconSet',
'BLImportMethod',
'BLKeymapItem',
'BLModifierType',
'BLNodeTreeInterfaceID',
'BLOperatorStatus',
'BLRegionType',
'BLSpaceType',
'KeymapItemDef',
'ManagedObjName',
'PresetName',
'SocketName',
'OperatorType',
'PanelType',
]

View File

@ -0,0 +1,93 @@
import random
import tomllib
from pathlib import Path
import bpy
import bpy_restrict_state
PATH_ADDON_ROOT = Path(__file__).resolve().parent.parent
with (PATH_ADDON_ROOT / 'pyproject.toml').open('rb') as f:
PROJ_SPEC = tomllib.load(f)
## bl_info is filled with PROJ_SPEC when packing the .zip.
NAME = PROJ_SPEC['project']['name']
VERSION = PROJ_SPEC['project']['version']
####################
# - Assets
####################
PATH_ASSETS = PATH_ADDON_ROOT / 'assets'
####################
# - PyDeps Info
####################
PATH_REQS = PATH_ADDON_ROOT / 'requirements.lock'
DEFAULT_PATH_DEPS = PATH_ADDON_ROOT / '.addon_dependencies'
## requirements.lock is written when packing the .zip.
## By default, the addon pydeps are kept in the addon dir.
####################
# - Local Addon Cache
####################
ADDON_CACHE = PATH_ADDON_ROOT / '.addon_cache'
ADDON_CACHE.mkdir(exist_ok=True)
####################
# - Dynamic Addon Information
####################
def is_loading() -> bool:
"""Checks whether the addon is currently loading.
While an addon is loading, `bpy.context` is temporarily very limited.
For example, operators can't run while the addon is loading.
By checking whether `bpy.context` is limited like this, we can determine whether the addon is currently loading.
Notes:
Since `bpy_restrict_state._RestrictContext` is a very internal thing, this function may be prone to breakage on Blender updates.
**Keep an eye out**!
Returns:
Whether the addon has been fully loaded, such that `bpy.context` is fully accessible.
"""
return isinstance(bpy.context, bpy_restrict_state._RestrictContext)
def operator(name: str, *operator_args, **operator_kwargs) -> None:
# Parse Operator Name
operator_namespace, operator_name = name.split('.')
if operator_namespace != NAME:
msg = f'Tried to call operator {operator_name}, but addon operators may only use the addon operator namespace "{operator_namespace}.<name>"'
raise RuntimeError(msg)
# Addon Not Loading: Run Operator
if not is_loading():
operator = getattr(getattr(bpy.ops, NAME), operator_name)
operator(*operator_args, **operator_kwargs)
else:
msg = f'Tried to call operator "{operator_name}" while addon is loading'
raise RuntimeError(msg)
def prefs() -> bpy.types.AddonPreferences | None:
if (addon := bpy.context.preferences.addons.get(NAME)) is None:
msg = 'Addon is not installed'
raise RuntimeError(msg)
return addon.preferences
####################
# - Logging Info
####################
DEFAULT_LOG_PATH = PATH_ADDON_ROOT / 'addon.log'
DEFAULT_LOG_PATH.touch(exist_ok=True)
## By default, the addon file log writes to the addon dir.
## The initial .log_level contents are written when packing the .zip.
## Subsequent changes are managed by nodeps.utils.simple_logger.py.
PATH_BOOTSTRAP_LOG_LEVEL = PATH_ADDON_ROOT / '.bootstrap_log_level'
with PATH_BOOTSTRAP_LOG_LEVEL.open('r') as f:
BOOTSTRAP_LOG_LEVEL = int(f.read().strip())

View File

@ -0,0 +1,96 @@
import typing as typ
import bpy
####################
# - Blender Strings
####################
BLEnumID = str
SocketName = str
####################
# - Blender Enums
####################
BLImportMethod: typ.TypeAlias = typ.Literal['append', 'link']
BLModifierType: typ.TypeAlias = typ.Literal['NODES', 'ARRAY']
BLNodeTreeInterfaceID: typ.TypeAlias = str
BLIconSet: frozenset[str] = frozenset(
bpy.types.UILayout.bl_rna.functions['prop'].parameters['icon'].enum_items.keys()
)
####################
# - Blender Structs
####################
BLClass: typ.TypeAlias = (
bpy.types.Panel
| bpy.types.UIList
| bpy.types.Menu
| bpy.types.Header
| bpy.types.Operator
| bpy.types.KeyingSetInfo
| bpy.types.RenderEngine
| bpy.types.AssetShelf
| bpy.types.FileHandler
)
BLKeymapItem: typ.TypeAlias = typ.Any ## TODO: Better Type
BLColorRGBA = tuple[float, float, float, float]
####################
# - Operators
####################
BLSpaceType: typ.TypeAlias = typ.Literal[
'EMPTY',
'VIEW_3D',
'IMAGE_EDITOR',
'NODE_EDITOR',
'SEQUENCE_EDITOR',
'CLIP_EDITOR',
'DOPESHEET_EDITOR',
'GRAPH_EDITOR',
'NLA_EDITOR',
'TEXT_EDITOR',
'CONSOLE',
'INFO',
'TOPBAR',
'STATUSBAR',
'OUTLINER',
'PROPERTIES',
'FILE_BROWSER',
'SPREADSHEET',
'PREFERENCES',
]
BLRegionType: typ.TypeAlias = typ.Literal[
'WINDOW',
'HEADER',
'CHANNELS',
'TEMPORARY',
'UI',
'TOOLS',
'TOOL_PROPS',
'ASSET_SHELF',
'ASSET_SHELF_HEADER',
'PREVIEW',
'HUD',
'NAVIGATION_BAR',
'EXECUTE',
'FOOTER',
'TOOL_HEADER',
'XR',
]
BLOperatorStatus: typ.TypeAlias = set[
typ.Literal['RUNNING_MODAL', 'CANCELLED', 'FINISHED', 'PASS_THROUGH', 'INTERFACE']
]
####################
# - Addon Types
####################
KeymapItemDef: typ.TypeAlias = typ.Any ## TODO: Better Type
ManagedObjName = str
####################
# - Blender Strings
####################
PresetName = str

View File

@ -0,0 +1,15 @@
"""Defines Operator Types as an enum, making it easy for any part of the addon to refer to any operator."""
import enum
from ..nodeps.utils import blender_type_enum
from .addon import NAME as ADDON_NAME
@blender_type_enum.prefix_values_with(f'{ADDON_NAME}.')
class OperatorType(enum.StrEnum):
"""Identifiers for addon-defined `bpy.types.Operator`."""
InstallPyDeps = enum.auto()
UninstallPyDeps = enum.auto()
ManagePyDeps = enum.auto()

View File

@ -0,0 +1,12 @@
"""Defines Panel Types as an enum, making it easy for any part of the addon to refer to any panel."""
import enum
from blender_maxwell.nodeps.utils import blender_type_enum
from .addon import NAME as ADDON_NAME
@blender_type_enum.prefix_values_with(f'{ADDON_NAME.upper()}_PT_')
class PanelType(enum.StrEnum):
"""Identifiers for addon-defined `bpy.types.Panel`."""

View File

@ -1,59 +0,0 @@
import tomllib
from pathlib import Path
import bpy
PATH_ADDON_ROOT = Path(__file__).resolve().parent
####################
# - Addon Info
####################
with (PATH_ADDON_ROOT / 'pyproject.toml').open('rb') as f:
PROJ_SPEC = tomllib.load(f)
## bl_info is filled with PROJ_SPEC when packing the .zip.
ADDON_NAME = PROJ_SPEC['project']['name']
ADDON_VERSION = PROJ_SPEC['project']['version']
####################
# - Asset Info
####################
PATH_ASSETS = PATH_ADDON_ROOT / 'assets'
####################
# - PyDeps Info
####################
PATH_REQS = PATH_ADDON_ROOT / 'requirements.lock'
DEFAULT_PATH_DEPS = PATH_ADDON_ROOT / '.addon_dependencies'
## requirements.lock is written when packing the .zip.
## By default, the addon pydeps are kept in the addon dir.
####################
# - Local Addon Cache
####################
ADDON_CACHE = PATH_ADDON_ROOT / '.addon_cache'
ADDON_CACHE.mkdir(exist_ok=True)
## TODO: Addon preferences?
####################
# - Logging Info
####################
DEFAULT_LOG_PATH = PATH_ADDON_ROOT / 'addon.log'
DEFAULT_LOG_PATH.touch(exist_ok=True)
## By default, the addon file log writes to the addon dir.
## The initial .log_level contents are written when packing the .zip.
## Subsequent changes are managed by nodeps.utils.simple_logger.py.
PATH_BOOTSTRAP_LOG_LEVEL = PATH_ADDON_ROOT / '.bootstrap_log_level'
with PATH_BOOTSTRAP_LOG_LEVEL.open('r') as f:
BOOTSTRAP_LOG_LEVEL = int(f.read().strip())
####################
# - Addon Prefs Info
####################
def addon_prefs() -> bpy.types.AddonPreferences | None:
if (addon := bpy.context.preferences.addons.get(ADDON_NAME)) is None:
return None
return addon.preferences

View File

@ -1,9 +1,3 @@
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 categories, node_tree, nodes, sockets

View File

@ -11,8 +11,9 @@ import typing as typ
import bpy
import sympy as sp
from ...utils import extra_sympy_units as spux
from ...utils import logger as _logger
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger as _logger
from . import contracts as ct
from . import sockets

View File

@ -1,79 +1,66 @@
# ruff: noqa: I001
from blender_maxwell.contracts import (
BLClass,
BLColorRGBA,
BLEnumID,
BLIconSet,
BLKeymapItem,
BLModifierType,
BLNodeTreeInterfaceID,
BLOperatorStatus,
BLRegionType,
BLSpaceType,
KeymapItemDef,
ManagedObjName,
OperatorType,
PanelType,
PresetName,
SocketName,
addon,
)
####################
# - 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 .bl_socket_desc_map import BL_SOCKET_DESCR_TYPE_MAP
from .bl_socket_types import BL_SOCKET_DESCR_ANNOT_STRING, BL_SOCKET_DIRECT_TYPE_MAP
from .category_labels import NODE_CAT_LABELS
from .category_types import NodeCategory
from .flow_events import FlowEvent
from .flow_kinds import (
ArrayFlow,
CapabilitiesFlow,
FlowKind,
InfoFlow,
LazyArrayRangeFlow,
LazyValueFuncFlow,
ParamsFlow,
ValueFlow,
)
from .icons import Icon
####################
# - Tree Types
####################
from .trees import TreeType
####################
# - Socket Types
####################
from .socket_types import SocketType
from .socket_units import SOCKET_UNITS
from .mobj_types import ManagedObjType
from .node_types import NodeType
from .socket_colors import SOCKET_COLORS
from .socket_shapes import SOCKET_SHAPES
from .socket_types import SocketType
from .socket_units import SOCKET_UNITS
from .tree_types import TreeType
from .unit_systems import UNITS_BLENDER, UNITS_TIDY3D
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,
DataCapabilities,
DataValue,
DataValueArray,
DataValueSpectrum,
LazyDataValue,
LazyDataValueRange,
LazyDataValueSpectrum,
)
from .data_flow_actions import DataFlowAction
####################
# - Export
####################
__all__ = [
'SocketName',
'PresetName',
'ManagedObjName',
'BLEnumID',
'BLClass',
'BLColorRGBA',
'BLEnumID',
'BLIconSet',
'BLKeymapItem',
'BLModifierType',
'BLNodeTreeInterfaceID',
'BLOperatorStatus',
'BLRegionType',
'BLSpaceType',
'KeymapItemDef',
'ManagedObjName',
'OperatorType',
'PanelType',
'PresetName',
'SocketName',
'addon',
'Icon',
'TreeType',
'SocketType',
@ -89,13 +76,13 @@ __all__ = [
'NodeCategory',
'NODE_CAT_LABELS',
'ManagedObjType',
'DataFlowKind',
'DataCapabilities',
'DataValue',
'DataValueArray',
'DataValueSpectrum',
'LazyDataValue',
'LazyDataValueRange',
'LazyDataValueSpectrum',
'DataFlowAction',
'FlowEvent',
'ArrayFlow',
'CapabilitiesFlow',
'FlowKind',
'InfoFlow',
'LazyArrayRangeFlow',
'LazyValueFuncFlow',
'ParamsFlow',
'ValueFlow',
]

View File

@ -1,35 +0,0 @@
import pydantic as pyd
import typing_extensions as pytypes_ext
####################
# - 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

@ -1,6 +1,6 @@
import enum
from ....utils.blender_type_enum import BlenderTypeEnum, wrap_values_in_MT
from blender_maxwell.utils.blender_type_enum import BlenderTypeEnum, wrap_values_in_MT
@wrap_values_in_MT

View File

@ -1,46 +0,0 @@
import enum
import typing as typ
import typing_extensions as typx
class DataFlowAction(enum.StrEnum):
# Locking
EnableLock = 'enable_lock'
DisableLock = 'disable_lock'
# Value
OutputRequested = 'output_requested'
DataChanged = 'value_changed'
# Previewing
ShowPreview = 'show_preview'
ShowPlot = 'show_plot'
def trigger_direction(action: typ.Self) -> typx.Literal['input', 'output']:
"""When a given action is triggered, all sockets/nodes/... in this direction should be recursively triggered.
Parameters:
action: The action for which to retrieve the trigger direction.
Returns:
The trigger direction, which can be used ex. in nodes to select `node.inputs` or `node.outputs`.
"""
return {
DataFlowAction.EnableLock: 'input',
DataFlowAction.DisableLock: 'input',
DataFlowAction.DataChanged: 'output',
DataFlowAction.OutputRequested: 'input',
DataFlowAction.ShowPreview: 'input',
DataFlowAction.ShowPlot: 'input',
}[action]
def stop_if_no_event_methods(action: typ.Self) -> bool:
return {
DataFlowAction.EnableLock: False,
DataFlowAction.DisableLock: False,
DataFlowAction.DataChanged: True,
DataFlowAction.OutputRequested: True,
DataFlowAction.ShowPreview: False,
DataFlowAction.ShowPlot: False,
}[action]

View File

@ -1,334 +0,0 @@
import dataclasses
import enum
import functools
import typing as typ
from types import MappingProxyType
# import colour ## TODO
import numpy as np
import sympy as sp
import sympy.physics.units as spu
import typing_extensions as typx
from ....utils import extra_sympy_units as spux
from ....utils import sci_constants as constants
from .socket_types import SocketType
class DataFlowKind(enum.StrEnum):
"""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 without any unknown symbols.
- 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.
SpectralValue: A value defined along a spectral range.
- {`np.array`
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).
"""
Capabilities = enum.auto()
# Values
Value = enum.auto()
ValueArray = enum.auto()
ValueSpectrum = enum.auto()
# Lazy
LazyValue = enum.auto()
LazyValueRange = enum.auto()
LazyValueSpectrum = enum.auto()
@classmethod
def scale_to_unit_system(cls, kind: typ.Self, value, socket_type, unit_system):
if kind == cls.Value:
return spux.sympy_to_python(
spux.scale_to_unit(
value,
unit_system[socket_type],
)
)
if kind == cls.LazyValueRange:
return value.rescale_to_unit(unit_system[socket_type])
msg = 'Tried to scale unknown kind'
raise ValueError(msg)
####################
# - Data Structures: Capabilities
####################
@dataclasses.dataclass(frozen=True, kw_only=True)
class DataCapabilities:
socket_type: SocketType
active_kind: DataFlowKind
is_universal: bool = False
def is_compatible_with(self, other: typ.Self) -> bool:
return (
self.socket_type == other.socket_type
and self.active_kind == other.active_kind
) or other.is_universal
####################
# - Data Structures: Non-Lazy
####################
DataValue: typ.TypeAlias = typ.Any
@dataclasses.dataclass(frozen=True, kw_only=True)
class DataValueArray:
"""A simple, flat array of values with an optionally-attached unit.
Attributes:
values: An ND array-like object of arbitrary numerical type.
unit: A `sympy` unit.
None if unitless.
"""
values: typ.Sequence[DataValue]
unit: spu.Quantity | None
@dataclasses.dataclass(frozen=True, kw_only=True)
class DataValueSpectrum:
"""A numerical representation of a spectral distribution.
Attributes:
wls: A 1D `numpy` float array of wavelength values.
wls_unit: The unit of wavelengths, as length dimension.
values: A 1D `numpy` float array of values corresponding to wavelength values.
values_unit: The unit of the value, as arbitrary dimension.
freqs_unit: The unit of the value, as arbitrary dimension.
"""
# Wavelength
wls: np.array
wls_unit: spu.Quantity
# Value
values: np.array
values_unit: spu.Quantity
# Frequency
freqs_unit: spu.Quantity = spu.hertz
@functools.cached_property
def freqs(self) -> np.array:
"""The spectral frequencies, computed from the wavelengths.
Frequencies are NOT reversed, so as to preserve the by-index mapping to `DataValueSpectrum.values`.
Returns:
Frequencies, as a unitless `numpy` array.
Use `DataValueSpectrum.wls_unit` to interpret this return value.
"""
unitless_speed_of_light = spux.sympy_to_python(
spux.scale_to_unit(
constants.vac_speed_of_light, (self.wl_unit / self.freq_unit)
)
)
return unitless_speed_of_light / self.wls
# TODO: Colour Library
# def as_colour_sd(self) -> colour.SpectralDistribution:
# """Returns the `colour` representation of this spectral distribution, ideal for plotting and colorimetric analysis."""
# return colour.SpectralDistribution(data=self.values, domain=self.wls)
####################
# - Data Structures: Lazy
####################
@dataclasses.dataclass(frozen=True, kw_only=True)
class LazyDataValue:
callback: typ.Callable[[...], [DataValue]]
def realize(self, *args: list[DataValue]) -> DataValue:
return self.callback(*args)
@dataclasses.dataclass(frozen=True, kw_only=True)
class LazyDataValueRange:
symbols: set[sp.Symbol]
start: sp.Basic
stop: sp.Basic
steps: int
scaling: typx.Literal['lin', 'geom', 'log'] = 'lin'
has_unit: bool = False
unit: spu.Quantity = False
def rescale_to_unit(self, unit: spu.Quantity) -> typ.Self:
if self.has_unit:
return LazyDataValueRange(
symbols=self.symbols,
has_unit=self.has_unit,
unit=unit,
start=spu.convert_to(self.start, unit),
stop=spu.convert_to(self.stop, unit),
steps=self.steps,
scaling=self.scaling,
)
msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}'
raise ValueError(msg)
def rescale_bounds(
self,
bound_cb: typ.Callable[[sp.Expr], sp.Expr],
reverse: bool = False,
) -> typ.Self:
"""Call a function on both bounds (start and stop), creating a new `LazyDataValueRange`."""
return LazyDataValueRange(
symbols=self.symbols,
has_unit=self.has_unit,
unit=self.unit,
start=spu.convert_to(
bound_cb(self.start if not reverse else self.stop), self.unit
),
stop=spu.convert_to(
bound_cb(self.stop if not reverse else self.start), self.unit
),
steps=self.steps,
scaling=self.scaling,
)
def realize(
self, symbol_values: dict[sp.Symbol, DataValue] = MappingProxyType({})
) -> DataValueArray:
# Realize Symbols
if not self.has_unit:
start = spux.sympy_to_python(self.start.subs(symbol_values))
stop = spux.sympy_to_python(self.stop.subs(symbol_values))
else:
start = spux.sympy_to_python(
spux.scale_to_unit(self.start.subs(symbol_values), self.unit)
)
stop = spux.sympy_to_python(
spux.scale_to_unit(self.stop.subs(symbol_values), self.unit)
)
# Return Linspace / Logspace
if self.scaling == 'lin':
return DataValueArray(
values=np.linspace(start, stop, self.steps), unit=self.unit
)
if self.scaling == 'geom':
return DataValueArray(np.geomspace(start, stop, self.steps), self.unit)
if self.scaling == 'log':
return DataValueArray(np.logspace(start, stop, self.steps), self.unit)
raise NotImplementedError
@dataclasses.dataclass(frozen=True, kw_only=True)
class LazyDataValueSpectrum:
wl_unit: spu.Quantity
value_unit: spu.Quantity
value_expr: sp.Expr
symbols: tuple[sp.Symbol, ...] = ()
freq_symbol: sp.Symbol = sp.Symbol('lamda') # noqa: RUF009
def rescale_to_unit(self, unit: spu.Quantity) -> typ.Self:
raise NotImplementedError
@functools.cached_property
def as_func(self) -> typ.Callable[[DataValue, ...], DataValue]:
"""Generates an optimized function for numerical evaluation of the spectral expression."""
return sp.lambdify([self.freq_symbol, *self.symbols], self.value_expr)
def realize(
self, wl_range: DataValueArray, symbol_values: tuple[DataValue, ...]
) -> DataValueSpectrum:
r"""Realizes the parameterized spectral function as a numerical spectral distribution.
Parameters:
wl_range: The lazy wavelength range to build the concrete spectral distribution with.
symbol_values: Numerical values for each symbol, in the same order as defined in `LazyDataValueSpectrum.symbols`.
The wavelength symbol ($\lambda$ by default) always goes first.
_This is used to call the spectral function using the output of `.as_func()`._
Returns:
The concrete, numerical spectral distribution.
"""
return DataValueSpectrum(
wls=wl_range.values,
wls_unit=self.wl_unit,
values=self.as_func(*list(symbol_values.values())),
values_unit=self.value_unit,
)
#
#
#####################
## - Data Pipeline
#####################
# @dataclasses.dataclass(frozen=True, kw_only=True)
# class DataPipelineDim:
# unit: spu.Quantity | None
#
# class DataPipelineDimType(enum.StrEnum):
# # Map Inputs
# Time = enum.auto()
# Freq = enum.auto()
# Space3D = enum.auto()
# DiffOrder = enum.auto()
#
# # Map Inputs
# Power = enum.auto()
# EVec = enum.auto()
# HVec = enum.auto()
# RelPerm = enum.auto()
#
#
# @dataclasses.dataclass(frozen=True, kw_only=True)
# class LazyDataPipeline:
# dims: list[DataPipelineDim]
#
# def _callable(self):
# """JITs the current pipeline of functions with `jax`."""
#
# def __call__(self):
# pass

View File

@ -0,0 +1,71 @@
import enum
from blender_maxwell.utils.staticproperty import staticproperty
class FlowEvent(enum.StrEnum):
"""Defines an event that can propagate through the graph (node-socket-node-...).
Contrary to `FlowKind`, a `FlowEvent` doesn't propagate any data.
Instead, it allows for dead-simple communication across direct graph connections.
The entire system is built around user-defined event handlers, which are also used internally.
See `events`.
Attributes:
EnableLock: Indicates that the node/socket should enable locking.
Locking prevents the use of the UI, including adding/removing links.
This event can lock a subset of the node tree graph.
DisableLock: Indicates that the node/socket should disable locking.
This event can unlock part of a locked subgraph.
ShowPreview: Indicates that the node/socket should enable its primary preview.
This should be used if a more specific preview-esque event doesn't apply.
ShowPlot: Indicates that the node/socket should enable its plotted preview.
This should generally be used if the node is rendering to an image, for viewing through the Blender image editor.
LinkChanged: Indicates that a link to a node/socket was added/removed.
In nodes, this is accompanied by a `socket_name` to indicate which socket it is that had its links altered.
DataChanged: Indicates that data flowing through a node/socket was altered.
In nodes, this event is accompanied by a `socket_name` or `prop_name`, to indicate which socket/property it is that was changed.
**This event is essential**, as it invalidates all input/output socket caches along its path.
"""
# Lock Events
EnableLock = enum.auto()
DisableLock = enum.auto()
# Preview Events
ShowPreview = enum.auto()
ShowPlot = enum.auto()
# Data Events
LinkChanged = enum.auto()
DataChanged = enum.auto()
# Non-Triggered Events
OutputRequested = enum.auto()
# Properties
@staticproperty
def flow_direction() -> typ.Literal['input', 'output']:
"""Describes the direction in which the event should flow.
Doesn't include `FlowEvent`s that aren't meant to be triggered:
- `OutputRequested`.
Parameters:
event: The event for which to retrieve the trigger direction.
Returns:
The trigger direction, which can be used ex. in nodes to select `node.inputs` or `node.outputs`.
"""
return {
# Lock Events
FlowEvent.EnableLock: 'input',
FlowEvent.DisableLock: 'input',
# Preview Events
FlowEvent.ShowPreview: 'input',
FlowEvent.ShowPlot: 'input',
# Data Events
FlowEvent.LinkChanged: 'output',
FlowEvent.DataChanged: 'output',
}

View File

@ -0,0 +1,641 @@
import dataclasses
import enum
import functools
import typing as typ
from types import MappingProxyType
import jax
import jax.numpy as jnp
import jaxtyping as jtyp
import numba
import sympy as sp
import sympy.physics.units as spu
from blender_maxwell.utils import extra_sympy_units as spux
from .socket_types import SocketType
class FlowKind(enum.StrEnum):
"""Defines a kind of data that can flow between nodes.
Each node link can be thought to contain **multiple pipelines for data to flow along**.
Each pipeline is cached incrementally, and independently, of the others.
Thus, the same socket can easily support several kinds of related data flow at the same time.
Attributes:
Capabilities: Describes a socket's linkeability with other sockets.
Links between sockets with incompatible capabilities will be rejected.
This doesn't need to be defined normally, as there is a default.
However, in some cases, defining it manually to control linkeability more granularly may be desirable.
Value: A generic object, which is "directly usable".
This should be chosen when a more specific flow kind doesn't apply.
Array: An object with dimensions, and possibly a unit.
Whenever a `Value` is defined, a single-element `list` will also be generated by default as `Array`
However, for any other array-like variants (or sockets that only represent array-like objects), `Array` should be defined manually.
LazyValueFunc: A composable function.
Can be used to represent computations for which all data is not yet known, or for which just-in-time compilation can drastically increase performance.
LazyArrayRange: An object that generates an `Array` from range information (start/stop/step/spacing).
This should be used instead of `Array` whenever possible.
Param: A dictionary providing particular parameters for a lazy value.
Info: An dictionary providing extra context about any aspect of flow.
"""
Capabilities = enum.auto()
# Values
Value = enum.auto()
Array = enum.auto()
# Lazy
LazyValueFunc = enum.auto()
LazyArrayRange = enum.auto()
# Auxiliary
Params = enum.auto()
Info = enum.auto()
@classmethod
def scale_to_unit_system(cls, kind: typ.Self, value, socket_type, unit_system):
if kind == cls.Value:
return spux.sympy_to_python(
spux.scale_to_unit(
value,
unit_system[socket_type],
)
)
if kind == cls.LazyArrayRange:
return value.rescale_to_unit(unit_system[socket_type])
msg = 'Tried to scale unknown kind'
raise ValueError(msg)
####################
# - Capabilities
####################
@dataclasses.dataclass(frozen=True, kw_only=True)
class CapabilitiesFlow:
socket_type: SocketType
active_kind: FlowKind
is_universal: bool = False
def is_compatible_with(self, other: typ.Self) -> bool:
return (
self.socket_type == other.socket_type
and self.active_kind == other.active_kind
) or other.is_universal
####################
# - Value
####################
ValueFlow: typ.TypeAlias = typ.Any
####################
# - Value Array
####################
@dataclasses.dataclass(frozen=True, kw_only=True)
class ArrayFlow:
"""A simple, flat array of values with an optionally-attached unit.
Attributes:
values: An ND array-like object of arbitrary numerical type.
unit: A `sympy` unit.
None if unitless.
"""
values: jtyp.Shaped[jtyp.Array, '...']
unit: spu.Quantity | None = None
def correct_unit(self, corrected_unit: spu.Quantity) -> typ.Self:
if self.unit is not None:
return ArrayFlow(values=self.values, unit=corrected_unit)
msg = f'Tried to correct unit of unitless LazyDataValueRange "{corrected_unit}"'
raise ValueError(msg)
def rescale_to_unit(self, unit: spu.Quantity) -> typ.Self:
if self.unit is not None:
return ArrayFlow(
values=float(spux.scaling_factor(self.unit, unit)) * self.values,
unit=unit,
)
## TODO: Is this scaling numerically stable?
msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}'
raise ValueError(msg)
####################
# - Lazy Value Func
####################
LazyFunction: typ.TypeAlias = typ.Callable[[typ.Any, ...], ValueFlow]
@dataclasses.dataclass(frozen=True, kw_only=True)
class LazyValueFuncFlow:
r"""Wraps a composable function, providing useful information and operations.
# Data Flow as Function Composition
When using nodes to do math, it can be a good idea to express a **flow of data as the composition of functions**.
Each node creates a new function, which uses the still-unknown (aka. **lazy**) output of the previous function to plan some calculations.
Some new arguments may also be added, of course.
## Root Function
Of course, one needs to select a "bottom" function, which has no previous function as input.
Thus, the first step is to define this **root function**:
$$
f_0:\ \ \ \ \biggl(
\underbrace{a_1, a_2, ..., a_p}_{\texttt{args}},\
\underbrace{
\begin{bmatrix} k_1 \\ v_1\end{bmatrix},
\begin{bmatrix} k_2 \\ v_2\end{bmatrix},
...,
\begin{bmatrix} k_q \\ v_q\end{bmatrix}
}_{\texttt{kwargs}}
\biggr) \to \text{output}_0
$$
We'll express this simple snippet like so:
```python
# Presume 'A0', 'KV0' contain only the args/kwargs for f_0
## 'A0', 'KV0' are of length 'p' and 'q'
def f_0(*args, **kwargs): ...
lazy_value_func_0 = LazyValueFuncFlow(
func=f_0,
func_args=[(a_i, type(a_i)) for a_i in A0],
func_kwargs={k: v for k,v in KV0},
)
output_0 = lazy_value_func.func(*A0_computed, **KV0_computed)
```
So far so good.
But of course, nothing interesting has really happened yet.
## Composing Functions
The key thing is the next step: The function that uses the result of $f_0$!
$$
f_1:\ \ \ \ \biggl(
f_0(...),\ \
\underbrace{\{a_i\}_p^{p+r}}_{\texttt{args[p:]}},\
\underbrace{\biggl\{
\begin{bmatrix} k_i \\ v_i\end{bmatrix}
\biggr\}_q^{q+s}}_{\texttt{kwargs[p:]}}
\biggr) \to \text{output}_1
$$
Notice that _$f_1$ needs the arguments of both $f_0$ and $f_1$_.
Tracking arguments is already getting out of hand; we already have to use `...` to keep it readeable!
But doing so with `LazyValueFunc` is not so complex:
```python
# Presume 'A1', 'K1' contain only the args/kwarg names for f_1
## 'A1', 'KV1' are therefore of length 'r' and 's'
def f_1(output_0, *args, **kwargs): ...
lazy_value_func_1 = lazy_value_func_0.compose_within(
enclosing_func=f_1,
enclosing_func_args=[(a_i, type(a_i)) for a_i in A1],
enclosing_func_kwargs={k: type(v) for k,v in K1},
)
A_computed = A0_computed + A1_computed
KW_computed = KV0_computed + KV1_computed
output_1 = lazy_value_func_1.func(*A_computed, **KW_computed)
```
We only need the arguments to $f_1$, and `LazyValueFunc` figures out how to make one function with enough arguments to call both.
## Isn't Laying Functions Slow/Hard?
Imagine that each function represents the action of a node, each of which performs expensive calculations on huge `numpy` arrays (**as one does when processing electromagnetic field data**).
At the end, a node might run the entire procedure with all arguments:
```python
output_n = lazy_value_func_n.func(*A_all, **KW_all)
```
It's rough: Most non-trivial pipelines drown in the time/memory overhead of incremental `numpy` operations - individually fast, but collectively iffy.
The killer feature of `LazyValueFuncFlow` is a sprinkle of black magic:
```python
func_n_jax = lazy_value_func_n.func_jax
output_n = func_n_jax(*A_all, **KW_all) ## Runs on your GPU
```
What happened was, **the entire pipeline** was compiled and optimized for high performance on not just your CPU, _but also (possibly) your GPU_.
All the layered function calls and inefficient incremental processing is **transformed into a high-performance program**.
Thank `jax` - specifically, `jax.jit` (https://jax.readthedocs.io/en/latest/_autosummary/jax.jit.html#jax.jit), which internally enables this magic with a single function call.
## Other Considerations
**Auto-Differentiation**: Incredibly, `jax.jit` isn't the killer feature of `jax`. The function that comes out of `LazyValueFuncFlow` can also be differentiated with `jax.grad` (read: high-performance Jacobians for optimizing input parameters).
Though designed for machine learning, there's no reason other fields can't enjoy their inventions!
**Impact of Independent Caching**: JIT'ing can be slow.
That's why `LazyValueFuncFlow` has its own `FlowKind` "lane", which means that **only changes to the processing procedures will cause recompilation**.
Generally, adjustable values that affect the output will flow via the `Param` "lane", which has its own incremental caching, and only meets the compiled function when it's "plugged in" for final evaluation.
The effect is a feeling of snappiness and interactivity, even as the volume of data grows.
Attributes:
func: The function that the object encapsulates.
bound_args: Arguments that will be packaged into function, which can't be later modifier.
func_kwargs: Arguments to be specified by the user at the time of use.
supports_jax: Whether the contained `self.function` can be compiled with JAX's JIT compiler.
supports_numba: Whether the contained `self.function` can be compiled with Numba's JIT compiler.
"""
func: LazyFunction
func_args: list[tuple[str, type]] = MappingProxyType({})
func_kwargs: dict[str, type] = MappingProxyType({})
supports_jax: bool = False
supports_numba: bool = False
# Composition
def compose_within(
self,
enclosing_func: LazyFunction,
enclosing_func_args: list[tuple[str, type]] = (),
enclosing_func_kwargs: dict[str, type] = MappingProxyType({}),
supports_jax: bool = False,
supports_numba: bool = False,
) -> typ.Self:
return LazyValueFuncFlow(
function=lambda *args, **kwargs: enclosing_func(
self.func(
*list(args[len(self.func_args) :]),
**{k: v for k, v in kwargs.items() if k in self.func_kwargs},
),
**kwargs,
),
func_args=self.func_args + enclosing_func_args,
func_kwargs=self.func_kwargs | enclosing_func_kwargs,
supports_jax=self.supports_jax and supports_jax,
supports_numba=self.supports_numba and supports_numba,
)
@functools.cached_property
def func_jax(self) -> LazyFunction:
if self.supports_jax:
return jax.jit(self.func)
msg = 'Can\'t express LazyValueFuncFlow as JAX function (using jax.jit), since "self.supports_jax" is False'
raise ValueError(msg)
@functools.cached_property
def func_numba(self) -> LazyFunction:
if self.supports_numba:
return numba.jit(self.func)
msg = 'Can\'t express LazyValueFuncFlow as Numba function (using numba.jit), since "self.supports_numba" is False'
raise ValueError(msg)
####################
# - Lazy Array Range
####################
@dataclasses.dataclass(frozen=True, kw_only=True)
class LazyArrayRangeFlow:
r"""Represents a linearly/logarithmically spaced array using symbolic boundary expressions, with support for units and lazy evaluation.
# Advantages
Whenever an array can be represented like this, the advantages over an `ArrayFlow` are numerous.
## Memory
`ArrayFlow` generally has a memory scaling of $O(n)$.
Naturally, `LazyArrayRangeFlow` is always constant, since only the boundaries and steps are stored.
## Symbolic
Both boundary points are symbolic expressions, within which pre-defined `sp.Symbol`s can participate in a constrained manner (ex. an integer symbol).
One need not know the value of the symbols immediately - such decisions can be deferred until later in the computational flow.
## Performant Unit-Aware Operations
While `ArrayFlow`s are also unit-aware, the time-cost of _any_ unit-scaling operation scales with $O(n)$.
`LazyArrayRangeFlow`, by contrast, scales as $O(1)$.
As a result, more complicated operations (like symbolic or unit-based) that might be difficult to perform interactively in real-time on an `ArrayFlow` will work perfectly with this object, even with added complexity
## High-Performance Composition and Gradiant
With `self.as_func`, a `jax` function is produced that generates the array according to the symbolic `start`, `stop` and `steps`.
There are two nice things about this:
- **Gradient**: The gradient of the output array, with respect to any symbols used to define the input bounds, can easily be found using `jax.grad` over `self.as_func`.
- **JIT**: When `self.as_func` is composed with other `jax` functions, and `jax.jit` is run to optimize the entire thing, the "cost of array generation" _will often be optimized away significantly or entirely_.
Thus, as part of larger computations, the performance properties of `LazyArrayRangeFlow` is extremely favorable.
## Numerical Properties
Since the bounds support exact (ex. rational) calculations and symbolic manipulations (_by virtue of being symbolic expressions_), the opportunities for certain kinds of numerical instability are mitigated.
Attributes:
start: An expression generating a scalar, unitless, complex value for the array's lower bound.
_Integer, rational, and real values are also supported._
stop: An expression generating a scalar, unitless, complex value for the array's upper bound.
_Integer, rational, and real values are also supported._
steps: The amount of steps (**inclusive**) to generate from `start` to `stop`.
scaling: The method of distributing `step` values between the two endpoints.
Generally, the linear default is sufficient.
unit: The unit of the generated array values
int_symbols: Set of integer-valued variables from which `start` and/or `stop` are determined.
real_symbols: Set of real-valued variables from which `start` and/or `stop` are determined.
complex_symbols: Set of complex-valued variables from which `start` and/or `stop` are determined.
"""
start: spux.ScalarUnitlessComplexExpr
stop: spux.ScalarUnitlessComplexExpr
steps: int
scaling: typ.Literal['lin', 'geom', 'log'] = 'lin'
unit: spux.Unit | None = None
int_symbols: set[spux.IntSymbol] = frozenset()
real_symbols: set[spux.RealSymbol] = frozenset()
complex_symbols: set[spux.ComplexSymbol] = frozenset()
@functools.cached_property
def symbols(self) -> list[sp.Symbol]:
"""Retrieves all symbols by concatenating int, real, and complex symbols, and sorting them by name.
The order is guaranteed to be **deterministic**.
Returns:
All symbols valid for use in the expression.
"""
return sorted(
self.int_symbols | self.real_symbols | self.complex_symbols,
key=lambda sym: sym.name,
)
####################
# - Units
####################
def correct_unit(self, corrected_unit: spux.Unit) -> typ.Self:
"""Replaces the unit without rescaling the unitless bounds.
Parameters:
corrected_unit: The unit to replace the current unit with.
Returns:
A new `LazyArrayRangeFlow` with replaced unit.
Raises:
ValueError: If the existing unit is `None`, indicating that there is no unit to correct.
"""
if self.unit is not None:
return LazyArrayRangeFlow(
start=self.start,
stop=self.stop,
steps=self.steps,
scaling=self.scaling,
unit=corrected_unit,
int_symbols=self.int_symbols,
real_symbols=self.real_symbols,
complex_symbols=self.complex_symbols,
)
msg = f'Tried to correct unit of unitless LazyDataValueRange "{corrected_unit}"'
raise ValueError(msg)
def rescale_to_unit(self, unit: spux.Unit) -> typ.Self:
"""Replaces the unit, **with** rescaling of the bounds.
Parameters:
unit: The unit to convert the bounds to.
Returns:
A new `LazyArrayRangeFlow` with replaced unit.
Raises:
ValueError: If the existing unit is `None`, indicating that there is no unit to correct.
"""
if self.unit is not None:
return LazyArrayRangeFlow(
start=spu.convert_to(self.start, unit),
stop=spu.convert_to(self.stop, unit),
steps=self.steps,
scaling=self.scaling,
unit=unit,
symbols=self.symbols,
int_symbols=self.int_symbols,
real_symbols=self.real_symbols,
complex_symbols=self.complex_symbols,
)
msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}'
raise ValueError(msg)
####################
# - Bound Operations
####################
def rescale_bounds(
self,
scaler: typ.Callable[
[spux.ScalarUnitlessComplexExpr], spux.ScalarUnitlessComplexExpr
],
reverse: bool = False,
) -> typ.Self:
"""Apply a function to the bounds, effectively rescaling the represented array.
Notes:
**It is presumed that the bounds are scaled with the same factor**.
Breaking this presumption may have unexpected results.
The scalar, unitless, complex-valuedness of the bounds must also be respected; additionally, new symbols must not be introduced.
Parameters:
scaler: The function that scales each bound.
reverse: Whether to reverse the bounds after running the `scaler`.
Returns:
A rescaled `LazyArrayRangeFlow`.
"""
return LazyArrayRangeFlow(
start=spu.convert_to(
scaler(self.start if not reverse else self.stop), self.unit
),
stop=spu.convert_to(
scaler(self.stop if not reverse else self.start), self.unit
),
steps=self.steps,
scaling=self.scaling,
unit=self.unit,
int_symbols=self.int_symbols,
real_symbols=self.real_symbols,
complex_symbols=self.complex_symbols,
)
####################
# - Lazy Representation
####################
@functools.cached_property
def array_generator(
self,
) -> typ.Callable[
[int | float | complex, int | float | complex, int],
jtyp.Inexact[jtyp.Array, ' steps'],
]:
"""Compute the correct `jnp.*space` array generator, where `*` is one of the supported scaling methods.
Returns:
A `jax` function that takes a valid `start`, `stop`, and `steps`, and returns a 1D `jax` array.
"""
jnp_nspace = {
'lin': jnp.linspace,
'geom': jnp.geomspace,
'log': jnp.logspace,
}.get(self.scaling)
if jnp_nspace is None:
msg = f'ArrayFlow scaling method {self.scaling} is unsupported'
raise RuntimeError(msg)
return jnp_nspace
@functools.cached_property
def as_func(
self,
) -> typ.Callable[[int | float | complex, ...], jtyp.Inexact[jtyp.Array, ' steps']]:
"""Create a function that can compute the non-lazy output array as a function of the symbols in the expressions for `start` and `stop`.
Notes:
The ordering of the symbols is identical to `self.symbols`, which is guaranteed to be a deterministically sorted list of symbols.
Returns:
A `LazyValueFuncFlow` that, given the input symbols defined in `self.symbols`,
"""
# Compile JAX Functions for Start/End Expressions
## FYI, JAX-in-JAX works perfectly fine.
start_jax = sp.lambdify(self.symbols, self.start, 'jax')
stop_jax = sp.lambdify(self.symbols, self.stop, 'jax')
# Compile ArrayGen Function
def gen_array(
*args: list[int | float | complex],
) -> jtyp.Inexact[jtyp.Array, ' steps']:
return self.array_generator(start_jax(*args), stop_jax(*args), self.steps)
# Return ArrayGen Function
return gen_array
@functools.cached_property
def as_lazy_value_func(self) -> LazyValueFuncFlow:
"""Creates a `LazyValueFuncFlow` using the output of `self.as_func`.
This is useful for ex. parameterizing the first array in the node graph, without binding an entire computed array.
Notes:
The the function enclosed in the `LazyValueFuncFlow` is identical to the one returned by `self.as_func`.
Returns:
A `LazyValueFuncFlow` containing `self.as_func`, as well as appropriate supporting settings.
"""
return LazyValueFuncFlow(
func=self.as_func,
func_args=[
(sym.name, spux.sympy_to_python_type(sym)) for sym in self.symbols
],
supports_jax=True,
)
####################
# - Realization
####################
def realize(
self,
symbol_values: dict[spux.Symbol, ValueFlow] = MappingProxyType({}),
kind: typ.Literal[FlowKind.Array, FlowKind.LazyValueFunc] = FlowKind.Array,
) -> ArrayFlow | LazyValueFuncFlow:
"""Apply a function to the bounds, effectively rescaling the represented array.
Notes:
**It is presumed that the bounds are scaled with the same factor**.
Breaking this presumption may have unexpected results.
The scalar, unitless, complex-valuedness of the bounds must also be respected; additionally, new symbols must not be introduced.
Parameters:
scaler: The function that scales each bound.
reverse: Whether to reverse the bounds after running the `scaler`.
Returns:
A rescaled `LazyArrayRangeFlow`.
"""
if not set(self.symbols).issubset(set(symbol_values.keys())):
msg = f'Provided symbols ({set(symbol_values.keys())}) do not provide values for all expression symbols ({self.symbols}) that may be found in the boundary expressions (start={self.start}, end={self.end})'
raise ValueError(msg)
# Realize Symbols
realized_start = spux.sympy_to_python(
self.start.subs({sym: symbol_values[sym.name] for sym in self.symbols})
)
realized_stop = spux.sympy_to_python(
self.stop.subs({sym: symbol_values[sym.name] for sym in self.symbols})
)
# Return Linspace / Logspace
def gen_array() -> jtyp.Inexact[jtyp.Array, ' steps']:
return self.array_generator(realized_start, realized_stop, self.steps)
if kind == FlowKind.Array:
return ArrayFlow(values=gen_array(), unit=self.unit)
if kind == FlowKind.LazyValueFunc:
return LazyValueFuncFlow(func=gen_array, supports_jax=True)
msg = f'Invalid kind: {kind}'
raise TypeError(msg)
####################
# - Params
####################
ParamsFlow: typ.TypeAlias = dict[str, typ.Any]
####################
# - Lazy Value Func
####################
@dataclasses.dataclass(frozen=True, kw_only=True)
class InfoFlow:
func_args: list[tuple[str, type]] = MappingProxyType({})
func_kwargs: dict[str, type] = MappingProxyType({})
# Dimension Information
has_ndims: bool = False
dim_names: list[str] = ()
dim_idx: dict[str, ArrayFlow | LazyArrayRangeFlow] = MappingProxyType({})
## TODO: Validation, esp. length of dims. Pydantic?
def compose_within(
self,
enclosing_func_args: list[tuple[str, type]] = (),
enclosing_func_kwargs: dict[str, type] = MappingProxyType({}),
) -> typ.Self:
return InfoFlow(
func_args=self.func_args + enclosing_func_args,
func_kwargs=self.func_kwargs | enclosing_func_kwargs,
)
def call_lazy_value_func(
self,
lazy_value_func: LazyValueFuncFlow,
*args: list[typ.Any],
**kwargs: dict[str, typ.Any],
) -> tuple[list[typ.Any], dict[str, typ.Any]]:
if lazy_value_func.supports_jax:
lazy_value_func.func_jax(*args, **kwargs)
lazy_value_func.func(*args, **kwargs)

View File

@ -1,4 +1,4 @@
from ....utils.blender_type_enum import BlenderTypeEnum
from blender_maxwell.utils.blender_type_enum import BlenderTypeEnum
class Icon(BlenderTypeEnum):

View File

@ -1,6 +1,6 @@
import enum
from ....utils.blender_type_enum import BlenderTypeEnum
from blender_maxwell.utils.blender_type_enum import BlenderTypeEnum
class ManagedObjType(BlenderTypeEnum):

View File

@ -1,6 +1,6 @@
import enum
from ....utils.blender_type_enum import (
from blender_maxwell.utils.blender_type_enum import (
BlenderTypeEnum,
append_cls_name_to_values,
)
@ -8,7 +8,7 @@ from ....utils.blender_type_enum import (
@append_cls_name_to_values
class NodeType(BlenderTypeEnum):
#KitchenSink = enum.auto()
# KitchenSink = enum.auto()
# Analysis
Viz = enum.auto()
@ -24,18 +24,18 @@ class NodeType(BlenderTypeEnum):
UnitSystem = enum.auto()
## Inputs / Scene
#Time = enum.auto()
# Time = enum.auto()
## Inputs / Web Importers
Tidy3DWebImporter = enum.auto()
## Inputs / File Importers
Tidy3DFileImporter = enum.auto()
## Inputs / Constants
ExprConstant = enum.auto()
ScientificConstant = enum.auto()
NumberConstant = enum.auto()
PhysicalConstant = enum.auto()
BlenderConstant = enum.auto()
# Outputs
Viewer = enum.auto()
## Outputs / File Exporters
@ -48,43 +48,43 @@ class NodeType(BlenderTypeEnum):
PointDipoleSource = enum.auto()
PlaneWaveSource = enum.auto()
UniformCurrentSource = enum.auto()
#ModeSource = enum.auto()
#GaussianBeamSource = enum.auto()
#AstigmaticGaussianBeamSource = enum.auto()
#TFSFSource = enum.auto()
#EHEquivalenceSource = enum.auto()
#EHSource = enum.auto()
# ModeSource = enum.auto()
# GaussianBeamSource = enum.auto()
# AstigmaticGaussianBeamSource = enum.auto()
# TFSFSource = enum.auto()
# EHEquivalenceSource = enum.auto()
# EHSource = enum.auto()
## Sources / Temporal Shapes
GaussianPulseTemporalShape = enum.auto()
#ContinuousWaveTemporalShape = enum.auto()
#ArrayTemporalShape = enum.auto()
# ContinuousWaveTemporalShape = enum.auto()
# ArrayTemporalShape = 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()
# 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()
# AddNonLinearity = enum.auto()
# ChiThreeSusceptibilityNonLinearity = enum.auto()
# TwoPhotonAbsorptionNonLinearity = enum.auto()
# KerrNonLinearity = enum.auto()
# Structures
#ObjectStructure = enum.auto()
# ObjectStructure = enum.auto()
GeoNodesStructure = enum.auto()
#ScriptedStructure = enum.auto()
# ScriptedStructure = enum.auto()
## Structures / Primitives
BoxStructure = enum.auto()
SphereStructure = enum.auto()
#CylinderStructure = enum.auto()
# CylinderStructure = enum.auto()
# Bounds
BoundConds = enum.auto()
@ -99,22 +99,22 @@ class NodeType(BlenderTypeEnum):
# Monitors
EHFieldMonitor = enum.auto()
PowerFluxMonitor = enum.auto()
#EpsilonTensorMonitor = enum.auto()
#DiffractionMonitor = enum.auto()
# EpsilonTensorMonitor = enum.auto()
# DiffractionMonitor = enum.auto()
## Monitors / Projected
#CartesianNearFieldProjectionMonitor = enum.auto()
#ObservationAngleNearFieldProjectionMonitor = enum.auto()
#KSpaceNearFieldProjectionMonitor = enum.auto()
# CartesianNearFieldProjectionMonitor = enum.auto()
# ObservationAngleNearFieldProjectionMonitor = enum.auto()
# KSpaceNearFieldProjectionMonitor = enum.auto()
# Sims
FDTDSim = enum.auto()
SimDomain = enum.auto()
SimGrid = enum.auto()
## Sims / Sim Grid Axis
#AutomaticSimGridAxis = enum.auto()
#ManualSimGridAxis = enum.auto()
#UniformSimGridAxis = enum.auto()
#ArraySimGridAxis = enum.auto()
# AutomaticSimGridAxis = enum.auto()
# ManualSimGridAxis = enum.auto()
# UniformSimGridAxis = enum.auto()
# ArraySimGridAxis = enum.auto()
# Utilities
Combine = enum.auto()

View File

@ -7,6 +7,7 @@ SOCKET_COLORS = {
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
ST.Expr: (0.5, 0.5, 0.5, 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

View File

@ -1,63 +0,0 @@
from .socket_types import SocketType as ST
SOCKET_SHAPES = {
# Basic
ST.Any: 'CIRCLE',
ST.Bool: 'CIRCLE',
ST.String: 'CIRCLE',
ST.FilePath: 'CIRCLE',
# Number
ST.IntegerNumber: 'CIRCLE',
ST.RationalNumber: 'CIRCLE',
ST.RealNumber: 'CIRCLE',
ST.ComplexNumber: 'CIRCLE',
# Vector
ST.Integer2DVector: 'CIRCLE',
ST.Real2DVector: 'CIRCLE',
ST.Complex2DVector: 'CIRCLE',
ST.Integer3DVector: 'CIRCLE',
ST.Real3DVector: 'CIRCLE',
ST.Complex3DVector: 'CIRCLE',
# Physical
ST.PhysicalUnitSystem: 'CIRCLE',
ST.PhysicalTime: 'CIRCLE',
ST.PhysicalAngle: 'CIRCLE',
ST.PhysicalLength: 'CIRCLE',
ST.PhysicalArea: 'CIRCLE',
ST.PhysicalVolume: 'CIRCLE',
ST.PhysicalPoint2D: 'CIRCLE',
ST.PhysicalPoint3D: 'CIRCLE',
ST.PhysicalSize2D: 'CIRCLE',
ST.PhysicalSize3D: 'CIRCLE',
ST.PhysicalMass: 'CIRCLE',
ST.PhysicalSpeed: 'CIRCLE',
ST.PhysicalAccelScalar: 'CIRCLE',
ST.PhysicalForceScalar: 'CIRCLE',
ST.PhysicalAccel3D: 'CIRCLE',
ST.PhysicalForce3D: 'CIRCLE',
ST.PhysicalPol: 'CIRCLE',
ST.PhysicalFreq: 'CIRCLE',
# Blender
ST.BlenderMaterial: 'DIAMOND',
ST.BlenderObject: 'DIAMOND',
ST.BlenderCollection: 'DIAMOND',
ST.BlenderImage: 'DIAMOND',
ST.BlenderGeoNodes: 'DIAMOND',
ST.BlenderText: 'DIAMOND',
# Maxwell
ST.MaxwellSource: 'CIRCLE',
ST.MaxwellTemporalShape: 'CIRCLE',
ST.MaxwellMedium: 'CIRCLE',
ST.MaxwellMediumNonLinearity: 'CIRCLE',
ST.MaxwellStructure: 'CIRCLE',
ST.MaxwellBoundConds: 'CIRCLE',
ST.MaxwellBoundCond: 'CIRCLE',
ST.MaxwellMonitor: 'CIRCLE',
ST.MaxwellFDTDSim: 'CIRCLE',
ST.MaxwellFDTDSimData: 'CIRCLE',
ST.MaxwellSimGrid: 'CIRCLE',
ST.MaxwellSimGridAxis: 'CIRCLE',
ST.MaxwellSimDomain: 'CIRCLE',
# Tidy3D
ST.Tidy3DCloudTask: 'DIAMOND',
}

View File

@ -1,6 +1,6 @@
import enum
from ....utils.blender_type_enum import (
from blender_maxwell.utils.blender_type_enum import (
BlenderTypeEnum,
append_cls_name_to_values,
)
@ -14,6 +14,7 @@ class SocketType(BlenderTypeEnum):
String = enum.auto()
FilePath = enum.auto()
Color = enum.auto()
Expr = enum.auto()
# Number
IntegerNumber = enum.auto()

View File

@ -1,6 +1,7 @@
import sympy.physics.units as spu
from ....utils import extra_sympy_units as spux
from blender_maxwell.utils import extra_sympy_units as spux
from .socket_types import SocketType as ST # noqa: N817
SOCKET_UNITS = {

View File

@ -1,6 +1,6 @@
import enum
from ....utils.blender_type_enum import (
from blender_maxwell.utils.blender_type_enum import (
BlenderTypeEnum,
append_cls_name_to_values,
)

View File

@ -2,8 +2,9 @@ import typing as typ
import sympy.physics.units as spu
from ....utils import extra_sympy_units as spux
from ....utils.pydantic_sympy import SympyExpr
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils.pydantic_sympy import SympyExpr
from .socket_types import SocketType as ST # noqa: N817
from .socket_units import SOCKET_UNITS

View File

@ -1,7 +1,7 @@
import bpy
from ....utils import logger
from blender_maxwell.utils import logger
log = logger.get(__name__)

View File

@ -7,9 +7,9 @@ import jax.numpy as jnp
import matplotlib
import matplotlib.axis as mpl_ax
import numpy as np
import typing_extensions as typx
from ....utils import logger
from blender_maxwell.utils import logger
from .. import contracts as ct
from . import base
@ -23,6 +23,9 @@ _MPL_CM = matplotlib.cm.get_cmap('viridis', 512)
VIRIDIS_COLORMAP = jnp.array([_MPL_CM(i)[:3] for i in range(512)])
####################
# - Image Functions
####################
def apply_colormap(normalized_data, colormap):
# Linear interpolation between colormap points
n_colors = colormap.shape[0]
@ -38,8 +41,8 @@ def apply_colormap(normalized_data, colormap):
@jax.jit
def rgba_image_from_xyzf__viridis(xyz_freq):
amplitude = jnp.abs(jnp.squeeze(xyz_freq))
def rgba_image_from_2d_map__viridis(map_2d):
amplitude = jnp.abs(map_2d)
amplitude_normalized = (amplitude - amplitude.min()) / (
amplitude.max() - amplitude.min()
)
@ -49,8 +52,8 @@ def rgba_image_from_xyzf__viridis(xyz_freq):
@jax.jit
def rgba_image_from_xyzf__grayscale(xyz_freq):
amplitude = jnp.abs(jnp.squeeze(xyz_freq))
def rgba_image_from_2d_map__grayscale(map_2d):
amplitude = jnp.abs(map_2d)
amplitude_normalized = (amplitude - amplitude.min()) / (
amplitude.max() - amplitude.min()
)
@ -59,23 +62,24 @@ def rgba_image_from_xyzf__grayscale(xyz_freq):
return jnp.dstack((rgb_array, alpha_channel))
def rgba_image_from_xyzf(xyz_freq, colormap: str | None = None):
"""RGBA Image from Squeezable XYZ-Freq w/fixed freq.
def rgba_image_from_2d_map(map_2d, colormap: str | None = None):
"""RGBA Image from a map of 2D coordinates to values.
Parameters:
xyz_freq: Shape (xlen, ylen, zlen), one dimension has length 1.
width_px: Pixel width to resize the image to.
height: Pixel height to resize the image to.
map_2d: Shape (width, height, value).
Returns:
Image as a JAX array of shape (height, width, 3)
Image as a JAX array of shape (height, width, 4)
"""
if colormap == 'VIRIDIS':
return rgba_image_from_xyzf__viridis(xyz_freq)
return rgba_image_from_2d_map__viridis(map_2d)
if colormap == 'GRAYSCALE':
return rgba_image_from_xyzf__grayscale(xyz_freq)
return rgba_image_from_2d_map__grayscale(map_2d)
####################
# - Managed BL Image
####################
class ManagedBLImage(base.ManagedObj):
managed_obj_type = ct.ManagedObjType.ManagedBLImage
_bl_image_name: str
@ -118,8 +122,8 @@ class ManagedBLImage(base.ManagedObj):
self,
width_px: int,
height_px: int,
color_model: typx.Literal['RGB', 'RGBA'],
dtype: typx.Literal['uint8', 'float32'],
color_model: typ.Literal['RGB', 'RGBA'],
dtype: typ.Literal['uint8', 'float32'],
):
"""Returns the managed blender image.
@ -172,7 +176,7 @@ class ManagedBLImage(base.ManagedObj):
)
####################
# - Actions
# - Methods
####################
def bl_select(self) -> None:
"""Synchronizes the managed object to the preview, by manipulating
@ -227,11 +231,11 @@ class ManagedBLImage(base.ManagedObj):
####################
# - Special Methods
####################
def xyzf_to_image(
self, xyz_freq, colormap: str | None = 'VIRIDIS', bl_select: bool = False
def map_2d_to_image(
self, map_2d, colormap: str | None = 'VIRIDIS', bl_select: bool = False
):
self.data_to_image(
lambda _: rgba_image_from_xyzf(xyz_freq, colormap=colormap),
lambda _: rgba_image_from_2d_map(map_2d, colormap=colormap),
bl_select=bl_select,
)

View File

@ -4,7 +4,8 @@ import bmesh
import bpy
import numpy as np
from ....utils import logger
from blender_maxwell.utils import logger
from .. import contracts as ct
from . import base
from .managed_bl_collection import managed_collection, preview_collection
@ -98,7 +99,7 @@ class ManagedBLMesh(base.ManagedObj):
bpy.data.meshes.remove(bl_object.data)
####################
# - Actions
# - Methods
####################
def show_preview(self) -> None:
"""Moves the managed Blender object to the preview collection.

View File

@ -3,17 +3,15 @@
import typing as typ
import bpy
import typing_extensions as typx
from ....utils import analyze_geonodes, logger
from blender_maxwell.utils import analyze_geonodes, logger
from .. import bl_socket_map
from .. import contracts as ct
from . import base
log = logger.get(__name__)
ModifierType: typ.TypeAlias = typx.Literal['NODES', 'ARRAY']
NodeTreeInterfaceID: typ.TypeAlias = str
UnitSystem: typ.TypeAlias = typ.Any
@ -33,7 +31,7 @@ class ModifierAttrsNODES(typ.TypedDict):
node_group: bpy.types.GeometryNodeTree
unit_system: UnitSystem
inputs: dict[NodeTreeInterfaceID, typ.Any]
inputs: dict[ct.BLNodeTreeInterfaceID, typ.Any]
class ModifierAttrsARRAY(typ.TypedDict):
@ -222,7 +220,7 @@ class ManagedBLModifier(base.ManagedObj):
def bl_modifier(
self,
bl_object: bpy.types.Object,
modifier_type: ModifierType,
modifier_type: ct.BLModifierType,
modifier_attrs: ModifierAttrs,
):
"""Creates a new modifier for the current `bl_object`.

View File

@ -3,7 +3,8 @@ import typing as typ
import bpy
from ...utils import logger
from blender_maxwell.utils import logger
from . import contracts as ct
log = logger.get(__name__)
@ -343,9 +344,11 @@ class MaxwellSimTree(bpy.types.NodeTree):
## The link has already been removed, but we can fix that.
## If NO: Queue re-adding the link (safe since the sockets exist)
## TODO: Crash if deleting removing linked loose sockets.
consent_removal = to_socket.sync_link_removed(from_socket)
consent_removal = to_socket.allow_remove_link(from_socket)
if not consent_removal:
link_corrections['to_add'].append((from_socket, to_socket))
else:
to_socket.on_link_removed(from_socket)
# Ensure Removal of Socket PTRs, PTRs->REFs
self.node_link_cache.remove_sockets_by_link_ptr(link_ptr)
@ -354,12 +357,14 @@ class MaxwellSimTree(bpy.types.NodeTree):
# Retrieve Link Reference
link = self.node_link_cache.link_ptrs_as_links[link_ptr]
# Ask 'to_socket' for Consent to Remove Link
# Ask 'to_socket' for Consent to Add Link
## The link has already been added, but we can fix that.
## If NO: Queue re-adding the link (safe since the sockets exist)
consent_added = link.to_socket.sync_link_added(link)
consent_added = link.to_socket.allow_add_link(link)
if not consent_added:
link_corrections['to_remove'].append(link)
else:
link.to_socket.on_link_added(link)
# Link Corrections
## ADD: Links that 'to_socket' don't want removed.

View File

@ -1,10 +1,12 @@
from . import extract_data, viz
from . import extract_data, math, viz
BL_REGISTER = [
*extract_data.BL_REGISTER,
*viz.BL_REGISTER,
*math.BL_REGISTER,
]
BL_NODES = {
**extract_data.BL_NODES,
**viz.BL_NODES,
**math.BL_NODES,
}

View File

@ -1,19 +1,20 @@
import typing as typ
import bpy
import jax.numpy as jnp
import sympy.physics.units as spu
from blender_maxwell.utils import bl_cache, logger
from .....utils import logger
from ... import contracts as ct
from ... import sockets
from .. import base, events
log = logger.get(__name__)
CACHE_SIM_DATA = {}
class ExtractDataNode(base.MaxwellSimNode):
"""Node for extracting data from other objects."""
"""Node for extracting data from particular objects."""
node_type = ct.NodeType.ExtractData
bl_label = 'Extract'
@ -28,228 +29,196 @@ class ExtractDataNode(base.MaxwellSimNode):
}
####################
# - Properties: Sim Data
# - Properties
####################
sim_data__monitor_name: bpy.props.EnumProperty(
name='Sim Data Monitor Name',
description='Monitor to extract from the attached SimData',
items=lambda self, context: self.search_monitors(context),
update=lambda self, context: self.sync_prop('sim_data__monitor_name', context),
extract_filter: bpy.props.EnumProperty(
name='Extract Filter',
description='Data to extract from the input',
search=lambda self, _, edit_text: self.search_extract_filters(edit_text),
update=lambda self, context: self.on_prop_changed('extract_filter', context),
)
cache__num_monitors: bpy.props.StringProperty(default='')
cache__monitor_names: bpy.props.StringProperty(default='')
cache__monitor_types: bpy.props.StringProperty(default='')
# Sim Data
sim_data_monitor_nametype: dict[str, str] = bl_cache.BLField({})
def search_monitors(self, _: bpy.types.Context) -> list[tuple[str, str, str]]:
"""Search the linked simulation data for monitors."""
# No Linked Sim Data: Return 'None'
if not self.inputs.get('Sim Data') or not self.inputs['Sim Data'].is_linked:
return [('NONE', 'None', 'No monitors')]
# Field Data
field_data_components: set[str] = bl_cache.BLField(set())
# Return Monitor Names
## Special Case for No Monitors
monitor_names = (
self.cache__monitor_names.split(',') if self.cache__monitor_names else []
)
monitor_types = (
self.cache__monitor_types.split(',') if self.cache__monitor_types else []
)
if len(monitor_names) == 0:
return [('NONE', 'None', 'No monitors')]
def search_extract_filters(
self, _: bpy.types.Context
) -> list[tuple[str, str, str]]:
# Sim Data
if self.active_socket_set == 'Sim Data' and self.inputs['Sim Data'].is_linked:
return [
(
monitor_name,
f'{monitor_name}',
f'Monitor "{monitor_name}" ({monitor_type}) recorded by the Sim',
)
for monitor_name, monitor_type in zip(
monitor_names, monitor_types, strict=False
)
for monitor_name, monitor_type in self.sim_data_monitor_nametype.items()
]
def draw_props__sim_data(
self, _: bpy.types.Context, col: bpy.types.UILayout
) -> None:
col.prop(self, 'sim_data__monitor_name', text='')
# Field Data
if self.active_socket_set == 'Field Data' and self.inputs['Sim Data'].is_linked:
return [
([('Ex', 'Ex', 'Ex')] if 'Ex' in self.field_data_components else [])
+ ([('Ey', 'Ey', 'Ey')] if 'Ey' in self.field_data_components else [])
+ ([('Ez', 'Ez', 'Ez')] if 'Ez' in self.field_data_components else [])
+ ([('Hx', 'Hx', 'Hx')] if 'Hx' in self.field_data_components else [])
+ ([('Hy', 'Hy', 'Hy')] if 'Hy' in self.field_data_components else [])
+ ([('Hz', 'Hz', 'Hz')] if 'Hz' in self.field_data_components else [])
]
def draw_info__sim_data(
self, _: bpy.types.Context, col: bpy.types.UILayout
) -> None:
if self.sim_data__monitor_name != 'NONE':
# Flux Data
## Nothing to extract.
# Fallback
return []
####################
# - UI
####################
def draw_props(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None:
col.prop(self, 'extract_filter', text='')
def draw_info(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None:
if self.active_socket_set == 'Sim Data' and self.inputs['Sim Data'].is_linked:
# Header
row = col.row()
row.alignment = 'CENTER'
row.label(text=f'{self.cache__num_monitors} Monitors')
# Monitor Info
if int(self.cache__num_monitors) > 0:
for monitor_name, monitor_type in zip(
self.cache__monitor_names.split(','),
self.cache__monitor_types.split(','),
strict=False,
):
if len(self.sim_data_monitor_nametype) > 0:
for (
monitor_name,
monitor_type,
) in self.sim_data_monitor_nametype.items():
col.label(text=f'{monitor_name}: {monitor_type}')
####################
# - Events: Sim Data
# - Events
####################
@events.on_value_changed(
socket_name='Sim Data',
input_sockets={'Sim Data'},
input_sockets_optional={'Sim Data': True},
)
def on_sim_data_changed(self):
# SimData Cache Hit and SimData Input Unlinked
## Delete Cache Entry
if (
CACHE_SIM_DATA.get(self.instance_id) is not None
and not self.inputs['Sim Data'].is_linked
):
CACHE_SIM_DATA.pop(self.instance_id, None) ## Both member-check
self.cache__num_monitors = ''
self.cache__monitor_names = ''
self.cache__monitor_types = ''
# SimData Cache Miss and Linked SimData
if (
CACHE_SIM_DATA.get(self.instance_id) is None
and self.inputs['Sim Data'].is_linked
):
sim_data = self._compute_input('Sim Data')
## Create Cache Entry
CACHE_SIM_DATA[self.instance_id] = {
'sim_data': sim_data,
'monitor_names': list(sim_data.monitor_data.keys()),
'monitor_types': [
monitor_data.type for monitor_data in sim_data.monitor_data.values()
],
def on_sim_data_changed(self, input_sockets: dict):
if input_sockets['Sim Data'] is not None:
self.sim_data_monitor_nametype = {
monitor_name: monitor_data.type
for monitor_name, monitor_data in input_sockets[
'Sim Data'
].monitor_data.items()
}
cache = CACHE_SIM_DATA[self.instance_id]
self.cache__num_monitors = str(len(cache['monitor_names']))
self.cache__monitor_names = ','.join(cache['monitor_names'])
self.cache__monitor_types = ','.join(cache['monitor_types'])
####################
# - Properties: Field Data
####################
field_data__component: bpy.props.EnumProperty(
name='Field Data Component',
description='Field monitor component to extract from the attached Field Data',
items=lambda self, context: self.search_field_data_components(context),
update=lambda self, context: self.sync_prop('field_data__component', context),
)
cache__components: bpy.props.StringProperty(default='')
def search_field_data_components(
self, _: bpy.types.Context
) -> list[tuple[str, str, str]]:
if not self.inputs.get('Field Data') or not self.inputs['Field Data'].is_linked:
return [('NONE', 'None', 'No data')]
if not self.cache__components:
return [('NONE', 'Loading...', 'Loading data...')]
components = [
tuple(component_str.split(','))
for component_str in self.cache__components.split('|')
]
if len(components) == 0:
return [('NONE', 'None', 'No components')]
return components
def draw_props__field_data(
self, _: bpy.types.Context, col: bpy.types.UILayout
) -> None:
col.prop(self, 'field_data__component', text='')
def draw_info__field_data(
self, _: bpy.types.Context, col: bpy.types.UILayout
) -> None:
pass
####################
# - Events: Field Data
####################
@events.on_value_changed(
socket_name='Field Data',
input_sockets={'Field Data'},
input_sockets_optional={'Field Data': True},
)
def on_field_data_changed(self):
if self.inputs['Field Data'].is_linked and not self.cache__components:
field_data = self._compute_input('Field Data')
components = [
*([('Ex', 'Ex', 'Ex')] if field_data.Ex is not None else []),
*([('Ey', 'Ey', 'Ey')] if field_data.Ey is not None else []),
*([('Ez', 'Ez', 'Ez')] if field_data.Ez is not None else []),
*([('Hx', 'Hx', 'Hx')] if field_data.Hx is not None else []),
*([('Hy', 'Hy', 'Hy')] if field_data.Hy is not None else []),
*([('Hz', 'Hz', 'Hz')] if field_data.Hz is not None else []),
]
self.cache__components = '|'.join(
[','.join(component) for component in components]
def on_field_data_changed(self, input_sockets: dict):
if input_sockets['Field Data'] is not None:
self.field_data_components = (
{'Ex'}
if input_sockets['Field Data'].Ex is not None
else set() | {'Ey'}
if input_sockets['Field Data'].Ey is not None
else set() | {'Ez'}
if input_sockets['Field Data'].Ez is not None
else set() | {'Hx'}
if input_sockets['Field Data'].Hx is not None
else set() | {'Hy'}
if input_sockets['Field Data'].Hy is not None
else set() | {'Hz'}
if input_sockets['Field Data'].Hz is not None
else set()
)
elif not self.inputs['Field Data'].is_linked and self.cache__components:
self.cache__components = ''
####################
# - Flux Data
# - Output: Value
####################
def draw_props__flux_data(
self, _: bpy.types.Context, col: bpy.types.UILayout
) -> None:
pass
def draw_info__flux_data(
self, _: bpy.types.Context, col: bpy.types.UILayout
) -> None:
pass
####################
# - Global
####################
def draw_props(self, context: bpy.types.Context, col: bpy.types.UILayout) -> None:
if self.active_socket_set == 'Sim Data':
self.draw_props__sim_data(context, col)
if self.active_socket_set == 'Field Data':
self.draw_props__field_data(context, col)
if self.active_socket_set == 'Flux Data':
self.draw_props__flux_data(context, col)
def draw_info(self, context: bpy.types.Context, col: bpy.types.UILayout) -> None:
if self.active_socket_set == 'Sim Data':
self.draw_info__sim_data(context, col)
if self.active_socket_set == 'Field Data':
self.draw_info__field_data(context, col)
if self.active_socket_set == 'Flux Data':
self.draw_info__flux_data(context, col)
@events.computes_output_socket(
'Data',
props={'sim_data__monitor_name', 'field_data__component'},
kind=ct.FlowKind.Value,
props={'active_socket_set', 'extract_filter'},
input_sockets={'Sim Data', 'Field Data', 'Flux Data'},
input_sockets_optional={
'Sim Data': True,
'Field Data': True,
'Flux Data': True,
},
)
def compute_extracted_data(self, props: dict):
if self.active_socket_set == 'Sim Data':
if (
CACHE_SIM_DATA.get(self.instance_id) is None
and self.inputs['Sim Data'].is_linked
):
self.on_sim_data_changed()
def compute_extracted_data(self, props: dict, input_sockets: dict):
if props['active_socket_set'] == 'Sim Data':
return input_sockets['Sim Data'].monitor_data[props['extract_filter']]
sim_data = CACHE_SIM_DATA[self.instance_id]['sim_data']
return sim_data.monitor_data[props['sim_data__monitor_name']]
if props['active_socket_set'] == 'Field Data':
return getattr(input_sockets['Field Data'], props['extract_filter'])
elif self.active_socket_set == 'Field Data': # noqa: RET505
field_data = self._compute_input('Field Data')
return getattr(field_data, props['field_data__component'])
if props['active_socket_set'] == 'Flux Data':
return input_sockets['Flux Data']
elif self.active_socket_set == 'Flux Data':
flux_data = self._compute_input('Flux Data')
return flux_data.flux
msg = f'Tried to get a "FlowKind.Value" from socket set {props["active_socket_set"]} in "{self.bl_label}"'
raise RuntimeError(msg)
msg = f'Tried to get data from unknown output socket in "{self.bl_label}"'
####################
# - Output: LazyValueFunc
####################
@events.computes_output_socket(
'Data',
kind=ct.FlowKind.LazyValueFunc,
props={'active_socket_set'},
output_sockets={'Data'},
output_socket_kinds={'Data': ct.FlowKind.Value},
)
def compute_extracted_data_lazy(self, props: dict, output_sockets: dict):
if self.active_socket_set in {'Field Data', 'Flux Data'}:
data = jnp.array(output_sockets['Data'].data)
return ct.LazyValueFuncFlow(func=lambda: data, supports_jax=True)
msg = f'Tried to get a "FlowKind.LazyValueFunc" from socket set {props["active_socket_set"]} in "{self.bl_label}"'
raise RuntimeError(msg)
####################
# - Output: Info
####################
@events.computes_output_socket(
'Data',
kind=ct.FlowKind.Info,
props={'active_socket_set'},
output_sockets={'Data'},
output_socket_kinds={'Data': ct.FlowKind.Value},
)
def compute_extracted_data_info(self, props: dict, output_sockets: dict):
if props['active_socket_set'] == 'Field Data':
xarr = output_sockets['Data']
return ct.InfoFlow(
dim_names=['x', 'y', 'z', 'f'],
dim_idx={
axis: ct.ArrayFlow(values=xarr.get_index(axis).values, unit=spu.um)
for axis in ['x', 'y', 'z']
}
| {
'f': ct.ArrayFlow(
values=xarr.get_index('f').values, unit=spu.hertz
),
},
)
if props['active_socket_set'] == 'Flux Data':
xarr = output_sockets['Data']
return ct.InfoFlow(
dim_names=['f'],
dim_idx={
'f': ct.ArrayFlow(
values=xarr.get_index('f').values, unit=spu.hertz
),
},
)
msg = f'Tried to get a "FlowKind.Info" from socket set {props["active_socket_set"]} in "{self.bl_label}"'
raise RuntimeError(msg)

View File

@ -0,0 +1,14 @@
from . import filter_math, map_math, operate_math, reduce_math
BL_REGISTER = [
*map_math.BL_REGISTER,
*filter_math.BL_REGISTER,
*reduce_math.BL_REGISTER,
*operate_math.BL_REGISTER,
]
BL_NODES = {
**map_math.BL_NODES,
**filter_math.BL_NODES,
**reduce_math.BL_NODES,
**operate_math.BL_NODES,
}

View File

@ -0,0 +1,119 @@
import typing as typ
import bpy
import jax.numpy as jnp
from blender_maxwell.utils import logger
from .... import contracts as ct
from .... import sockets
from ... import base, events
log = logger.get(__name__)
# @functools.partial(jax.jit, static_argnames=('fixed_axis', 'fixed_axis_value'))
# jax.jit
def fix_axis(data, fixed_axis: int, fixed_axis_value: float):
log.critical(data.shape)
# Select Values of Fixed Axis
fixed_axis_values = data[
tuple(slice(None) if i == fixed_axis else 0 for i in range(data.ndim))
]
log.critical(fixed_axis_values)
# Compute Nearest Index on Fixed Axis
idx_of_nearest = jnp.argmin(jnp.abs(fixed_axis_values - fixed_axis_value))
log.critical(idx_of_nearest)
# Select Values along Fixed Axis Value
return jnp.take(data, idx_of_nearest, axis=fixed_axis)
class FilterMathNode(base.MaxwellSimNode):
node_type = ct.NodeType.FilterMath
bl_label = 'Filter Math'
input_sockets: typ.ClassVar = {
'Data': sockets.AnySocketDef(),
}
input_socket_sets: typ.ClassVar = {
'By Axis Value': {
'Axis': sockets.IntegerNumberSocketDef(),
'Value': sockets.RealNumberSocketDef(),
},
'By Axis': {
'Axis': sockets.IntegerNumberSocketDef(),
},
## TODO: bool arrays for comparison/switching/sparse 0-setting/etc. .
}
output_sockets: typ.ClassVar = {
'Data': sockets.AnySocketDef(),
}
####################
# - Properties
####################
operation: bpy.props.EnumProperty(
name='Op',
description='Operation to reduce the input axis with',
items=lambda self, _: self.search_operations(),
update=lambda self, context: self.on_prop_changed('operation', context),
)
def search_operations(self) -> list[tuple[str, str, str]]:
items = []
if self.active_socket_set == 'By Axis Value':
items += [
('FIX', 'Fix Coordinate', '(*, N, *) -> (*, *)'),
]
if self.active_socket_set == 'By Axis':
items += [
('SQUEEZE', 'Squeeze', '(*, 1, *) -> (*, *)'),
]
else:
items += [('NONE', 'None', 'No operations...')]
return items
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
if self.active_socket_set != 'Axis Expr':
layout.prop(self, 'operation')
####################
# - Compute
####################
@events.computes_output_socket(
'Data',
props={'operation', 'active_socket_set'},
input_sockets={'Data', 'Axis', 'Value'},
input_sockets_optional={'Axis': True, 'Value': True},
)
def compute_data(self, props: dict, input_sockets: dict):
if not hasattr(input_sockets['Data'], 'shape'):
msg = 'Input socket "Data" must be an N-D Array (with a "shape" attribute)'
raise ValueError(msg)
# By Axis Value
if props['active_socket_set'] == 'By Axis Value':
if props['operation'] == 'FIX':
return fix_axis(
input_sockets['Data'], input_sockets['Axis'], input_sockets['Value']
)
# By Axis
if props['active_socket_set'] == 'By Axis':
if props['operation'] == 'SQUEEZE':
return jnp.squeeze(input_sockets['Data'], axis=input_sockets['Axis'])
msg = 'Operation invalid'
raise ValueError(msg)
####################
# - Blender Registration
####################
BL_REGISTER = [
FilterMathNode,
]
BL_NODES = {ct.NodeType.FilterMath: (ct.NodeCategory.MAXWELLSIM_ANALYSIS_MATH)}

View File

@ -0,0 +1,169 @@
import typing as typ
import bpy
import jax
import jax.numpy as jnp
import sympy as sp
from blender_maxwell.utils import logger
from .... import contracts as ct
from .... import sockets
from ... import base, events
log = logger.get(__name__)
class MapMathNode(base.MaxwellSimNode):
node_type = ct.NodeType.MapMath
bl_label = 'Map Math'
input_sockets: typ.ClassVar = {
'Data': sockets.AnySocketDef(),
}
input_socket_sets: typ.ClassVar = {
'By Element': {},
'By Vector': {},
'By Matrix': {},
'Expr': {
'Mapper': sockets.ExprSocketDef(
symbols=[sp.Symbol('x')],
default_expr=sp.Symbol('x'),
),
},
}
output_sockets: typ.ClassVar = {
'Data': sockets.AnySocketDef(),
}
####################
# - Properties
####################
operation: bpy.props.EnumProperty(
name='Op',
description='Operation to apply to the input',
items=lambda self, _: self.search_operations(),
update=lambda self, context: self.on_prop_changed('operation', context),
)
def search_operations(self) -> list[tuple[str, str, str]]:
items = []
if self.active_socket_set == 'By Element':
items += [
# General
('REAL', 'real', '(L) (by el)'),
('IMAG', 'imag', 'Im(L) (by el)'),
('ABS', 'abs', '|L| (by el)'),
('SQ', 'square', 'L^2 (by el)'),
('SQRT', 'sqrt', 'sqrt(L) (by el)'),
('INV_SQRT', '1/sqrt', '1/sqrt(L) (by el)'),
# Trigonometry
('COS', 'cos', 'cos(L) (by el)'),
('SIN', 'sin', 'sin(L) (by el)'),
('TAN', 'tan', 'tan(L) (by el)'),
('ACOS', 'acos', 'acos(L) (by el)'),
('ASIN', 'asin', 'asin(L) (by el)'),
('ATAN', 'atan', 'atan(L) (by el)'),
]
elif self.active_socket_set in 'By Vector':
items += [
('NORM_2', '2-Norm', '||L||_2 (by Vec)'),
]
elif self.active_socket_set == 'By Matrix':
items += [
# Matrix -> Number
('DET', 'Determinant', 'det(L) (by Mat)'),
('COND', 'Condition', 'κ(L) (by Mat)'),
('NORM_FRO', 'Frobenius Norm', '||L||_F (by Mat)'),
('RANK', 'Rank', 'rank(L) (by Mat)'),
# Matrix -> Array
('DIAG', 'Diagonal', 'diag(L) (by Mat)'),
('EIG_VALS', 'Eigenvalues', 'eigvals(L) (by Mat)'),
('SVD_VALS', 'SVD', 'svd(L) -> diag(Σ) (by Mat)'),
# Matrix -> Matrix
('INV', 'Invert', 'L^(-1) (by Mat)'),
('TRA', 'Transpose', 'L^T (by Mat)'),
# Matrix -> Matrices
('QR', 'QR', 'L -> Q·R (by Mat)'),
('CHOL', 'Cholesky', 'L -> L·Lh (by Mat)'),
('SVD', 'SVD', 'L -> U·Σ·Vh (by Mat)'),
]
else:
items += ['EXPR_EL', 'Expr (by el)', 'Expression-defined (by el)']
return items
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
if self.active_socket_set not in {'Expr (Element)'}:
layout.prop(self, 'operation')
####################
# - Compute
####################
@events.computes_output_socket(
'Data',
kind=ct.FlowKind.LazyValueFunc,
props={'active_socket_set', 'operation'},
input_sockets={'Data', 'Mapper'},
input_socket_kinds={
'Data': ct.FlowKind.LazyValueFunc,
'Mapper': ct.FlowKind.LazyValueFunc,
},
input_sockets_optional={'Mapper': True},
)
def compute_data(self, props: dict, input_sockets: dict):
mapping_func: typ.Callable[[jax.Array], jax.Array] = {
'By Element': {
'REAL': lambda data: jnp.real(data),
'IMAG': lambda data: jnp.imag(data),
'ABS': lambda data: jnp.abs(data),
'SQ': lambda data: jnp.square(data),
'SQRT': lambda data: jnp.sqrt(data),
'INV_SQRT': lambda data: 1 / jnp.sqrt(data),
'COS': lambda data: jnp.cos(data),
'SIN': lambda data: jnp.sin(data),
'TAN': lambda data: jnp.tan(data),
'ACOS': lambda data: jnp.acos(data),
'ASIN': lambda data: jnp.asin(data),
'ATAN': lambda data: jnp.atan(data),
'SINC': lambda data: jnp.sinc(data),
},
'By Vector': {
'NORM_2': lambda data: jnp.norm(data, ord=2, axis=-1),
},
'By Matrix': {
# Matrix -> Number
'DET': lambda data: jnp.linalg.det(data),
'COND': lambda data: jnp.linalg.cond(data),
'NORM_FRO': lambda data: jnp.linalg.matrix_norm(data, ord='fro'),
'RANK': lambda data: jnp.linalg.matrix_rank(data),
# Matrix -> Vec
'DIAG': lambda data: jnp.diag(data),
'EIG_VALS': lambda data: jnp.eigvals(data),
'SVD_VALS': lambda data: jnp.svdvals(data),
# Matrix -> Matrix
'INV': lambda data: jnp.inv(data),
'TRA': lambda data: jnp.matrix_transpose(data),
# Matrix -> Matrices
'QR': lambda data: jnp.inv(data),
'CHOL': lambda data: jnp.linalg.cholesky(data),
'SVD': lambda data: jnp.linalg.svd(data),
},
'By El (Expr)': {
'EXPR_EL': lambda data: input_sockets['Mapper'](data),
},
}[props['active_socket_set']][props['operation']]
# Compose w/Lazy Root Function Data
return input_sockets['Data'].compose_within(
mapping_func,
supports_jax=True,
)
####################
# - Blender Registration
####################
BL_REGISTER = [
MapMathNode,
]
BL_NODES = {ct.NodeType.MapMath: (ct.NodeCategory.MAXWELLSIM_ANALYSIS_MATH)}

View File

@ -0,0 +1,138 @@
import typing as typ
import bpy
import jax.numpy as jnp
from blender_maxwell.utils import logger
from .... import contracts as ct
from .... import sockets
from ... import base, events
log = logger.get(__name__)
class OperateMathNode(base.MaxwellSimNode):
node_type = ct.NodeType.OperateMath
bl_label = 'Operate Math'
input_socket_sets: typ.ClassVar = {
'Elementwise': {
'Data L': sockets.AnySocketDef(),
'Data R': sockets.AnySocketDef(),
},
## TODO: Filter-array building operations
'Vec-Vec': {
'Data L': sockets.AnySocketDef(),
'Data R': sockets.AnySocketDef(),
},
'Mat-Vec': {
'Data L': sockets.AnySocketDef(),
'Data R': sockets.AnySocketDef(),
},
}
output_sockets: typ.ClassVar = {
'Data': sockets.AnySocketDef(),
}
####################
# - Properties
####################
operation: bpy.props.EnumProperty(
name='Op',
description='Operation to apply to the two inputs',
items=lambda self, _: self.search_operations(),
update=lambda self, context: self.on_prop_changed('operation', context),
)
def search_operations(self) -> list[tuple[str, str, str]]:
items = []
if self.active_socket_set == 'Elementwise':
items = [
('ADD', 'Add', 'L + R (by el)'),
('SUB', 'Subtract', 'L - R (by el)'),
('MUL', 'Multiply', 'L · R (by el)'),
('DIV', 'Divide', 'L ÷ R (by el)'),
('POW', 'Power', 'L^R (by el)'),
('FMOD', 'Trunc Modulo', 'fmod(L,R) (by el)'),
('ATAN2', 'atan2', 'atan2(L,R) (by el)'),
('HEAVISIDE', 'Heaviside', '{0|L<0 1|L>0 R|L=0} (by el)'),
]
elif self.active_socket_set in 'Vec | Vec':
items = [
('DOT', 'Dot', 'L · R'),
('CROSS', 'Cross', 'L x R (by last-axis'),
]
elif self.active_socket_set == 'Mat | Vec':
items = [
('DOT', 'Dot', 'L · R'),
('LIN_SOLVE', 'Lin Solve', 'Lx = R -> x (by last-axis of R)'),
('LSQ_SOLVE', 'LSq Solve', 'Lx = R ~> x (by last-axis of R)'),
]
return items
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
layout.prop(self, 'operation')
####################
# - Properties
####################
@events.computes_output_socket(
'Data',
props={'operation'},
input_sockets={'Data L', 'Data R'},
)
def compute_data(self, props: dict, input_sockets: dict):
if self.active_socket_set == 'Elementwise':
# Element-Wise Arithmetic
if props['operation'] == 'ADD':
return input_sockets['Data L'] + input_sockets['Data R']
if props['operation'] == 'SUB':
return input_sockets['Data L'] - input_sockets['Data R']
if props['operation'] == 'MUL':
return input_sockets['Data L'] * input_sockets['Data R']
if props['operation'] == 'DIV':
return input_sockets['Data L'] / input_sockets['Data R']
# Element-Wise Arithmetic
if props['operation'] == 'POW':
return input_sockets['Data L'] ** input_sockets['Data R']
# Binary Trigonometry
if props['operation'] == 'ATAN2':
return jnp.atan2(input_sockets['Data L'], input_sockets['Data R'])
# Special Functions
if props['operation'] == 'HEAVISIDE':
return jnp.heaviside(input_sockets['Data L'], input_sockets['Data R'])
# Linear Algebra
if self.active_socket_set in {'Vec-Vec', 'Mat-Vec'}:
if props['operation'] == 'DOT':
return jnp.dot(input_sockets['Data L'], input_sockets['Data R'])
elif self.active_socket_set == 'Vec-Vec':
if props['operation'] == 'CROSS':
return jnp.cross(input_sockets['Data L'], input_sockets['Data R'])
elif self.active_socket_set == 'Mat-Vec':
if props['operation'] == 'LIN_SOLVE':
return jnp.linalg.lstsq(
input_sockets['Data L'], input_sockets['Data R']
)
if props['operation'] == 'LSQ_SOLVE':
return jnp.linalg.solve(
input_sockets['Data L'], input_sockets['Data R']
)
msg = 'Invalid operation'
raise ValueError(msg)
####################
# - Blender Registration
####################
BL_REGISTER = [
OperateMathNode,
]
BL_NODES = {ct.NodeType.OperateMath: (ct.NodeCategory.MAXWELLSIM_ANALYSIS_MATH)}

View File

@ -0,0 +1,129 @@
import typing as typ
import bpy
import jax.numpy as jnp
import sympy as sp
from blender_maxwell.utils import logger
from .... import contracts as ct
from .... import sockets
from ... import base, events
log = logger.get(__name__)
class ReduceMathNode(base.MaxwellSimNode):
node_type = ct.NodeType.ReduceMath
bl_label = 'Reduce Math'
input_sockets: typ.ClassVar = {
'Data': sockets.AnySocketDef(),
'Axis': sockets.IntegerNumberSocketDef(),
}
input_socket_sets: typ.ClassVar = {
'By Axis': {
'Axis': sockets.IntegerNumberSocketDef(),
},
'Expr': {
'Reducer': sockets.ExprSocketDef(
symbols=[sp.Symbol('a'), sp.Symbol('b')],
default_expr=sp.Symbol('a') + sp.Symbol('b'),
),
'Axis': sockets.IntegerNumberSocketDef(),
},
}
output_sockets: typ.ClassVar = {
'Data': sockets.AnySocketDef(),
}
####################
# - Properties
####################
operation: bpy.props.EnumProperty(
name='Op',
description='Operation to reduce the input axis with',
items=lambda self, _: self.search_operations(),
update=lambda self, context: self.on_prop_changed('operation', context),
)
def search_operations(self) -> list[tuple[str, str, str]]:
items = []
if self.active_socket_set == 'By Axis':
items += [
# Accumulation
('SUM', 'Sum', 'sum(*, N, *) -> (*, 1, *)'),
('PROD', 'Prod', 'prod(*, N, *) -> (*, 1, *)'),
('MIN', 'Axis-Min', '(*, N, *) -> (*, 1, *)'),
('MAX', 'Axis-Max', '(*, N, *) -> (*, 1, *)'),
('P2P', 'Peak-to-Peak', '(*, N, *) -> (*, 1 *)'),
# Stats
('MEAN', 'Mean', 'mean(*, N, *) -> (*, 1, *)'),
('MEDIAN', 'Median', 'median(*, N, *) -> (*, 1, *)'),
('STDDEV', 'Std Dev', 'stddev(*, N, *) -> (*, 1, *)'),
('VARIANCE', 'Variance', 'var(*, N, *) -> (*, 1, *)'),
# Dimension Reduction
('SQUEEZE', 'Squeeze', '(*, 1, *) -> (*, *)'),
]
else:
items += [('NONE', 'None', 'No operations...')]
return items
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
if self.active_socket_set != 'Axis Expr':
layout.prop(self, 'operation')
####################
# - Compute
####################
@events.computes_output_socket(
'Data',
props={'active_socket_set', 'operation'},
input_sockets={'Data', 'Axis', 'Reducer'},
input_socket_kinds={'Reducer': ct.FlowKind.LazyValueFunc},
input_sockets_optional={'Reducer': True},
)
def compute_data(self, props: dict, input_sockets: dict):
if props['active_socket_set'] == 'By Axis':
# Simple Accumulation
if props['operation'] == 'SUM':
return jnp.sum(input_sockets['Data'], axis=input_sockets['Axis'])
if props['operation'] == 'PROD':
return jnp.prod(input_sockets['Data'], axis=input_sockets['Axis'])
if props['operation'] == 'MIN':
return jnp.min(input_sockets['Data'], axis=input_sockets['Axis'])
if props['operation'] == 'MAX':
return jnp.max(input_sockets['Data'], axis=input_sockets['Axis'])
if props['operation'] == 'P2P':
return jnp.p2p(input_sockets['Data'], axis=input_sockets['Axis'])
# Stats
if props['operation'] == 'MEAN':
return jnp.mean(input_sockets['Data'], axis=input_sockets['Axis'])
if props['operation'] == 'MEDIAN':
return jnp.median(input_sockets['Data'], axis=input_sockets['Axis'])
if props['operation'] == 'STDDEV':
return jnp.std(input_sockets['Data'], axis=input_sockets['Axis'])
if props['operation'] == 'VARIANCE':
return jnp.var(input_sockets['Data'], axis=input_sockets['Axis'])
# Dimension Reduction
if props['operation'] == 'SQUEEZE':
return jnp.squeeze(input_sockets['Data'], axis=input_sockets['Axis'])
if props['active_socket_set'] == 'Expr':
ufunc = jnp.ufunc(input_sockets['Reducer'], nin=2, nout=1)
return ufunc.reduce(input_sockets['Data'], axis=input_sockets['Axis'])
msg = 'Operation invalid'
raise ValueError(msg)
####################
# - Blender Registration
####################
BL_REGISTER = [
ReduceMathNode,
]
BL_NODES = {ct.NodeType.ReduceMath: (ct.NodeCategory.MAXWELLSIM_ANALYSIS_MATH)}

View File

@ -1,9 +1,9 @@
import typing as typ
import bpy
import jax.numpy as jnp
from .....utils import logger
from blender_maxwell.utils import logger
from ... import contracts as ct
from ... import managed_objs, sockets
from .. import base, events
@ -20,7 +20,7 @@ class VizNode(base.MaxwellSimNode):
####################
# - Sockets
####################
input_sockets = {
input_sockets: typ.ClassVar = {
'Data': sockets.AnySocketDef(),
'Freq': sockets.PhysicalFreqSocketDef(),
}
@ -43,7 +43,7 @@ class VizNode(base.MaxwellSimNode):
('GRAYSCALE', 'Grayscale', 'Barebones'),
],
default='VIRIDIS',
update=lambda self, context: self.sync_prop('colormap', context),
update=lambda self, context: self.on_prop_changed('colormap', context),
)
#####################
@ -72,20 +72,12 @@ class VizNode(base.MaxwellSimNode):
props: dict,
unit_systems: dict,
):
selected_data = jnp.array(
input_sockets['Data'].sel(f=input_sockets['Freq'], method='nearest')
)
managed_objs['plot'].xyzf_to_image(
selected_data,
managed_objs['plot'].map_2d_to_image(
input_sockets['Data'].as_bound_jax_func(),
colormap=props['colormap'],
bl_select=True,
)
# @events.on_init()
# def on_init(self):
# self.on_changed_inputs()
####################
# - Blender Registration

View File

@ -10,13 +10,12 @@ from types import MappingProxyType
import bpy
import sympy as sp
import typing_extensions as typx
from blender_maxwell.utils import logger
from blender_maxwell.utils import bl_cache, logger
from .. import bl_cache, sockets
from .. import contracts as ct
from .. import managed_objs as _managed_objs
from .. import sockets
from . import events
from . import presets as _presets
@ -102,12 +101,12 @@ class MaxwellSimNode(bpy.types.Node):
Parameters:
name: The name of the property to set.
prop: The `bpy.types.Property` to instantiate and attach..
no_update: Don't attach a `self.sync_prop()` callback to the property's `update`.
no_update: Don't attach a `self.on_prop_changed()` callback to the property's `update`.
"""
_update_with_name = prop_name if update_with_name is None else update_with_name
extra_kwargs = (
{
'update': lambda self, context: self.sync_prop(
'update': lambda self, context: self.on_prop_changed(
_update_with_name, context
),
}
@ -121,28 +120,26 @@ class MaxwellSimNode(bpy.types.Node):
@classmethod
def _gather_event_methods(cls) -> dict[str, typ.Callable[[], None]]:
"""Gathers all methods called in response to actions/events observed by the node.
"""Gathers all methods called in response to events observed by the node.
Notes:
- 'Event methods' must have an attribute 'action_type' in order to be picked up.
- 'Event methods' must have an attribute 'action_type'.
- 'Event methods' must have an attribute 'event' in order to be picked up.
- 'Event methods' must have an attribute 'event'.
Returns:
Event methods, indexed by the action that (maybe) triggers them.
Event methods, indexed by the event that (maybe) triggers them.
"""
event_methods = [
method
for attr_name in dir(cls)
if hasattr(method := getattr(cls, attr_name), 'action_type')
and method.action_type in set(ct.DataFlowAction)
if hasattr(method := getattr(cls, attr_name), 'event')
and method.event in set(ct.FlowEvent)
]
event_methods_by_action = {
action_type: [] for action_type in set(ct.DataFlowAction)
}
event_methods_by_event = {event: [] for event in set(ct.FlowEvent)}
for method in event_methods:
event_methods_by_action[method.action_type].append(method)
event_methods_by_event[method.event].append(method)
return event_methods_by_action
return event_methods_by_event
@classmethod
def socket_set_names(cls) -> list[str]:
@ -185,7 +182,7 @@ class MaxwellSimNode(bpy.types.Node):
cls.set_prop('locked', bpy.props.BoolProperty, no_update=True, default=False)
## Event Method Callbacks
cls.event_methods_by_action = cls._gather_event_methods()
cls.event_methods_by_event = cls._gather_event_methods()
## Active Socket Set
if len(cls.input_socket_sets) + len(cls.output_socket_sets) > 0:
@ -194,7 +191,6 @@ class MaxwellSimNode(bpy.types.Node):
'active_socket_set',
bpy.props.EnumProperty,
name='Active Socket Set',
description='Selector of active sockets',
items=[
(socket_set_name, socket_set_name, socket_set_name)
for socket_set_name in socket_set_names
@ -273,7 +269,7 @@ class MaxwellSimNode(bpy.types.Node):
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)
## TODO: Account for DataFlowKind
## TODO: Account for FlowKind
bl_socket.value = socket_value
@events.on_show_preview()
@ -319,7 +315,7 @@ class MaxwellSimNode(bpy.types.Node):
# - Socket Accessors
####################
def _bl_sockets(
self, direc: typx.Literal['input', 'output']
self, direc: typ.Literal['input', 'output']
) -> bpy.types.NodeInputs:
"""Retrieve currently visible Blender sockets on the node, by-direction.
@ -338,7 +334,7 @@ class MaxwellSimNode(bpy.types.Node):
def _active_socket_set_socket_defs(
self,
direc: typx.Literal['input', 'output'],
direc: typ.Literal['input', 'output'],
) -> dict[ct.SocketName, sockets.base.SocketDef]:
"""Retrieve all socket definitions for sockets that should be defined, according to the `self.active_socket_set`.
@ -364,7 +360,7 @@ class MaxwellSimNode(bpy.types.Node):
return socket_sets.get(self.active_socket_set, {})
def active_socket_defs(
self, direc: typx.Literal['input', 'output']
self, direc: typ.Literal['input', 'output']
) -> dict[ct.SocketName, sockets.base.SocketDef]:
"""Retrieve all socket definitions for sockets that should be defined.
@ -483,25 +479,22 @@ class MaxwellSimNode(bpy.types.Node):
# - Event Methods
####################
@property
def _event_method_filter_by_action(self) -> dict[ct.DataFlowAction, typ.Callable]:
"""Compute a map of DataFlowActions, to a function that filters its event methods.
def _event_method_filter_by_event(self) -> dict[ct.FlowEvent, typ.Callable]:
"""Compute a map of FlowEvents, to a function that filters its event methods.
The returned filter functions are hard-coded, and must always return a `bool`.
They may use attributes of `self`, always return `True` or `False`, or something different.
Notes:
This is an internal method; you probably want `self.filtered_event_methods_by_action`.
This is an internal method; you probably want `self.filtered_event_methods_by_event`.
Returns:
The map of `ct.DataFlowAction` to a function that can determine whether any `event_method` should be run.
The map of `ct.FlowEvent` to a function that can determine whether any `event_method` should be run.
"""
return {
ct.DataFlowAction.EnableLock: lambda *_: True,
ct.DataFlowAction.DisableLock: lambda *_: True,
ct.DataFlowAction.DataChanged: lambda event_method,
socket_name,
prop_name,
_: (
ct.FlowEvent.EnableLock: lambda *_: True,
ct.FlowEvent.DisableLock: lambda *_: True,
ct.FlowEvent.DataChanged: lambda event_method, socket_name, prop_name, _: (
(
socket_name
and socket_name in event_method.callback_info.on_changed_sockets
@ -516,7 +509,7 @@ class MaxwellSimNode(bpy.types.Node):
and socket_name in self.loose_input_sockets
)
),
ct.DataFlowAction.OutputRequested: lambda output_socket_method,
ct.FlowEvent.OutputRequested: lambda output_socket_method,
output_socket_name,
_,
kind: (
@ -526,26 +519,26 @@ class MaxwellSimNode(bpy.types.Node):
== output_socket_method.callback_info.output_socket_name
)
),
ct.DataFlowAction.ShowPreview: lambda *_: True,
ct.DataFlowAction.ShowPlot: lambda *_: True,
ct.FlowEvent.ShowPreview: lambda *_: True,
ct.FlowEvent.ShowPlot: lambda *_: True,
}
def filtered_event_methods_by_action(
def filtered_event_methods_by_event(
self,
action: ct.DataFlowAction,
event: ct.FlowEvent,
_filter: tuple[ct.SocketName, str],
) -> list[typ.Callable]:
"""Return all event methods that should run, given the context provided by `_filter`.
The inclusion decision is made by the internal property `self._event_method_filter_by_action`.
The inclusion decision is made by the internal property `self._event_method_filter_by_event`.
Returns:
All `event_method`s that should run, as callable objects (they can be run using `event_method(self)`).
"""
return [
event_method
for event_method in self.event_methods_by_action[action]
if self._event_method_filter_by_action[action](event_method, *_filter)
for event_method in self.event_methods_by_event[event]
if self._event_method_filter_by_event[event](event_method, *_filter)
]
####################
@ -558,7 +551,7 @@ class MaxwellSimNode(bpy.types.Node):
def _compute_input(
self,
input_socket_name: ct.SocketName,
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
kind: ct.FlowKind = ct.FlowKind.Value,
unit_system: dict[ct.SocketType, sp.Expr] | None = None,
optional: bool = False,
) -> typ.Any:
@ -574,7 +567,7 @@ class MaxwellSimNode(bpy.types.Node):
"""
if (bl_socket := self.inputs.get(input_socket_name)) is not None:
return (
ct.DataFlowKind.scale_to_unit_system(
ct.FlowKind.scale_to_unit_system(
kind,
bl_socket.compute_data(kind=kind),
bl_socket.socket_type,
@ -591,7 +584,7 @@ class MaxwellSimNode(bpy.types.Node):
raise ValueError(msg)
####################
# - Compute Action: Output Socket
# - Compute Event: Output Socket
####################
@bl_cache.keyed_cache(
exclude={'self', 'optional'},
@ -599,14 +592,14 @@ class MaxwellSimNode(bpy.types.Node):
def compute_output(
self,
output_socket_name: ct.SocketName,
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
kind: ct.FlowKind = ct.FlowKind.Value,
optional: bool = False,
) -> typ.Any:
"""Computes the value of an output socket.
Parameters:
output_socket_name: The name declaring the output socket, for which this method computes the output.
kind: The DataFlowKind to use when computing the output socket value.
kind: The FlowKind to use when computing the output socket value.
Returns:
The value of the output socket, as computed by the dedicated method
@ -619,8 +612,8 @@ class MaxwellSimNode(bpy.types.Node):
msg = f"Can't compute nonexistent output socket name {output_socket_name}, as it's not currently active"
raise RuntimeError(msg)
output_socket_methods = self.filtered_event_methods_by_action(
ct.DataFlowAction.OutputRequested,
output_socket_methods = self.filtered_event_methods_by_event(
ct.FlowEvent.OutputRequested,
(output_socket_name, None, kind),
)
@ -636,7 +629,7 @@ class MaxwellSimNode(bpy.types.Node):
raise ValueError(msg)
####################
# - Action Trigger
# - Event Trigger
####################
def _should_recompute_output_socket(
self,
@ -657,25 +650,28 @@ class MaxwellSimNode(bpy.types.Node):
)
)
def trigger_action(
def trigger_event(
self,
action: ct.DataFlowAction,
event: ct.FlowEvent,
socket_name: ct.SocketName | None = None,
prop_name: ct.SocketName | None = None,
) -> None:
"""Recursively triggers actions/events forwards or backwards along the node tree, allowing nodes in the update path to react.
"""Recursively triggers events forwards or backwards along the node tree, allowing nodes in the update path to react.
Use `events` decorators to define methods that react to particular `ct.DataFlowAction`s.
Use `events` decorators to define methods that react to particular `ct.FlowEvent`s.
Notes:
This can be an unpredictably heavy function, depending on the node graph topology.
Doesn't currently accept `LinkChanged` (->Output) events; rather, these propagate as `DataChanged` events.
**This may change** if it becomes important for the node to differentiate between "change in data" and "change in link".
Parameters:
action: The action/event to report forwards/backwards along the node tree.
event: The event to report forwards/backwards along the node tree.
socket_name: The input socket that was altered, if any, in order to trigger this event.
pop_name: The property that was altered, if any, in order to trigger this event.
"""
if action == ct.DataFlowAction.DataChanged:
if event == ct.FlowEvent.DataChanged:
input_socket_name = socket_name ## Trigger direction is forwards
# Invalidate Input Socket Cache
@ -687,8 +683,8 @@ class MaxwellSimNode(bpy.types.Node):
)
# Invalidate Output Socket Cache
for output_socket_method in self.event_methods_by_action[
ct.DataFlowAction.OutputRequested
for output_socket_method in self.event_methods_by_event[
ct.FlowEvent.OutputRequested
]:
method_info = output_socket_method.callback_info
if self._should_recompute_output_socket(
@ -701,26 +697,26 @@ class MaxwellSimNode(bpy.types.Node):
# Run Triggered Event Methods
stop_propagation = False
triggered_event_methods = self.filtered_event_methods_by_action(
action, (socket_name, prop_name, None)
triggered_event_methods = self.filtered_event_methods_by_event(
event, (socket_name, prop_name, None)
)
for event_method in triggered_event_methods:
stop_propagation |= event_method.stop_propagation
event_method(self)
# Propagate Action to All Sockets in "Trigger Direction"
# Propagate Event to All Sockets in "Trigger Direction"
## The trigger chain goes node/socket/node/socket/...
if not stop_propagation:
triggered_sockets = self._bl_sockets(
direc=ct.DataFlowAction.trigger_direction(action)
direc=ct.FlowEvent.flow_direction[event]
)
for bl_socket in triggered_sockets:
bl_socket.trigger_action(action)
bl_socket.trigger_event(event)
####################
# - Property Action: On Update
# - Property Event: On Update
####################
def sync_prop(self, prop_name: str, _: bpy.types.Context) -> None:
def on_prop_changed(self, prop_name: str, _: bpy.types.Context) -> None:
"""Report that a particular property has changed, which may cause certain caches to regenerate.
Notes:
@ -732,7 +728,7 @@ class MaxwellSimNode(bpy.types.Node):
prop_name: The name of the property that changed.
"""
if hasattr(self, prop_name):
self.trigger_action(ct.DataFlowAction.DataChanged, prop_name=prop_name)
self.trigger_event(ct.FlowEvent.DataChanged, prop_name=prop_name)
else:
msg = f'Property {prop_name} not defined on node {self}'
raise RuntimeError(msg)
@ -747,13 +743,13 @@ class MaxwellSimNode(bpy.types.Node):
) -> None:
"""Draws the UI of the node.
- Locked (`self.locked`): The UI will be unusable.
- Active Preset (`self.active_preset`): The preset selector will display.
- Active Socket Set (`self.active_socket_set`): The socket set selector will display.
- Use Sim Node Name (`self.use_sim_node_name`): The "Sim Node Name will display.
- Properties (`self.draw_props()`): Node properties will display.
- Operators (`self.draw_operators()`): Node operators will display.
- Info (`self.draw_operators()`): Node information will display.
- **Locked** (`self.locked`): The UI will be unusable.
- **Active Preset** (`self.active_preset`): The preset selector will display.
- **Active Socket Set** (`self.active_socket_set`): The socket set selector will display.
- **Use Sim Node Name** (`self.use_sim_node_name`): The `self.sim_node_name` will display.
- **Properties**: Node properties will display, if `self.draw_props()` is overridden.
- **Operators**: Node operators will display, if `self.draw_operators()` is overridden.
- **Info**: Node information will display, if `self.draw_info()` is overridden.
Parameters:
context: The current Blender context.
@ -864,9 +860,7 @@ class MaxwellSimNode(bpy.types.Node):
## -> Compromise: Users explicitly say 'run_on_init' in @on_value_changed
for event_method in [
event_method
for event_method in self.event_methods_by_action[
ct.DataFlowAction.DataChanged
]
for event_method in self.event_methods_by_event[ct.FlowEvent.DataChanged]
if event_method.callback_info.run_on_init
]:
event_method(self)
@ -915,7 +909,7 @@ class MaxwellSimNode(bpy.types.Node):
bl_socket.is_linked and bl_socket.locked
for bl_socket in self.inputs.values()
):
self.trigger_action(ct.DataFlowAction.DisableLock)
self.trigger_event(ct.FlowEvent.DisableLock)
# Free Managed Objects
for managed_obj in self.managed_objs.values():

View File

@ -3,7 +3,8 @@ import inspect
import typing as typ
from types import MappingProxyType
from ....utils import logger
from blender_maxwell.utils import logger
from .. import contracts as ct
log = logger.get(__name__)
@ -26,16 +27,16 @@ class InfoDataChanged:
@dataclasses.dataclass(kw_only=True, frozen=True)
class InfoOutputRequested:
output_socket_name: ct.SocketName
kind: ct.DataFlowKind
kind: ct.FlowKind
depon_props: set[str]
depon_input_sockets: set[ct.SocketName]
depon_input_socket_kinds: dict[ct.SocketName, ct.DataFlowKind]
depon_input_socket_kinds: dict[ct.SocketName, ct.FlowKind]
depon_all_loose_input_sockets: bool
depon_output_sockets: set[ct.SocketName]
depon_output_socket_kinds: dict[ct.SocketName, ct.DataFlowKind]
depon_output_socket_kinds: dict[ct.SocketName, ct.FlowKind]
depon_all_loose_output_sockets: bool
@ -50,7 +51,7 @@ PropName: typ.TypeAlias = str
def event_decorator(
action_type: ct.DataFlowAction,
event: ct.FlowEvent,
callback_info: EventCallbackInfo | None,
stop_propagation: bool = False,
# Request Data for Callback
@ -58,10 +59,10 @@ def event_decorator(
props: set[PropName] = frozenset(),
input_sockets: set[ct.SocketName] = frozenset(),
input_sockets_optional: dict[ct.SocketName, bool] = MappingProxyType({}),
input_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}),
input_socket_kinds: dict[ct.SocketName, ct.FlowKind] = MappingProxyType({}),
output_sockets: set[ct.SocketName] = frozenset(),
output_sockets_optional: dict[ct.SocketName, bool] = MappingProxyType({}),
output_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}),
output_socket_kinds: dict[ct.SocketName, ct.FlowKind] = MappingProxyType({}),
all_loose_input_sockets: bool = False,
all_loose_output_sockets: bool = False,
# Request Unit System Scaling
@ -72,17 +73,17 @@ def event_decorator(
"""Returns a decorator for a method of `MaxwellSimNode`, declaring it as able respond to events passing through a node.
Parameters:
action_type: A name describing which event the decorator should respond to.
Set to `return_method.action_type`
callback_info: A dictionary that provides the caller with additional per-`action_type` information.
This might include parameters to help select the most appropriate method(s) to respond to an event with, or actions to take after running the callback.
event: A name describing which event the decorator should respond to.
Set to `return_method.event`
callback_info: A dictionary that provides the caller with additional per-`event` information.
This might include parameters to help select the most appropriate method(s) to respond to an event with, or events to take after running the callback.
props: Set of `props` to compute, then pass to the decorated method.
stop_propagation: Whether or stop propagating the event through the graph after encountering this method.
Other methods defined on the same node will still run.
managed_objs: Set of `managed_objs` to retrieve, then pass to the decorated method.
input_sockets: Set of `input_sockets` to compute, then pass to the decorated method.
input_socket_kinds: The `ct.DataFlowKind` to compute per-input-socket.
If an input socket isn't specified, it defaults to `ct.DataFlowKind.Value`.
input_socket_kinds: The `ct.FlowKind` to compute per-input-socket.
If an input socket isn't specified, it defaults to `ct.FlowKind.Value`.
output_sockets: Set of `output_sockets` to compute, then pass to the decorated method.
all_loose_input_sockets: Whether to compute all loose input sockets and pass them to the decorated method.
Used when the names of the loose input sockets are unknown, but all of their values are needed.
@ -93,7 +94,7 @@ def event_decorator(
A decorator, which can be applied to a method of `MaxwellSimNode`.
When a `MaxwellSimNode` subclass initializes, such a decorated method will be picked up on.
When the `action_type` action passes through the node, then `callback_info` is used to determine
When `event` passes through the node, then `callback_info` is used to determine
"""
req_params = (
{'self'}
@ -157,7 +158,7 @@ def event_decorator(
input_socket_name: node._compute_input(
input_socket_name,
kind=input_socket_kinds.get(
input_socket_name, ct.DataFlowKind.Value
input_socket_name, ct.FlowKind.Value
),
unit_system=(
unit_system := unit_systems.get(
@ -179,10 +180,10 @@ def event_decorator(
method_kw_args |= (
{
'output_sockets': {
output_socket_name: ct.DataFlowKind.scale_to_unit_system(
output_socket_name: ct.FlowKind.scale_to_unit_system(
(
output_socket_kind := output_socket_kinds.get(
output_socket_name, ct.DataFlowKind.Value
output_socket_name, ct.FlowKind.Value
)
),
node.compute_output(
@ -201,7 +202,7 @@ def event_decorator(
else node.compute_output(
output_socket_name,
kind=output_socket_kinds.get(
output_socket_name, ct.DataFlowKind.Value
output_socket_name, ct.FlowKind.Value
),
optional=output_sockets_optional.get(
output_socket_name, False
@ -252,14 +253,14 @@ def event_decorator(
)
# Set Decorated Attributes and Return
## Fix Introspection + Documentation
## TODO: Fix Introspection + Documentation
# decorated.__name__ = method.__name__
# decorated.__module__ = method.__module__
# decorated.__qualname__ = method.__qualname__
# decorated.__doc__ = method.__doc__
decorated.__doc__ = method.__doc__
## Add Spice
decorated.action_type = action_type
decorated.event = event
decorated.callback_info = callback_info
decorated.stop_propagation = stop_propagation
@ -275,7 +276,7 @@ def on_enable_lock(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.EnableLock,
event=ct.FlowEvent.EnableLock,
callback_info=None,
**kwargs,
)
@ -285,7 +286,7 @@ def on_disable_lock(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.DisableLock,
event=ct.FlowEvent.DisableLock,
callback_info=None,
**kwargs,
)
@ -300,7 +301,7 @@ def on_value_changed(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.DataChanged,
event=ct.FlowEvent.DataChanged,
callback_info=InfoDataChanged(
run_on_init=run_on_init,
on_changed_sockets=(
@ -316,11 +317,11 @@ def on_value_changed(
## TODO: Change name to 'on_output_requested'
def computes_output_socket(
output_socket_name: ct.SocketName | None,
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
kind: ct.FlowKind = ct.FlowKind.Value,
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.OutputRequested,
event=ct.FlowEvent.OutputRequested,
callback_info=InfoOutputRequested(
output_socket_name=output_socket_name,
kind=kind,
@ -342,7 +343,7 @@ def on_show_preview(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.ShowPreview,
event=ct.FlowEvent.ShowPreview,
callback_info={},
**kwargs,
)
@ -353,7 +354,7 @@ def on_show_plot(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.ShowPlot,
event=ct.FlowEvent.ShowPlot,
callback_info={},
stop_propagation=stop_propagation,
**kwargs,

View File

@ -1,14 +1,16 @@
# from . import scientific_constant
# from . import physical_constant
from . import blender_constant, number_constant, scientific_constant
from . import blender_constant, expr_constant, number_constant, scientific_constant
BL_REGISTER = [
*expr_constant.BL_REGISTER,
*scientific_constant.BL_REGISTER,
*number_constant.BL_REGISTER,
# *physical_constant.BL_REGISTER,
*blender_constant.BL_REGISTER,
]
BL_NODES = {
**expr_constant.BL_NODES,
**scientific_constant.BL_NODES,
**number_constant.BL_NODES,
# **physical_constant.BL_NODES,

View File

@ -0,0 +1,41 @@
import typing as typ
from .... import contracts as ct
from .... import sockets
from ... import base, events
class ExprConstantNode(base.MaxwellSimNode):
node_type = ct.NodeType.ExprConstant
bl_label = 'Expr Constant'
input_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(),
}
output_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(),
}
## TODO: Symbols (defined w/props?)
## - Currently expr constant isn't excessively useful, since there are no variables.
## - We'll define the #, type, name with props.
## - We'll add loose-socket inputs as int/real/complex/physical socket (based on type) for Param.
## - We the output expr would support `Value` (just the expression), `LazyValueFunc` (evaluate w/symbol support), `Param` (example values for symbols).
####################
# - Callbacks
####################
@events.computes_output_socket(
'Expr', kind=ct.FlowKind.Value, input_sockets={'Expr'}
)
def compute_value(self, input_sockets: dict) -> typ.Any:
return input_sockets['Expr']
####################
# - Blender Registration
####################
BL_REGISTER = [
ExprConstantNode,
]
BL_NODES = {ct.NodeType.ExprConstant: (ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS)}

View File

@ -2,7 +2,8 @@ import typing as typ
import bpy
from ......utils import sci_constants as constants
from blender_maxwell.utils import sci_constants as constants
from .... import contracts as ct
from .... import sockets
from ... import base, events
@ -55,7 +56,7 @@ class ScientificConstantNode(base.MaxwellSimNode):
self.cache__units = ''
self.cache__uncertainty = ''
self.sync_prop('sci_constant', context)
self.on_prop_changed('sci_constant', context)
####################
# - UI

View File

@ -5,7 +5,8 @@ import bpy
import tidy3d as td
import tidy3d.plugins.dispersion as td_dispersion
from ......utils import logger
from blender_maxwell.utils import logger
from .... import contracts as ct
from .... import managed_objs, sockets
from ... import base, events
@ -73,7 +74,7 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode):
),
],
default='SIMULATION_DATA',
update=lambda self, context: self.sync_prop('tidy3d_type', context),
update=lambda self, context: self.on_prop_changed('tidy3d_type', context),
)
disp_fit__min_poles: bpy.props.IntProperty(

View File

@ -4,9 +4,10 @@ import bpy
import sympy as sp
import sympy.physics.units as spu
from .....utils import extra_sympy_units as spux
from .....utils import logger
from .....utils import sci_constants as constants
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from blender_maxwell.utils import sci_constants as constants
from ... import contracts as ct
from ... import sockets
from .. import base, events
@ -27,7 +28,7 @@ class WaveConstantNode(base.MaxwellSimNode):
name='Range',
description='Whether to use a wavelength/frequency range',
default=False,
update=lambda self, context: self.sync_prop('use_range', context),
update=lambda self, context: self.on_prop_changed('use_range', context),
)
def draw_props(self, _: bpy.types.Context, col: bpy.types.UILayout):
@ -38,7 +39,7 @@ class WaveConstantNode(base.MaxwellSimNode):
####################
@events.computes_output_socket(
'WL',
kind=ct.DataFlowKind.Value,
kind=ct.FlowKind.Value,
# Data
input_sockets={'WL', 'Freq'},
input_sockets_optional={'WL': True, 'Freq': True},
@ -55,7 +56,7 @@ class WaveConstantNode(base.MaxwellSimNode):
@events.computes_output_socket(
'Freq',
kind=ct.DataFlowKind.Value,
kind=ct.FlowKind.Value,
# Data
input_sockets={'WL', 'Freq'},
input_sockets_optional={'WL': True, 'Freq': True},
@ -73,7 +74,7 @@ class WaveConstantNode(base.MaxwellSimNode):
@events.computes_output_socket(
'WL',
kind=ct.DataFlowKind.LazyValueRange,
kind=ct.FlowKind.LazyArrayRange,
# Data
input_sockets={'WL', 'Freq'},
input_sockets_optional={'WL': True, 'Freq': True},
@ -92,12 +93,12 @@ class WaveConstantNode(base.MaxwellSimNode):
@events.computes_output_socket(
'Freq',
kind=ct.DataFlowKind.LazyValueRange,
kind=ct.FlowKind.LazyArrayRange,
# Data
input_sockets={'WL', 'Freq'},
input_socket_kinds={
'WL': ct.DataFlowKind.LazyValueRange,
'Freq': ct.DataFlowKind.LazyValueRange,
'WL': ct.FlowKind.LazyArrayRange,
'Freq': ct.FlowKind.LazyArrayRange,
},
input_sockets_optional={'WL': True, 'Freq': True},
)

View File

@ -1,9 +1,9 @@
import typing as typ
from pathlib import Path
from ...... import info
from blender_maxwell.utils import logger
from ......services import tdcloud
from ......utils import logger
from .... import contracts as ct
from .... import sockets
from ... import base, events
@ -17,8 +17,8 @@ def _sim_data_cache_path(task_id: str) -> Path:
Arguments:
task_id: The ID of the Tidy3D cloud task.
"""
(info.ADDON_CACHE / task_id).mkdir(exist_ok=True)
return info.ADDON_CACHE / task_id / 'sim_data.hdf5'
(ct.addon.ADDON_CACHE / task_id).mkdir(exist_ok=True)
return ct.addon.ADDON_CACHE / task_id / 'sim_data.hdf5'
####################

View File

@ -1,4 +1,3 @@
import typing as typ
import bpy
import scipy as sc
@ -6,7 +5,8 @@ import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
from .....utils import extra_sympy_units as spuex
from blender_maxwell.utils import extra_sympy_units as spuex
from ... import contracts as ct
from ... import managed_objs, sockets
from .. import base, events
@ -54,7 +54,7 @@ class LibraryMediumNode(base.MaxwellSimNode):
if mat_key != 'graphene' ## For some reason, it's unique...
],
default='Au',
update=(lambda self, context: self.sync_prop('material', context)),
update=(lambda self, context: self.on_prop_changed('material', context)),
)
@property

View File

@ -3,9 +3,10 @@ import typing as typ
import sympy as sp
import tidy3d as td
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from .....assets.import_geonodes import GeoNodes, import_geonodes
from .....utils import extra_sympy_units as spux
from .....utils import logger
from ... import contracts as ct
from ... import managed_objs, sockets
from .. import base, events
@ -67,7 +68,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
'Freqs',
},
input_socket_kinds={
'Freqs': ct.DataFlowKind.LazyValueRange,
'Freqs': ct.FlowKind.LazyArrayRange,
},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={

View File

@ -3,9 +3,10 @@ import typing as typ
import sympy as sp
import tidy3d as td
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from .....assets.import_geonodes import GeoNodes, import_geonodes
from .....utils import extra_sympy_units as spux
from .....utils import logger
from ... import contracts as ct
from ... import managed_objs, sockets
from .. import base, events
@ -67,7 +68,7 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
'Direction',
},
input_socket_kinds={
'Freqs': ct.DataFlowKind.LazyValueRange,
'Freqs': ct.FlowKind.LazyArrayRange,
},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={

View File

@ -3,7 +3,8 @@ import typing as typ
import bpy
import sympy as sp
from .....utils import logger
from blender_maxwell.utils import logger
from ... import contracts as ct
from ... import sockets
from .. import base, events
@ -59,14 +60,14 @@ class ViewerNode(base.MaxwellSimNode):
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),
update=lambda self, context: self.on_prop_changed('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=True,
update=lambda self, context: self.sync_prop('auto_3d_preview', context),
update=lambda self, context: self.on_prop_changed('auto_3d_preview', context),
)
####################
@ -124,7 +125,7 @@ class ViewerNode(base.MaxwellSimNode):
)
def on_changed_plot_preview(self, props):
if self.inputs['Data'].is_linked and props['auto_plot']:
self.trigger_action(ct.DataFlowAction.ShowPlot)
self.trigger_event(ct.FlowEvent.ShowPlot)
@events.on_value_changed(
socket_name='Data',
@ -137,7 +138,7 @@ class ViewerNode(base.MaxwellSimNode):
# Remove Non-Repreviewed Previews on Close
with node_tree.repreview_all():
if self.inputs['Data'].is_linked and props['auto_3d_preview']:
self.trigger_action(ct.DataFlowAction.ShowPreview)
self.trigger_event(ct.FlowEvent.ShowPreview)
####################

View File

@ -180,7 +180,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
####################
def sync_lock_tree(self, context):
if self.lock_tree:
self.trigger_action(ct.DataFlowAction.EnableLock)
self.trigger_event(ct.FlowEvent.EnableLock)
self.locked = False
for bl_socket in self.inputs:
if bl_socket.name == 'FDTD Sim':
@ -188,9 +188,9 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
bl_socket.locked = False
else:
self.trigger_action(ct.DataFlowAction.DisableLock)
self.trigger_event(ct.FlowEvent.DisableLock)
self.sync_prop('lock_tree', context)
self.on_prop_changed('lock_tree', context)
def sync_tracked_task_id(self, context):
# Select Tracked Task
@ -212,7 +212,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
self.inputs['Cloud Task'].sync_prepare_new_task()
self.inputs['Cloud Task'].locked = False
self.sync_prop('tracked_task_id', context)
self.on_prop_changed('tracked_task_id', context)
####################
# - Output Socket Callbacks

View File

@ -35,7 +35,7 @@ class FDTDSimNode(base.MaxwellSimNode):
####################
@events.computes_output_socket(
'FDTD Sim',
kind=ct.DataFlowKind.Value,
kind=ct.FlowKind.Value,
input_sockets={'Sources', 'Structures', 'Domain', 'BCs', 'Monitors'},
)
def compute_fdtd_sim(self, input_sockets: dict) -> sp.Expr:

View File

@ -42,7 +42,7 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
('EZ', 'Ez', 'Electric field in z-dir'),
],
default='EX',
update=(lambda self, context: self.sync_prop('pol_axis', context)),
update=(lambda self, context: self.on_prop_changed('pol_axis', context)),
)
####################

View File

@ -1,11 +1,11 @@
import typing as typ
import bpy
import numpy as np
import sympy.physics.units as spu
import tidy3d as td
from ......utils import extra_sympy_units as spuex
from blender_maxwell.utils import extra_sympy_units as spuex
from .... import contracts as ct
from .... import managed_objs, sockets
from ... import base, events
@ -52,13 +52,13 @@ class GaussianPulseTemporalShapeNode(base.MaxwellSimNode):
name='Plot Time Start (ps)',
description='The instance ID of a particular MaxwellSimNode instance, used to index caches',
default=0.0,
update=(lambda self, context: self.sync_prop('plot_time_start', context)),
update=(lambda self, context: self.on_prop_changed('plot_time_start', context)),
)
plot_time_end: bpy.props.FloatProperty(
name='Plot Time End (ps)',
description='The instance ID of a particular MaxwellSimNode instance, used to index caches',
default=5,
update=(lambda self, context: self.sync_prop('plot_time_start', context)),
update=(lambda self, context: self.on_prop_changed('plot_time_start', context)),
)
####################

View File

@ -2,7 +2,8 @@ import typing as typ
import tidy3d as td
from .....utils import analyze_geonodes, logger
from blender_maxwell.utils import analyze_geonodes, logger
from ... import bl_socket_map, managed_objs, sockets
from ... import contracts as ct
from .. import base, events

View File

@ -69,7 +69,7 @@ class CombineNode(base.MaxwellSimNode):
default=1,
min=1,
# max=MAX_AMOUNT,
update=lambda self, context: self.sync_prop('amount', context),
update=lambda self, context: self.on_prop_changed('amount', context),
)
####################

View File

@ -8,7 +8,7 @@
#class MaxwellSimProp(bpy.types.PropertyGroup):
# """A Blender property usable in nodes and sockets."""
# name: str = ""
# data_flow_kind: ct.DataFlowKind
# data_flow_kind: ct.FlowKind
#
# value: dict[str, tuple[bpy.types.Property, dict]] | None = None
#

View File

@ -1,4 +1,5 @@
from ....utils import logger
from blender_maxwell.utils import logger
from .. import contracts as ct
from . import basic, blender, maxwell, number, physical, tidy3d, vector
from .scan_socket_defs import scan_for_socket_defs

View File

@ -1,11 +1,12 @@
from . import any as any_socket
from . import bool as bool_socket
from . import file_path, string
from . import expr, file_path, string
AnySocketDef = any_socket.AnySocketDef
BoolSocketDef = bool_socket.BoolSocketDef
FilePathSocketDef = file_path.FilePathSocketDef
StringSocketDef = string.StringSocketDef
FilePathSocketDef = file_path.FilePathSocketDef
ExprSocketDef = expr.ExprSocketDef
BL_REGISTER = [
@ -13,4 +14,5 @@ BL_REGISTER = [
*bool_socket.BL_REGISTER,
*string.BL_REGISTER,
*file_path.BL_REGISTER,
*expr.BL_REGISTER,
]

View File

@ -18,7 +18,7 @@ class BoolBLSocket(base.MaxwellSimSocket):
name='Boolean',
description='Represents a boolean value',
default=False,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -0,0 +1,132 @@
import typing as typ
import bpy
import pydantic as pyd
import sympy as sp
from blender_maxwell.utils import bl_cache
from blender_maxwell.utils import extra_sympy_units as spux
from ... import contracts as ct
from .. import base
class ExprBLSocket(base.MaxwellSimSocket):
socket_type = ct.SocketType.Expr
bl_label = 'Expr'
####################
# - Properties
####################
raw_value: bpy.props.StringProperty(
name='Expr',
description='Represents a symbolic expression',
default='',
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
int_symbols: set[spux.IntSymbol] = bl_cache.BLField([])
real_symbols: set[spux.RealSymbol] = bl_cache.BLField([])
complex_symbols: set[spux.ComplexSymbol] = bl_cache.BLField([])
@property
def symbols(self) -> list[spux.Symbol]:
"""Retrieves all symbols by concatenating int, real, and complex symbols, and sorting them by name.
The order is guaranteed to be **deterministic**.
Returns:
All symbols valid for use in the expression.
"""
return sorted(
self.int_symbols | self.real_symbols | self.complex_symbols,
key=lambda sym: sym.name,
)
####################
# - Socket UI
####################
def draw_value(self, col: bpy.types.UILayout) -> None:
col.prop(self, 'raw_value', text='')
####################
# - Computation of Default Value
####################
@property
def value(self) -> sp.Expr:
expr = sp.sympify(
self.raw_value,
locals={sym.name: sym for sym in self.symbols},
strict=False,
convert_xor=True,
).subs(spux.ALL_UNIT_SYMBOLS)
if not expr.free_symbols.issubset(self.symbols):
msg = f'Expression "{expr}" (symbols={self.expr.free_symbols}) has invalid symbols (valid symbols: {self.symbols})'
raise ValueError(msg)
return expr
@value.setter
def value(self, value: str) -> None:
self.raw_value = sp.sstr(value)
@property
def lazy_value_func(self) -> ct.LazyValueFuncFlow:
return ct.LazyValueFuncFlow(
func=sp.lambdify(self.symbols, self.value, 'jax'),
func_args=[
(sym.name, spux.sympy_to_python_type(sym)) for sym in self.symbols
],
supports_jax=True,
)
####################
# - Socket Configuration
####################
class ExprSocketDef(base.SocketDef):
socket_type: ct.SocketType = ct.SocketType.Expr
_x = sp.Symbol('x', real=True)
int_symbols: list[spux.IntSymbol] = []
real_symbols: list[spux.RealSymbol] = [_x]
complex_symbols: list[spux.ComplexSymbol] = []
# Expression
default_expr: spux.SympyExpr = _x
allow_units: bool = True
@pyd.model_validator(mode='after')
def check_default_expr_follows_unit_allowance(self) -> typ.Self:
"""Checks that `self.default_expr` only uses units if `self.allow_units` is defined.
Raises:
ValueError: If the expression uses symbols not defined in `self.symbols`.
"""
if not spux.uses_units(self.default_expr):
msg = f'Expression symbols ({self.default_expr.free_symbol}) are not a strict subset of defined symbols ({self.symbols})'
raise ValueError(msg)
@pyd.model_validator(mode='after')
def check_default_expr_uses_allowed_symbols(self) -> typ.Self:
"""Checks that `self.default_expr` only uses symbols defined in `self.symbols`.
Raises:
ValueError: If the expression uses symbols not defined in `self.symbols`.
"""
if not self.default_expr.free_symbols.issubset(self.symbols):
msg = f'Expression symbols ({self.default_expr.free_symbol}) are not a strict subset of defined symbols ({self.symbols})'
raise ValueError(msg)
def init(self, bl_socket: ExprBLSocket) -> None:
bl_socket.value = self.default_expr
bl_socket.symbols = self.symbols
####################
# - Blender Registration
####################
BL_REGISTER = [
ExprBLSocket,
]

View File

@ -20,7 +20,7 @@ class FilePathBLSocket(base.MaxwellSimSocket):
name='File Path',
description='Represents the path to a file',
subtype='FILE_PATH',
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -18,7 +18,7 @@ class StringBLSocket(base.MaxwellSimSocket):
name='String',
description='Represents a string',
default='',
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################
@ -57,3 +57,6 @@ class StringSocketDef(base.SocketDef):
BL_REGISTER = [
StringBLSocket,
]

View File

@ -18,7 +18,7 @@ class BlenderCollectionBLSocket(base.MaxwellSimSocket):
name='Blender Collection',
description='A Blender collection',
type=bpy.types.Collection,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -21,7 +21,7 @@ class BlenderMaxwellResetGeoNodesSocket(bpy.types.Operator):
socket = node.inputs[self.socket_name]
# Report as though the GeoNodes Tree Changed
socket.sync_prop('raw_value', context)
socket.on_prop_changed('raw_value', context)
return {'FINISHED'}
@ -41,7 +41,7 @@ class BlenderGeoNodesBLSocket(base.MaxwellSimSocket):
description='Represents a Blender GeoNodes Tree',
type=bpy.types.NodeTree,
poll=(lambda self, obj: obj.bl_idname == 'GeometryNodeTree'),
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -18,7 +18,7 @@ class BlenderImageBLSocket(base.MaxwellSimSocket):
name='Blender Image',
description='Represents a Blender Image',
type=bpy.types.Image,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -15,7 +15,7 @@ class BlenderMaterialBLSocket(base.MaxwellSimSocket):
name='Blender Material',
description='Represents a Blender material',
type=bpy.types.Material,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -39,7 +39,7 @@ class BlenderObjectBLSocket(base.MaxwellSimSocket):
name='Blender Object',
description='Represents a Blender object',
type=bpy.types.Object,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -18,7 +18,7 @@ class BlenderTextBLSocket(base.MaxwellSimSocket):
name='Blender Text',
description='Represents a Blender text datablock',
type=bpy.types.Text,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -1,6 +1,5 @@
import bpy
import tidy3d as td
import typing_extensions as typx
from ... import contracts as ct
from .. import base
@ -23,7 +22,7 @@ class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
('PERIODIC', 'Periodic', 'Infinitely periodic layer'),
],
default='PML',
update=(lambda self, context: self.sync_prop('default_choice', context)),
update=(lambda self, context: self.on_prop_changed('default_choice', context)),
)
####################
@ -45,7 +44,7 @@ class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
}[self.default_choice]
@value.setter
def value(self, value: typx.Literal['PML', 'PEC', 'PMC', 'PERIODIC']) -> None:
def value(self, value: typ.Literal['PML', 'PEC', 'PMC', 'PERIODIC']) -> None:
self.default_choice = value
@ -55,7 +54,7 @@ class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
class MaxwellBoundCondSocketDef(base.SocketDef):
socket_type: ct.SocketType = ct.SocketType.MaxwellBoundCond
default_choice: typx.Literal['PML', 'PEC', 'PMC', 'PERIODIC'] = 'PML'
default_choice: typ.Literal['PML', 'PEC', 'PMC', 'PERIODIC'] = 'PML'
def init(self, bl_socket: MaxwellBoundCondBLSocket) -> None:
bl_socket.value = self.default_choice

View File

@ -29,7 +29,7 @@ class MaxwellBoundCondsBLSocket(base.MaxwellSimSocket):
name='Show Bounds Definition',
description='Toggle to show bound faces',
default=False,
update=(lambda self, context: self.sync_prop('show_definition', context)),
update=(lambda self, context: self.on_prop_changed('show_definition', context)),
)
x_pos: bpy.props.EnumProperty(
@ -37,42 +37,42 @@ class MaxwellBoundCondsBLSocket(base.MaxwellSimSocket):
description='+x choice of default boundary face',
items=BOUND_FACE_ITEMS,
default='PML',
update=(lambda self, context: self.sync_prop('x_pos', context)),
update=(lambda self, context: self.on_prop_changed('x_pos', context)),
)
x_neg: bpy.props.EnumProperty(
name='-x Bound Face',
description='-x choice of default boundary face',
items=BOUND_FACE_ITEMS,
default='PML',
update=(lambda self, context: self.sync_prop('x_neg', context)),
update=(lambda self, context: self.on_prop_changed('x_neg', context)),
)
y_pos: bpy.props.EnumProperty(
name='+y Bound Face',
description='+y choice of default boundary face',
items=BOUND_FACE_ITEMS,
default='PML',
update=(lambda self, context: self.sync_prop('y_pos', context)),
update=(lambda self, context: self.on_prop_changed('y_pos', context)),
)
y_neg: bpy.props.EnumProperty(
name='-y Bound Face',
description='-y choice of default boundary face',
items=BOUND_FACE_ITEMS,
default='PML',
update=(lambda self, context: self.sync_prop('y_neg', context)),
update=(lambda self, context: self.on_prop_changed('y_neg', context)),
)
z_pos: bpy.props.EnumProperty(
name='+z Bound Face',
description='+z choice of default boundary face',
items=BOUND_FACE_ITEMS,
default='PML',
update=(lambda self, context: self.sync_prop('z_pos', context)),
update=(lambda self, context: self.on_prop_changed('z_pos', context)),
)
z_neg: bpy.props.EnumProperty(
name='-z Bound Face',
description='-z choice of default boundary face',
items=BOUND_FACE_ITEMS,
default='PML',
update=(lambda self, context: self.sync_prop('z_neg', context)),
update=(lambda self, context: self.on_prop_changed('z_neg', context)),
)
####################

View File

@ -3,7 +3,8 @@ import scipy as sc
import sympy.physics.units as spu
import tidy3d as td
from .....utils.pydantic_sympy import ConstrSympyExpr
from blender_maxwell.utils import extra_sympy_units as spux
from ... import contracts as ct
from .. import base
@ -24,7 +25,7 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
default=500.0,
precision=4,
step=50,
update=(lambda self, context: self.sync_prop('wl', context)),
update=(lambda self, context: self.on_prop_changed('wl', context)),
)
rel_permittivity: bpy.props.FloatVectorProperty(
@ -33,7 +34,9 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
size=2,
default=(1.0, 0.0),
precision=2,
update=(lambda self, context: self.sync_prop('rel_permittivity', context)),
update=(
lambda self, context: self.on_prop_changed('rel_permittivity', context)
),
)
####################
@ -71,7 +74,7 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
@value.setter
def value(
self, value: tuple[ConstrSympyExpr(allow_variables=False), complex]
self, value: tuple[spux.ConstrSympyExpr(allow_variables=False), complex]
) -> None:
_wl, rel_permittivity = value

View File

@ -18,7 +18,7 @@ class MaxwellMonitorSocketDef(base.SocketDef):
def init(self, bl_socket: MaxwellMonitorBLSocket) -> None:
if self.is_list:
bl_socket.active_kind = ct.DataFlowKind.ValueArray
bl_socket.active_kind = ct.FlowKind.Array
####################

View File

@ -19,7 +19,7 @@ class MaxwellSimGridBLSocket(base.MaxwellSimSocket):
min=0.01,
# step=10,
precision=2,
update=(lambda self, context: self.sync_prop('min_steps_per_wl', context)),
update=(lambda self, context: self.on_prop_changed('min_steps_per_wl', context)),
)
####################

View File

@ -18,7 +18,7 @@ class MaxwellSourceSocketDef(base.SocketDef):
def init(self, bl_socket: MaxwellSourceBLSocket) -> None:
if self.is_list:
bl_socket.active_kind = ct.DataFlowKind.ValueArray
bl_socket.active_kind = ct.FlowKind.Array
####################

View File

@ -18,7 +18,7 @@ class MaxwellStructureSocketDef(base.SocketDef):
def init(self, bl_socket: MaxwellStructureBLSocket) -> None:
if self.is_list:
bl_socket.active_kind = ct.DataFlowKind.ValueArray
bl_socket.active_kind = ct.FlowKind.ValueArray
####################

View File

@ -3,7 +3,8 @@ import typing as typ
import bpy
import sympy as sp
from .....utils.pydantic_sympy import SympyExpr
from blender_maxwell.utils import extra_sympy_units as spux
from ... import contracts as ct
from .. import base
@ -23,8 +24,7 @@ class ComplexNumberBLSocket(base.MaxwellSimSocket):
description='Represents a complex number (real, imaginary)',
size=2,
default=(0.0, 0.0),
subtype='NONE',
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
coord_sys: bpy.props.EnumProperty(
name='Coordinate System',
@ -46,58 +46,20 @@ class ComplexNumberBLSocket(base.MaxwellSimSocket):
),
],
default='CARTESIAN',
update=lambda self, context: self._sync_coord_sys(context),
update=lambda self, context: self.on_coord_sys_changed(context),
)
####################
# - Socket UI
# - Event Methods
####################
def draw_value(self, col: bpy.types.UILayout) -> None:
"""Draw the value of the complex number, including a toggle for
specifying the active coordinate system.
def on_coord_sys_changed(self, context: bpy.types.Context):
r"""Transforms values when the coordinate system changes.
Notes:
Cartesian coordinates with $y=0$ has no corresponding $\theta$
Therefore, we manually set $\theta=0$.
"""
col_row = col.row()
col_row.prop(self, 'raw_value', text='')
col.prop(self, 'coord_sys', text='')
####################
# - Computation of Default Value
####################
@property
def value(self) -> SympyExpr:
"""Return the complex number as a sympy expression, of a form
determined by the coordinate system.
- Cartesian: a,b -> a + ib
- Polar: r,t -> re^(it)
Returns:
The sympy expression representing the complex number.
"""
v1, v2 = self.raw_value
return {
'CARTESIAN': v1 + sp.I * v2,
'POLAR': v1 * sp.exp(sp.I * v2),
}[self.coord_sys]
@value.setter
def value(self, value: SympyExpr) -> None:
"""Set the complex number from a sympy expression, using an internal
representation determined by the coordinate system.
- Cartesian: a,b -> a + ib
- Polar: r,t -> re^(it)
"""
self.raw_value = {
'CARTESIAN': (sp.re(value), sp.im(value)),
'POLAR': (sp.Abs(value), sp.arg(value)),
}[self.coord_sys]
####################
# - Internal Update Methods
####################
def _sync_coord_sys(self, context: bpy.types.Context):
if self.coord_sys == 'CARTESIAN':
r, theta_rad = self.raw_value
self.raw_value = (
@ -108,11 +70,58 @@ class ComplexNumberBLSocket(base.MaxwellSimSocket):
x, y = self.raw_value
cart_value = x + sp.I * y
self.raw_value = (
sp.Abs(cart_value),
sp.arg(cart_value) if y != 0 else 0,
float(sp.Abs(cart_value)),
float(sp.arg(cart_value)) if y != 0 else float(0),
)
self.sync_prop('coord_sys', context)
self.on_prop_changed('coord_sys', context)
####################
# - Socket UI
####################
def draw_value(self, col: bpy.types.UILayout) -> None:
"""Draw the value of the complex number, including a toggle for specifying the active coordinate system."""
# Value Row
row = col.row()
row.prop(self, 'raw_value', text='')
# Coordinate System Dropdown
col.prop(self, 'coord_sys', text='')
####################
# - Computation of Default Value
####################
@property
def value(self) -> spux.Complex:
"""Return the complex number as a sympy expression, of a form determined by the coordinate system.
- **Cartesian**: $(a,b) -> a + ib$
- **Polar**: $(r,t) -> re^(it)$
Returns:
The complex number as a `sympy` type.
"""
v1, v2 = self.raw_value
return {
'CARTESIAN': v1 + sp.I * v2,
'POLAR': v1 * sp.exp(sp.I * v2),
}[self.coord_sys]
@value.setter
def value(self, value: spux.Complex) -> None:
"""Set the complex number from a sympy expression, by numerically simplifying it into coordinate-system determined components.
- **Cartesian**: $(a,b) -> a + ib$
- **Polar**: $(r,t) -> re^(it)$
Parameters:
value: The complex number as a `sympy` type.
"""
self.raw_value = {
'CARTESIAN': (float(sp.re(value)), float(sp.im(value))),
'POLAR': (float(sp.Abs(value)), float(sp.arg(value))),
}[self.coord_sys]
####################
@ -121,7 +130,7 @@ class ComplexNumberBLSocket(base.MaxwellSimSocket):
class ComplexNumberSocketDef(base.SocketDef):
socket_type: ct.SocketType = ct.SocketType.ComplexNumber
default_value: SympyExpr = sp.S(0 + 0j)
default_value: spux.Complex = sp.S(0)
coord_sys: typ.Literal['CARTESIAN', 'POLAR'] = 'CARTESIAN'
def init(self, bl_socket: ComplexNumberBLSocket) -> None:

View File

@ -18,7 +18,7 @@ class IntegerNumberBLSocket(base.MaxwellSimSocket):
name='Integer',
description='Represents an integer',
default=0,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -1,7 +1,8 @@
import bpy
import sympy as sp
from .....utils.pydantic_sympy import SympyExpr
from blender_maxwell.utils.pydantic_sympy import SympyExpr
from ... import contracts as ct
from .. import base
@ -22,7 +23,7 @@ class RationalNumberBLSocket(base.MaxwellSimSocket):
size=2,
default=(1, 1),
subtype='NONE',
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -1,6 +1,7 @@
import bpy
from .....utils.pydantic_sympy import SympyExpr
from blender_maxwell.utils.pydantic_sympy import SympyExpr
from ... import contracts as ct
from .. import base
@ -20,7 +21,7 @@ class RealNumberBLSocket(base.MaxwellSimSocket):
description='Represents a real number',
default=0.0,
precision=6,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -1,7 +1,8 @@
import bpy
import sympy.physics.units as spu
from .....utils.pydantic_sympy import SympyExpr
from blender_maxwell.utils.pydantic_sympy import SympyExpr
from ... import contracts as ct
from .. import base
@ -22,7 +23,7 @@ class PhysicalAccelScalarBLSocket(base.MaxwellSimSocket):
description='Represents the unitless part of the acceleration',
default=0.0,
precision=6,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -1,7 +1,8 @@
import bpy
import sympy.physics.units as spu
from .....utils.pydantic_sympy import SympyExpr
from blender_maxwell.utils.pydantic_sympy import SympyExpr
from ... import contracts as ct
from .. import base
@ -22,7 +23,7 @@ class PhysicalAngleBLSocket(base.MaxwellSimSocket):
description='Represents the unitless part of the acceleration',
default=0.0,
precision=4,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -20,7 +20,7 @@ class PhysicalAreaBLSocket(base.MaxwellSimSocket):
description='Represents the unitless part of the area',
default=0.0,
precision=6,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -1,7 +1,8 @@
import bpy
import sympy.physics.units as spu
from .....utils.pydantic_sympy import SympyExpr
from blender_maxwell.utils.pydantic_sympy import SympyExpr
from ... import contracts as ct
from .. import base
@ -22,7 +23,7 @@ class PhysicalForceScalarBLSocket(base.MaxwellSimSocket):
description='Represents the unitless part of the force',
default=0.0,
precision=6,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

View File

@ -1,9 +1,10 @@
import bpy
import sympy as sp
from .....utils import extra_sympy_units as spux
from .....utils import logger
from .....utils.pydantic_sympy import SympyExpr
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from blender_maxwell.utils.pydantic_sympy import SympyExpr
from ... import contracts as ct
from .. import base
@ -26,7 +27,7 @@ class PhysicalFreqBLSocket(base.MaxwellSimSocket):
description='Represents the unitless part of the frequency',
default=0.0,
precision=6,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
min_freq: bpy.props.FloatProperty(
@ -34,20 +35,20 @@ class PhysicalFreqBLSocket(base.MaxwellSimSocket):
description='Lowest frequency',
default=0.0,
precision=4,
update=(lambda self, context: self.sync_prop('min_freq', context)),
update=(lambda self, context: self.on_prop_changed('min_freq', context)),
)
max_freq: bpy.props.FloatProperty(
name='Max Frequency',
description='Highest frequency',
default=0.0,
precision=4,
update=(lambda self, context: self.sync_prop('max_freq', context)),
update=(lambda self, context: self.on_prop_changed('max_freq', context)),
)
steps: bpy.props.IntProperty(
name='Frequency Steps',
description='# of steps between min and max',
default=2,
update=(lambda self, context: self.sync_prop('steps', context)),
update=(lambda self, context: self.on_prop_changed('steps', context)),
)
####################
@ -73,10 +74,9 @@ class PhysicalFreqBLSocket(base.MaxwellSimSocket):
self.raw_value = spux.sympy_to_python(spux.scale_to_unit(value, self.unit))
@property
def lazy_value_range(self) -> ct.LazyDataValueRange:
return ct.LazyDataValueRange(
def lazy_array_range(self) -> ct.LazyArrayRange:
return ct.LazyArrayRange(
symbols=set(),
has_unit=True,
unit=self.unit,
start=sp.S(self.min_freq) * self.unit,
stop=sp.S(self.max_freq) * self.unit,
@ -84,9 +84,8 @@ class PhysicalFreqBLSocket(base.MaxwellSimSocket):
scaling='lin',
)
@lazy_value_range.setter
def lazy_value_range(self, value: tuple[sp.Expr, sp.Expr, int]) -> None:
log.debug('Lazy Value Range: %s', str(value))
@lazy_array_range.setter
def lazy_array_range(self, value: ct.LazyArrayRangeFlow) -> None:
self.min_freq = spux.sympy_to_python(spux.scale_to_unit(value[0], self.unit))
self.max_freq = spux.sympy_to_python(spux.scale_to_unit(value[1], self.unit))
self.steps = value[2]
@ -111,7 +110,7 @@ class PhysicalFreqSocketDef(base.SocketDef):
bl_socket.value = self.default_value
if self.is_array:
bl_socket.active_kind = ct.DataFlowKind.LazyValueRange
bl_socket.active_kind = ct.FlowKind.LazyArrayRange
bl_socket.lazy_value_range = (self.min_freq, self.max_freq, self.steps)

View File

@ -2,9 +2,10 @@ import bpy
import sympy as sp
import sympy.physics.units as spu
from .....utils import extra_sympy_units as spux
from .....utils import logger
from .....utils.pydantic_sympy import SympyExpr
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from blender_maxwell.utils.pydantic_sympy import SympyExpr
from ... import contracts as ct
from .. import base
@ -27,7 +28,7 @@ class PhysicalLengthBLSocket(base.MaxwellSimSocket):
description='Represents the unitless part of the length',
default=0.0,
precision=6,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
min_len: bpy.props.FloatProperty(
@ -35,20 +36,20 @@ class PhysicalLengthBLSocket(base.MaxwellSimSocket):
description='Lowest length',
default=0.0,
precision=4,
update=(lambda self, context: self.sync_prop('min_len', context)),
update=(lambda self, context: self.on_prop_changed('min_len', context)),
)
max_len: bpy.props.FloatProperty(
name='Max Length',
description='Highest length',
default=0.0,
precision=4,
update=(lambda self, context: self.sync_prop('max_len', context)),
update=(lambda self, context: self.on_prop_changed('max_len', context)),
)
steps: bpy.props.IntProperty(
name='Length Steps',
description='# of steps between min and max',
default=2,
update=(lambda self, context: self.sync_prop('steps', context)),
update=(lambda self, context: self.on_prop_changed('steps', context)),
)
####################
@ -74,10 +75,9 @@ class PhysicalLengthBLSocket(base.MaxwellSimSocket):
self.raw_value = spux.sympy_to_python(spux.scale_to_unit(value, self.unit))
@property
def lazy_value_range(self) -> ct.LazyDataValueRange:
return ct.LazyDataValueRange(
def lazy_array_range(self) -> ct.LazyArrayRange:
return ct.LazyArrayRange(
symbols=set(),
has_unit=True,
unit=self.unit,
start=sp.S(self.min_len) * self.unit,
stop=sp.S(self.max_len) * self.unit,
@ -85,7 +85,7 @@ class PhysicalLengthBLSocket(base.MaxwellSimSocket):
scaling='lin',
)
@lazy_value_range.setter
@lazy_array_range.setter
def lazy_value_range(self, value: tuple[sp.Expr, sp.Expr, int]) -> None:
self.min_len = spux.sympy_to_python(spux.scale_to_unit(value[0], self.unit))
self.max_len = spux.sympy_to_python(spux.scale_to_unit(value[1], self.unit))
@ -112,7 +112,7 @@ class PhysicalLengthSocketDef(base.SocketDef):
bl_socket.value = self.default_value
if self.is_array:
bl_socket.active_kind = ct.DataFlowKind.LazyValueRange
bl_socket.active_kind = ct.FlowKind.LazyArrayRange
bl_socket.lazy_value_range = (self.min_len, self.max_len, self.steps)

View File

@ -1,7 +1,8 @@
import bpy
import sympy.physics.units as spu
from .....utils.pydantic_sympy import SympyExpr
from blender_maxwell.utils.pydantic_sympy import SympyExpr
from ... import contracts as ct
from .. import base
@ -22,7 +23,7 @@ class PhysicalMassBLSocket(base.MaxwellSimSocket):
description='Represents the unitless part of mass',
default=0.0,
precision=6,
update=(lambda self, context: self.sync_prop('raw_value', context)),
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
####################

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