fix: Caching now (seems to) work robustly.

The serialization routines are fast and effective.
Overall, the node graph feels snappy, and everything updates smoothly.
Logging on the action chain suggests that there aren't extraneous calls,
and that existing calls (ex. no-op previews) are fast.

There will likely be edge cases, and we'll see how it scales - but
for now, let's go with it!
main
Sofus Albert Høgsbro Rose 2024-04-15 19:15:20 +02:00
parent 269af4ba32
commit 568fc449e8
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
27 changed files with 112 additions and 189 deletions

View File

@ -504,7 +504,7 @@ Unreported:
- We found the translation callback! https://projects.blender.org/blender/blender/commit/8564e03cdf59fb2a71d545e81871411b82f561d9
- This can update the node center!!
- [ ] Optimize the `DataChanged` invalidator.
- [x] Optimize the `DataChanged` invalidator.
- [ ] Optimize unit stripping.
@ -551,7 +551,7 @@ We need support for arbitrary objects, but still backed by the persistance seman
- [x] For serializeability, let the encoder/decoder be able to make use of an optional `.msgspec_encodable()` and similar decoder respectively, and add support for these in the ENCODER/DECODER functions.
- [x] Define a superclass for `SocketDef` and make everyone inherit from it
- [ ] Collect with a `BL_SOCKET_DEFS` object, instead of manually from `__init__.py`s
- [ ] Add support for `.msgspec_*()` methods, so that we remove the dependency on sockets from the serialization module.
- [x] Add support for `.msgspec_*()` methods, so that we remove the dependency on sockets from the serialization module.
### Sweeping Features
- [ ] Replace all raw Blender properties with `BLField`.

View File

@ -33,9 +33,11 @@ class BLInstance(typ.Protocol):
) -> None: ...
PropGetMethod: typ.TypeAlias = typ.Callable[[BLInstance], serialize.EncodableValue]
PropGetMethod: typ.TypeAlias = typ.Callable[
[BLInstance], serialize.NaivelyEncodableType
]
PropSetMethod: typ.TypeAlias = typ.Callable[
[BLInstance, serialize.EncodableValue], None
[BLInstance, serialize.NaivelyEncodableType], None
]

View File

