From 14b98d219e283cb681c28d1ca518229898feb3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofus=20Albert=20H=C3=B8gsbro=20Rose?= Date: Thu, 2 May 2024 14:24:23 +0200 Subject: [PATCH] feat: Finished Library Medium node. It now supports selecting the variant, and exporting a generated LazyArrayRange of valid freqs/wls. The UI was also revamped, with greatly more readable range values, dynamic labels, link button pointing to the original data, etc. . While more LOC, the code structure is also far more explicit and predictable to maintain. --- TODO.md | 40 +- .../maxwell_sim_nodes/nodes/__init__.py | 6 +- .../nodes/mediums/library_medium.py | 346 +++++++++++++----- 3 files changed, 286 insertions(+), 106 deletions(-) diff --git a/TODO.md b/TODO.md index 3b964db..1ef2c5b 100644 --- a/TODO.md +++ b/TODO.md @@ -7,14 +7,21 @@ - [ ] TFSF Source - [ ] Gaussian Beam Source - [ ] Astig. Gauss Beam -- Material Data Fitting - - [ ] Data File Import - - [ ] DataFit Medium - Monitors - [x] EH Field - [x] Power Flux - [ ] Permittivity - [ ] Diffraction +- Tidy3D / Integration + - [ ] Exporter + - [ ] Combine + - [ ] Importer +- Sim Grid + - [ ] Sim Grid + - [ ] Auto + - [ ] Manual + - [ ] Uniform + - [ ] Data - Structures - [ ] Cylinder - [ ] Cylinder Array @@ -23,22 +30,22 @@ - [ ] FCC Lattice - [ ] BCC Lattice - [ ] Monkey -- Sim Grid - - [ ] Sim Grid - - [ ] Auto - - [ ] Manual - - [ ] Uniform - - [ ] Data +- Expr Socket + - [ ] Array Mode +- Math Nodes + - [ ] Reduce Math + - [ ] Transform Math - reindex freq->wl +- Material Data Fitting + - [ ] Data File Import + - [ ] DataFit Medium - Mediums + - [ ] Non-Linearities - [ ] PEC Medium - [ ] Isotropic Medium - [ ] Sellmeier Medium - [ ] Drude Medium - [ ] Debye Medium - [ ] Anisotropic Medium -- Tidy3D - - [ ] Exporter - - [ ] Importer - Integration - [ ] Simulation and Analysis of Maxim's Cavity - Constants @@ -62,15 +69,15 @@ - [ ] Pol SocketType: 2D elliptical visualization of Jones vectors. - [ ] Pol SocketType: 3D Poincare sphere visualization of Stokes vectors. +- [x] Math / Operate Math + - [ ] Remove two-layered dropdown; directly filter operations and use categories to seperate them. + - [ ] Implement Expr socket advancements to make a better experience operating between random expression-like sockets. - [x] Math / Map Math - [x] Remove "By x" socket set let socket sets only be "Function"/"Expr"; then add a dynamic enum underneath to select "By x" based on data support. - [ ] Filter the operations based on data support, ex. use positive-definiteness to guide cholesky. - [ ] Implement support for additional symbols via `Expr`. - [x] Math / Filter Math - [ ] Math / Reduce Math -- [x] Math / Operate Math - - [ ] Remove two-layered dropdown; directly filter operations and use categories to seperate them. - - [ ] Implement Expr socket advancements to make a better experience operating between random expression-like sockets. ## Inputs - [x] Wave Constant @@ -156,8 +163,7 @@ ## Mediums - [x] Library Medium - - [ ] Implement frequency range output (listy), perhaps in the `InfoFlow` lane? - - [ ] Implement dynamic label. + - [ ] Implement wavelength-based plot, as opposed to merely the frequency plot. - [ ] DataFit Medium - [ ] Implement by migrating the material data fitting logic from the `Tidy3D File Importer`, except now only accept a `Data` input socket, and rely on the `Data File Importer` to do the parsing into an acceptable `Data` socket format. - [ ] Save the result in the node, specifically in a property (serialized!) and lock the input graph while saved. diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py index aa89f84..739465e 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py @@ -2,7 +2,7 @@ from . import ( analysis, bounds, inputs, - # mediums, + mediums, monitors, outputs, # simulations, @@ -16,7 +16,7 @@ BL_REGISTER = [ *inputs.BL_REGISTER, *outputs.BL_REGISTER, # *sources.BL_REGISTER, - # *mediums.BL_REGISTER, + *mediums.BL_REGISTER, # *structures.BL_REGISTER, *bounds.BL_REGISTER, *monitors.BL_REGISTER, @@ -28,7 +28,7 @@ BL_NODES = { **inputs.BL_NODES, **outputs.BL_NODES, # **sources.BL_NODES, - # **mediums.BL_NODES, + **mediums.BL_NODES, # **structures.BL_NODES, **bounds.BL_NODES, **monitors.BL_NODES, diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/library_medium.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/library_medium.py index 7b2323e..3e499a2 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/library_medium.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/mediums/library_medium.py @@ -1,17 +1,89 @@ +import enum +import typing as typ import bpy -import scipy as sc import sympy as sp import sympy.physics.units as spu import tidy3d as td +from tidy3d.material_library.material_library import MaterialItem as Tidy3DMediumItem +from tidy3d.material_library.material_library import VariantItem as Tidy3DMediumVariant -from blender_maxwell.utils import extra_sympy_units as spuex +from blender_maxwell.utils import bl_cache, sci_constants +from blender_maxwell.utils import extra_sympy_units as spux from ... import contracts as ct from ... import managed_objs, sockets from .. import base, events -VAC_SPEED_OF_LIGHT = sc.constants.speed_of_light * spu.meter / spu.second +_mat_lib_iter = iter(td.material_library) +_mat_key = '' + + +class VendoredMedium(enum.StrEnum): + # Declare StrEnum of All Tidy3D Mediums + ## -> This is a 'for ... in ...', which uses globals as loop variables. + ## -> It's a bit of a hack, but very effective. + while True: + try: + globals()['_mat_key'] = next(_mat_lib_iter) + except StopIteration: + break + + ## -> Exclude graphene. Graphene is special. + if _mat_key != 'graphene': + locals()[_mat_key] = _mat_key + + @staticmethod + def to_name(v: typ.Self) -> str: + return td.material_library[v].name + + @staticmethod + def to_icon(_: typ.Self) -> str: + return '' + + #################### + # - Medium Properties + #################### + @property + def tidy3d_medium_item(self) -> Tidy3DMediumItem: + """Extracts the Tidy3D "Medium Item", which encapsulates all the provided experimental variants.""" + return td.material_library[self] + + #################### + # - Medium Variant Properties + #################### + @property + def medium_variants(self) -> set[Tidy3DMediumVariant]: + """Extracts the list of medium variants, each corresponding to a particular experiment in the literature.""" + return self.tidy3d_medium_item.variants + + @property + def default_medium_variant(self) -> Tidy3DMediumVariant: + """Extracts the "default" medium variant, as selected by Tidy3D.""" + return self.medium_variants[self.tidy3d_medium_item.default] + + #################### + # - Enum Helper + #################### + @property + def variants_as_bl_enum_elements(self) -> list[ct.BLEnumElement]: + """Computes a list of variants in a format suitable for use in a dynamic `EnumProperty`. + + Notes: + This `EnumProperty` will only return a string `variant_name`. + + To reconstruct the actual `Tidy3DMediumVariant` object, one must therefore access it via the `vendored_medium.medium_variants[variant_name]`. + """ + return [ + ( + variant_name, + variant_name, + ' | '.join([ref.journal for ref in variant.reference]), + '', + i, + ) + for i, (variant_name, variant) in enumerate(self.medium_variants.items()) + ] class LibraryMediumNode(base.MaxwellSimNode): @@ -21,124 +93,226 @@ class LibraryMediumNode(base.MaxwellSimNode): #################### # - Sockets #################### - input_sockets = {} - output_sockets = { + input_sockets: typ.ClassVar = { + 'Generated Steps': sockets.ExprSocketDef( + mathtype=spux.MathType.Integer, default_value=2, abs_min=2 + ) + } + output_sockets: typ.ClassVar = { 'Medium': sockets.MaxwellMediumSocketDef(), + 'Valid Freqs': sockets.ExprSocketDef( + active_kind=ct.FlowKind.LazyArrayRange, + physical_type=spux.PhysicalType.Freq, + ), + 'Valid WLs': sockets.ExprSocketDef( + active_kind=ct.FlowKind.LazyArrayRange, + physical_type=spux.PhysicalType.Length, + ), } - managed_obj_types = { - 'nk_plot': managed_objs.ManagedBLImage, + managed_obj_types: typ.ClassVar = { + 'plot': managed_objs.ManagedBLImage, } #################### # - Properties #################### - material: bpy.props.EnumProperty( - name='', - description='', - # icon="NODE_MATERIAL", - items=[ - ( - mat_key, - td.material_library[mat_key].name, - ', '.join( - [ - ref.journal - for ref in td.material_library[mat_key] - .variants[td.material_library[mat_key].default] - .reference - ] - ), - ) - for mat_key in td.material_library - if mat_key != 'graphene' ## For some reason, it's unique... - ], - default='Au', - update=(lambda self, context: self.on_prop_changed('material', context)), + vendored_medium: VendoredMedium = bl_cache.BLField(VendoredMedium.Au, prop_ui=True) + variant_name: enum.Enum = bl_cache.BLField( + prop_ui=True, enum_cb=lambda self, _: self.search_variants() ) + def search_variants(self) -> list[ct.BLEnumElement]: + """Search for all valid variant of the current `self.vendored_medium`.""" + return self.vendored_medium.variants_as_bl_enum_elements + + #################### + # - Computed + #################### @property - def freq_range_str(self) -> tuple[sp.Expr, sp.Expr]: - ## TODO: Cache (node instances don't seem able to keep data outside of properties, not even cached_property) - mat = td.material_library[self.material] - freq_range = [ - spu.convert_to( - val * spu.hertz, - spuex.terahertz, - ) - / spuex.terahertz - for val in mat.medium.frequency_range - ] - return sp.pretty([freq_range[0].n(4), freq_range[1].n(4)], use_unicode=True) + def variant(self) -> Tidy3DMediumVariant: + """Deduce the actual medium variant from `self.vendored_medium` and `self.variant_name`.""" + return self.vendored_medium.medium_variants[self.variant_name] @property - def nm_range_str(self) -> str: - ## TODO: Cache (node instances don't seem able to keep data outside of properties, not even cached_property) - mat = td.material_library[self.material] - nm_range = [ - spu.convert_to( - VAC_SPEED_OF_LIGHT / (val * spu.hertz), - spu.nanometer, - ) - / spu.nanometer - for val in reversed(mat.medium.frequency_range) - ] - return sp.pretty([nm_range[0].n(4), nm_range[1].n(4)], use_unicode=True) + def medium(self) -> td.PoleResidue: + """Deduce the actual currently selected `PoleResidue` medium from `self.variant`.""" + return self.variant.medium + + @property + def data_url(self) -> str | None: + """Deduce the URL associated with the currently selected medium from `self.variant`.""" + return self.variant.data_url + + @property + def references(self) -> td.PoleResidue: + """Deduce the references associated with the currently selected `PoleResidue` medium from `self.variant`.""" + return self.variant.reference + + @property + def freq_range(self) -> spux.SympyExpr: + """Deduce the frequency range as a unit-aware (THz, for convenience) column vector. + + A rational approximation to each frequency bound is computed with `sp.nsimplify`, in order to **guarantee** lack of precision-loss as computations are performed on the frequency. + + """ + return spu.convert_to( + sp.Matrix([sp.nsimplify(el) for el in self.medium.frequency_range]) + * spu.hertz, + spux.terahertz, + ) + + @property + def wl_range(self) -> spux.SympyExpr: + """Deduce the vacuum wavelength range as a unit-aware (nanometer, for convenience) column vector.""" + return sp.Matrix( + self.freq_range.applyfunc( + lambda el: spu.convert_to( + sci_constants.vac_speed_of_light / el, spu.nanometer + ) + )[::-1] + ) + + #################### + # - Cached UI Properties + #################### + @staticmethod + def _ui_range_format(sp_number: spux.SympyExpr, e_not_limit: int = 6): + if sp_number.is_infinite: + return sp.pretty(sp_number, use_unicode=True) + + number = float(sp_number.subs({spux.THz: 1, spu.nm: 1})) + formatted_str = f'{number:.2f}' + if len(formatted_str) > e_not_limit: + formatted_str = f'{number:.2e}' + return formatted_str + + @bl_cache.cached_bl_property() + def ui_freq_range(self) -> tuple[str, str]: + """Cached mirror of `self.wl_range` which contains UI-ready strings.""" + return tuple([self._ui_range_format(el) for el in self.freq_range]) + + @bl_cache.cached_bl_property() + def ui_wl_range(self) -> tuple[str, str]: + """Cached mirror of `self.wl_range` which contains UI-ready strings.""" + return tuple([self._ui_range_format(el) for el in self.wl_range]) #################### # - UI #################### - def draw_props(self, context, layout): - layout.prop(self, 'material', text='') + def draw_label(self) -> str: + return f'Medium: {self.vendored_medium}' - def draw_info(self, context, col): - # UI Drawing - split = col.split(factor=0.23, align=True) + def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None: + layout.prop(self, self.blfields['vendored_medium'], text='') + layout.prop(self, self.blfields['variant_name'], text='') - _col = split.column(align=True) - _col.alignment = 'LEFT' - _col.label(text='nm') - _col.label(text='THz') + def draw_info(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None: + box = col.box() - _col = split.column(align=True) - _col.alignment = 'RIGHT' - _col.label(text=self.nm_range_str) - _col.label(text=self.freq_range_str) + row = box.row(align=True) + row.alignment = 'CENTER' + row.label(text='min|max') + + grid = box.grid_flow(row_major=True, columns=2, align=True) + grid.label(text='λ Range') + grid.label(text='𝑓 Range') + + grid.label(text=self.ui_wl_range[0]) + grid.label(text=self.ui_freq_range[0]) + grid.label(text=self.ui_wl_range[1]) + grid.label(text=self.ui_freq_range[1]) + + # URL Link + if self.data_url is not None: + box.operator('wm.url_open', text='Link to Data').url = self.data_url #################### - # - Output Sockets + # - Events #################### - @events.computes_output_socket('Medium') - def compute_vac_wl(self) -> sp.Expr: - return td.material_library[self.material].medium + @events.on_value_changed( + prop_name={'vendored_medium', 'variant_name'}, + run_on_init=True, + props={'vendored_medium'}, + ) + def on_medium_changed(self, props): + if self.variant_name not in props['vendored_medium'].medium_variants: + self.variant_name = bl_cache.Signal.ResetEnumItems + + self.ui_freq_range = bl_cache.Signal.InvalidateCache + self.ui_wl_range = bl_cache.Signal.InvalidateCache #################### - # - Event Callbacks + # - Output + #################### + @events.computes_output_socket( + 'Medium', + props={'medium'}, + ) + def compute_medium(self, props) -> sp.Expr: + return props['medium'] + + @events.computes_output_socket( + 'Valid Freqs', + props={'freq_range'}, + ) + def compute_valid_freqs(self, props) -> sp.Expr: + return props['freq_range'] + + @events.computes_output_socket( + 'Valid Freqs', + kind=ct.FlowKind.LazyArrayRange, + props={'freq_range'}, + input_sockets={'Generated Steps'}, + ) + def compute_valid_freqs_lazy(self, props, input_sockets) -> sp.Expr: + return ct.LazyArrayRangeFlow( + start=props['freq_range'][0] / spux.THz, + stop=props['freq_range'][1] / spux.THz, + steps=input_sockets['Generated Steps'], + scaling='lin', + unit=spux.THz, + ) + + @events.computes_output_socket( + 'Valid WLs', + props={'wl_range'}, + ) + def compute_valid_wls(self, props) -> sp.Expr: + return props['wl_range'] + + @events.computes_output_socket( + 'Valid WLs', + kind=ct.FlowKind.LazyArrayRange, + props={'wl_range'}, + input_sockets={'Generated Steps'}, + ) + def compute_valid_wls_lazy(self, props, input_sockets) -> sp.Expr: + return ct.LazyArrayRangeFlow( + start=props['wl_range'][0] / spu.nm, + stop=props['wl_range'][0] / spu.nm, + steps=input_sockets['Generated Steps'], + scaling='lin', + unit=spu.nm, + ) + + #################### + # - Preview #################### @events.on_show_plot( - managed_objs={'nk_plot'}, + managed_objs={'plot'}, props={'material'}, - stop_propagation=True, ## Plot only the first plottable node + stop_propagation=True, ) def on_show_plot( self, managed_objs: dict, - props: dict, ): - medium = td.material_library[props['material']].medium - freq_range = [ - spu.convert_to( - val * spu.hertz, - spuex.terahertz, - ) - / spu.hertz - for val in medium.frequency_range - ] - - managed_objs['nk_plot'].mpl_plot_to_image( - lambda ax: medium.plot(medium.frequency_range, ax=ax), + managed_objs['plot'].mpl_plot_to_image( + lambda ax: self.medium.plot(self.medium.frequency_range, ax=ax), bl_select=True, ) + ## TODO: Plot based on Wl, not freq. ####################