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.
main
Sofus Albert Høgsbro Rose 2024-05-02 14:24:23 +02:00
parent 339ee0226d
commit 14b98d219e
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
3 changed files with 286 additions and 106 deletions

40
TODO.md
View File

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

View File

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

View File

@ -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
]
),
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()
)
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)),
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 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 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 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,
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]
)
/ spuex.terahertz
for val in mat.medium.frequency_range
]
return sp.pretty([freq_range[0].n(4), freq_range[1].n(4)], use_unicode=True)
@property
def nm_range_str(self) -> str:
## TODO: Cache (node instances don't seem able to keep data outside of properties, not even cached_property)
mat = td.material_library[self.material]
nm_range = [
spu.convert_to(
VAC_SPEED_OF_LIGHT / (val * spu.hertz),
spu.nanometer,
)
/ spu.nanometer
for val in reversed(mat.medium.frequency_range)
]
return sp.pretty([nm_range[0].n(4), nm_range[1].n(4)], use_unicode=True)
####################
# - 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.
####################