@ -132,7 +132,7 @@ def _socket_def_from_bl_socket(
def socket_def_from_bl_socket(
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
) -> ct.schemas.SocketDef:
) -> sockets.base.SocketDef:
"""Computes an appropriate (no-arg) SocketDef from the given `bl_interface_socket`, by parsing it."""
return _socket_def_from_bl_socket(
bl_interface_socket.description, bl_interface_socket.bl_socket_idname

View File

@ -65,11 +65,6 @@ from .data_flows import (
)
from .data_flow_actions import DataFlowAction
####################
# - Schemas
####################
from . import schemas
####################
# - Export
####################
@ -103,5 +98,4 @@ __all__ = [
'LazyDataValueRange',
'LazyDataValueSpectrum',
'DataFlowAction',
'schemas',
]

View File

@ -1,11 +0,0 @@
from .managed_obj import ManagedObj
from .managed_obj_def import ManagedObjDef
from .preset_def import PresetDef
from .socket_def import SocketDef
__all__ = [
'SocketDef',
'ManagedObj',
'ManagedObjDef',
'PresetDef',
]

View File

@ -1,26 +0,0 @@
import abc
import typing as typ
import bpy
import pydantic as pyd
from .....utils import serialize
from ..socket_types import SocketType
class SocketDef(pyd.BaseModel, abc.ABC):
socket_type: SocketType
@abc.abstractmethod
def init(self, bl_socket: bpy.types.NodeSocket) -> None:
"""Initializes a real Blender node socket from this socket definition."""
####################
# - Serialization
####################
def dump_as_msgspec(self) -> serialize.NaiveRepresentation:
return [serialize.TypeID.SocketDef, self.__class__.__name__, self.model_dump()]
@staticmethod
def parse_as_msgspec(obj: serialize.NaiveRepresentation) -> typ.Self:
return SocketDef.__subclasses__[obj[1]](**obj[2])

View File

@ -1,9 +1,12 @@
import abc
import typing as typ
from ....utils import serialize
from blender_maxwell.utils import logger, serialize
from .. import contracts as ct
log = logger.get(__name__)
class ManagedObj(abc.ABC):
managed_obj_type: ct.ManagedObjType
@ -50,9 +53,13 @@ class ManagedObj(abc.ABC):
return [
serialize.TypeID.ManagedObj,
self.__class__.__name__,
(self.managed_obj_type, self.name),
self.name,
]
@staticmethod
def parse_as_msgspec(obj: serialize.NaiveRepresentation) -> typ.Self:
return ManagedObj.__subclasses__[obj[1]](**obj[2])
return next(
subclass(obj[2])
for subclass in ManagedObj.__subclasses__()
if subclass.__name__ == obj[1]
)

View File

@ -24,23 +24,12 @@ class VizNode(base.MaxwellSimNode):
'Data': sockets.AnySocketDef(),
'Freq': sockets.PhysicalFreqSocketDef(),
}
# input_sockets_sets: typ.ClassVar = {
# '2D Freq': {
# 'Data': sockets.AnySocketDef(),
# 'Freq': sockets.PhysicalFreqSocketDef(),
# },
# }
output_sockets: typ.ClassVar = {
'Preview': sockets.AnySocketDef(),
}
managed_obj_defs: typ.ClassVar = {
'plot': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLImage(name),
),
#'empty': ct.schemas.ManagedObjDef(
# mk=lambda name: managed_objs.ManagedBLEmpty(name),
# ),
managed_obj_types: typ.ClassVar = {
'plot': managed_objs.ManagedBLImage,
}
#####################
@ -78,7 +67,7 @@ class VizNode(base.MaxwellSimNode):
)
def on_show_plot(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
managed_objs: dict,
input_sockets: dict,
props: dict,
unit_systems: dict,

View File

@ -12,11 +12,13 @@ import bpy
import sympy as sp
import typing_extensions as typx
from ....utils import logger
from .. import bl_cache
from blender_maxwell.utils import logger
from .. import bl_cache, sockets
from .. import contracts as ct
from .. import managed_objs as _managed_objs
from . import events
from . import presets as _presets
log = logger.get(__name__)
@ -42,17 +44,23 @@ class MaxwellSimNode(bpy.types.Node):
## TODO: bl_description from first line of __doc__?
# Sockets
input_sockets: typ.ClassVar[dict[str, ct.schemas.SocketDef]] = MappingProxyType({})
output_sockets: typ.ClassVar[dict[str, ct.schemas.SocketDef]] = MappingProxyType({})
input_socket_sets: typ.ClassVar[dict[str, dict[str, ct.schemas.SocketDef]]] = (
input_sockets: typ.ClassVar[dict[str, sockets.base.SocketDef]] = MappingProxyType(
{}
)
output_sockets: typ.ClassVar[dict[str, sockets.base.SocketDef]] = MappingProxyType(
{}
)
input_socket_sets: typ.ClassVar[dict[str, dict[str, sockets.base.SocketDef]]] = (
MappingProxyType({})
)
output_socket_sets: typ.ClassVar[dict[str, dict[str, ct.schemas.SocketDef]]] = (
output_socket_sets: typ.ClassVar[dict[str, dict[str, sockets.base.SocketDef]]] = (
MappingProxyType({})
)
# Presets
presets: typ.ClassVar = MappingProxyType({})
presets: typ.ClassVar[dict[str, dict[str, _presets.PresetDef]]] = MappingProxyType(
{}
)
# Managed Objects
managed_obj_types: typ.ClassVar[
@ -300,8 +308,8 @@ class MaxwellSimNode(bpy.types.Node):
####################
# - Loose Sockets w/Events
####################
loose_input_sockets: dict[str, ct.schemas.SocketDef] = bl_cache.BLField({})
loose_output_sockets: dict[str, ct.schemas.SocketDef] = bl_cache.BLField({})
loose_input_sockets: dict[str, sockets.base.SocketDef] = bl_cache.BLField({})
loose_output_sockets: dict[str, sockets.base.SocketDef] = bl_cache.BLField({})
@events.on_value_changed(prop_name={'loose_input_sockets', 'loose_output_sockets'})
def _on_loose_sockets_changed(self):
@ -331,7 +339,7 @@ class MaxwellSimNode(bpy.types.Node):
def _active_socket_set_socket_defs(
self,
direc: typx.Literal['input', 'output'],
) -> dict[ct.SocketName, ct.schemas.SocketDef]:
) -> dict[ct.SocketName, sockets.base.SocketDef]:
"""Retrieve all socket definitions for sockets that should be defined, according to the `self.active_socket_set`.
Notes:
@ -341,7 +349,7 @@ class MaxwellSimNode(bpy.types.Node):
direc: The direction to load Blender sockets from.
Returns:
Mapping from socket names to corresponding `ct.schemas.SocketDef`s.
Mapping from socket names to corresponding `sockets.base.SocketDef`s.
If `self.active_socket_set` is None, the empty dict is returned.
"""
@ -357,14 +365,14 @@ class MaxwellSimNode(bpy.types.Node):
def active_socket_defs(
self, direc: typx.Literal['input', 'output']
) -> dict[ct.SocketName, ct.schemas.SocketDef]:
) -> dict[ct.SocketName, sockets.base.SocketDef]:
"""Retrieve all socket definitions for sockets that should be defined.
Parameters:
direc: The direction to load Blender sockets from.
Returns:
Mapping from socket names to corresponding `ct.schemas.SocketDef`s.
Mapping from socket names to corresponding `sockets.base.SocketDef`s.
"""
static_sockets = self.input_sockets if direc == 'input' else self.output_sockets
loose_sockets = (
@ -700,13 +708,6 @@ class MaxwellSimNode(bpy.types.Node):
stop_propagation |= event_method.stop_propagation
event_method(self)
# Stop Propagation (maybe)
if (
ct.DataFlowAction.stop_if_no_event_methods(action)
and len(triggered_event_methods) == 0
):
return
# Propagate Action to All Sockets in "Trigger Direction"
## The trigger chain goes node/socket/node/socket/...
if not stop_propagation:

View File

@ -47,11 +47,8 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode):
input_sockets: typ.ClassVar = {
'File Path': sockets.FilePathSocketDef(),
}
managed_obj_defs: typ.ClassVar = {
'plot': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLImage(name),
name_prefix='',
),
managed_obj_types: typ.ClassVar = {
'plot': managed_objs.ManagedBLImage,
}
####################

View File

@ -26,11 +26,8 @@ class LibraryMediumNode(base.MaxwellSimNode):
'Medium': sockets.MaxwellMediumSocketDef(),
}
managed_obj_defs = {
'nk_plot': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLImage(name),
name_prefix='',
)
managed_obj_types = {
'nk_plot': managed_objs.ManagedBLImage,
}
####################
@ -125,8 +122,8 @@ class LibraryMediumNode(base.MaxwellSimNode):
)
def on_show_plot(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
props: dict[str, typ.Any],
managed_objs: dict,
props: dict,
):
medium = td.material_library[props['material']].medium
freq_range = [

View File

@ -49,13 +49,9 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
'Time Domain': {'Time Monitor': sockets.MaxwellMonitorSocketDef()},
}
managed_obj_defs: typ.ClassVar = {
'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name),
),
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
managed_obj_types: typ.ClassVar = {
'mesh': managed_objs.ManagedBLMesh,
'modifier': managed_objs.ManagedBLModifier,
}
####################
@ -117,7 +113,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
def on_inputs_changed(
self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
managed_objs: dict,
input_sockets: dict,
unit_systems: dict,
):

View File

@ -48,13 +48,9 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
'Time Domain': {'Time Monitor': sockets.MaxwellMonitorSocketDef()},
}
managed_obj_defs: typ.ClassVar = {
'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name),
),
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
managed_obj_types: typ.ClassVar = {
'mesh': managed_objs.ManagedBLMesh,
'modifier': managed_objs.ManagedBLModifier,
}
####################
@ -96,7 +92,7 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
center=input_sockets['Center'],
size=input_sockets['Size'],
name=props['sim_node_name'],
interval_space=(1,1,1),
interval_space=(1, 1, 1),
freqs=input_sockets['Freqs'].realize().values,
normal_dir='+' if input_sockets['Direction'] else '-',
)
@ -118,7 +114,7 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
def on_inputs_changed(
self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
managed_objs: dict,
input_sockets: dict,
unit_systems: dict,
):

View File

@ -2,10 +2,10 @@ import typing as typ
import pydantic as pyd
from ..bl import PresetName, SocketName
from .. import contracts as ct
class PresetDef(pyd.BaseModel):
label: PresetName
label: ct.PresetName
description: str
values: dict[SocketName, typ.Any]
values: dict[ct.SocketName, typ.Any]

View File

@ -28,14 +28,9 @@ class SimDomainNode(base.MaxwellSimNode):
'Domain': sockets.MaxwellSimDomainSocketDef(),
}
managed_obj_defs: typ.ClassVar = {
'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name),
name_prefix='',
),
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
managed_obj_types: typ.ClassVar = {
'mesh': managed_objs.ManagedBLMesh,
'modifier': managed_objs.ManagedBLModifier,
}
####################
@ -75,7 +70,7 @@ class SimDomainNode(base.MaxwellSimNode):
def on_input_changed(
self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
managed_objs: dict,
input_sockets: dict,
unit_systems: dict,
):

View File

@ -56,11 +56,8 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
'Source': sockets.MaxwellSourceSocketDef(),
}
managed_obj_defs: typ.ClassVar = {
'plane_wave_source': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix='',
)
managed_obj_types: typ.ClassVar = {
'plane_wave_source': managed_objs.ManagedBLMesh,
}
####################

View File

@ -26,11 +26,8 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
'Source': sockets.MaxwellSourceSocketDef(),
}
managed_obj_defs = {
'sphere_empty': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix='',
)
managed_obj_types = {
'mesh': managed_objs.ManagedBLMesh,
}
####################

View File

@ -41,11 +41,8 @@ class GaussianPulseTemporalShapeNode(base.MaxwellSimNode):
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
}
managed_obj_defs = {
'amp_time': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLImage(name),
name_prefix='amp_time_',
)
managed_obj_types = {
'amp_time': managed_objs.ManagedBLImage,
}
####################
@ -123,9 +120,9 @@ class GaussianPulseTemporalShapeNode(base.MaxwellSimNode):
)
def on_show_plot(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
output_sockets: dict[str, typ.Any],
props: dict[str, typ.Any],
managed_objs: dict,
output_sockets: dict,
props: dict,
):
temporal_shape = output_sockets['Temporal Shape']
plot_time_start = props['plot_time_start'] * 1e-15

View File

@ -27,13 +27,9 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
'Structure': sockets.MaxwellStructureSocketDef(),
}
managed_obj_defs: typ.ClassVar = {
'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name),
),
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
managed_obj_types: typ.ClassVar = {
'mesh': managed_objs.ManagedBLMesh,
'modifier': managed_objs.ManagedBLModifier,
}
####################
@ -46,8 +42,8 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
)
def compute_structure(
self,
input_sockets: dict[str, typ.Any],
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict,
managed_objs: dict,
) -> td.Structure:
# Simulate Input Value Change
## This ensures that the mesh has been re-computed.
@ -68,7 +64,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
# - Event Methods
####################
@events.on_value_changed(
socket_name='GeoNodes',
socket_name={'GeoNodes', 'Center'},
prop_name='preview_active',
any_loose_input_socket=True,
run_on_init=True,
@ -83,7 +79,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
def on_input_changed(
self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
managed_objs: dict,
input_sockets: dict,
loose_input_sockets: dict,
unit_systems: dict,

View File

@ -29,13 +29,9 @@ class BoxStructureNode(base.MaxwellSimNode):
'Structure': sockets.MaxwellStructureSocketDef(),
}
managed_obj_defs: typ.ClassVar = {
'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name),
),
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
managed_obj_types: typ.ClassVar = {
'mesh': managed_objs.ManagedBLMesh,
'modifier': managed_objs.ManagedBLModifier,
}
####################
@ -74,7 +70,7 @@ class BoxStructureNode(base.MaxwellSimNode):
def on_inputs_changed(
self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
managed_objs: dict,
input_sockets: dict,
unit_systems: dict,
):

View File

@ -28,13 +28,9 @@ class SphereStructureNode(base.MaxwellSimNode):
'Structure': sockets.MaxwellStructureSocketDef(),
}
managed_obj_defs: typ.ClassVar = {
'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLMesh(name),
),
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
managed_obj_types: typ.ClassVar = {
'mesh': managed_objs.ManagedBLMesh,
'modifier': managed_objs.ManagedBLModifier,
}
####################
@ -77,7 +73,7 @@ class SphereStructureNode(base.MaxwellSimNode):
def on_inputs_changed(
self,
props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
managed_objs: dict,
input_sockets: dict,
unit_systems: dict,
):

View File

@ -7,10 +7,8 @@ import pydantic as pyd
import sympy as sp
import typing_extensions as typx
from .....utils import serialize
from ....utils import logger
from ....utils import logger, serialize
from .. import contracts as ct
from ..socket_types import SocketType
log = logger.get(__name__)
@ -19,7 +17,7 @@ log = logger.get(__name__)
# - SocketDef
####################
class SocketDef(pyd.BaseModel, abc.ABC):
socket_type: SocketType
socket_type: ct.SocketType
@abc.abstractmethod
def init(self, bl_socket: bpy.types.NodeSocket) -> None:
@ -33,7 +31,11 @@ class SocketDef(pyd.BaseModel, abc.ABC):
@staticmethod
def parse_as_msgspec(obj: serialize.NaiveRepresentation) -> typ.Self:
return SocketDef.__subclasses__[obj[1]](**obj[2])
return next(
subclass(**obj[2])
for subclass in SocketDef.__subclasses__()
if subclass.__name__ == obj[1]
)
####################

View File

@ -3,9 +3,10 @@ import types
from .. import contracts as ct
## TODO: Replace with BL_SOCKET_DEFS export from each module, a little like BL_NODES.
def scan_for_socket_defs(
sockets_module: types.ModuleType,
) -> dict[ct.SocketType, type[ct.schemas.SocketDef]]:
) -> dict:
return {
socket_type: getattr(
sockets_module,

View File

@ -5,7 +5,7 @@ import typing as typ
import sympy as sp
import sympy.physics.units as spu
SympyType = sp.Basic | sp.Expr | sp.MatrixBase | sp.Quantity
SympyType = sp.Basic | sp.Expr | sp.MatrixBase | spu.Quantity
####################

View File

@ -1,6 +1,7 @@
"""Attributes:
NaiveEncodableType:
See <https://jcristharif.com/msgspec/supported-types.html> for details.
"""Robust serialization tool for use in the addon.
Attributes:
NaiveEncodableType: See <https://jcristharif.com/msgspec/supported-types.html> for details.
"""
import dataclasses
@ -61,19 +62,20 @@ NaivelyEncodableType: typ.TypeAlias = (
| enum.Flag
| enum.IntFlag
| dataclasses.dataclass
| typ.Optional
| typ.Union
| typ.Optional[typ.Any] # noqa: UP007
| typ.Union[typ.Any, ...] # noqa: UP007
| typ.NewType
| typ.TypeAlias
| typ.TypeAliasType
| typ.Generic
| typ.TypeVar
| typ.Final
## SUPPORT:
# | typ.Generic[typ.Any]
# | typ.TypeVar
# | typ.Final
| msgspec.Raw
## NO SUPPORT:
# | msgspec.UNSET
)
_NaivelyEncodableTypeSet = frozenset(typ.get_args(NaivelyEncodableType))
## TODO: Use for runtime type check? Beartype?
class TypeID(enum.StrEnum):
@ -87,7 +89,7 @@ NaiveRepresentation: typ.TypeAlias = list[TypeID, str | None, typ.Any]
def is_representation(obj: NaivelyEncodableType) -> bool:
return isinstance(obj, list) and obj[0] in set(TypeID) and len(obj) == 3 # noqa: PLR2004
return isinstance(obj, list) and len(obj) == 3 and obj[0] in set(TypeID) # noqa: PLR2004
####################
@ -141,7 +143,9 @@ def _dec_hook(_type: type, obj: NaivelyEncodableType) -> typ.Any:
obj_value = obj[2]
return sp.sympify(obj_value).subs(spux.ALL_UNIT_SYMBOLS)
if hasattr(obj, 'parse_as_msgspec'):
if hasattr(_type, 'parse_as_msgspec') and (
is_representation(obj) and obj[0] in [TypeID.SocketDef, TypeID.ManagedObj]
):
return _type.parse_as_msgspec(obj)
msg = f'Can\'t decode "{obj}" to type {type(obj)}'