feat: Implemented fit of experim. medium data.

It's only good for dispersive media; specifically, a text file with
three floats per line: 'wl n k'.
A custom script was used to convert Maxim's data.

It's very fast, and has a ton of options.
Only the most important are exposed in the node for now.

A bug in MPL plotting aspect ratio declaration on MPL axis object was
fixed by manually running `set_aspect('auto')` after the fact.
It shouldn't do anything, and it doesn't, other than fix the bug :)

Also brought the plotting function of the viewer to parity with
the 3D preview, so the "Auto Plot" button works as expected.
main
Sofus Albert Høgsbro Rose 2024-04-08 12:51:09 +02:00
parent fd6df15b62
commit 54dc46290a
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
4 changed files with 158 additions and 21 deletions

View File

@ -1,5 +1,5 @@
# Acute Tasks
- Implement Material Import for Maxim Data
- [x] Implement Material Import for Maxim Data
- Move preview GN trees to the asset library.

View File

@ -6,8 +6,11 @@ import matplotlib.axis as mpl_ax
import numpy as np
import typing_extensions as typx
from ....utils import logger
from .. import contracts as ct
log = logger.get(__name__)
AREA_TYPE = 'IMAGE_EDITOR'
SPACE_TYPE = 'IMAGE_EDITOR'
@ -163,6 +166,7 @@ class ManagedBLImage(ct.schemas.ManagedObj):
# Compute Plot Dimensions
aspect_ratio = _width_inches / _height_inches
log.debug('Create MPL Axes (aspect=%d, width=%d, height=%d)', aspect_ratio, _width_inches, _height_inches)
# Create MPL Figure, Axes, and Compute Figure Geometry
fig, ax = plt.subplots(
figsize=[_width_inches, _height_inches],
@ -171,6 +175,7 @@ class ManagedBLImage(ct.schemas.ManagedObj):
ax.set_aspect(aspect_ratio)
cmp_width_px, cmp_height_px = fig.canvas.get_width_height()
## Use computed pixel w/h to preempt off-by-one size errors.
ax.set_aspect('auto') ## Workaround aspect-ratio bugs
# Plot w/User Parameter
func_plotter(ax)

View File

@ -3,21 +3,39 @@ from pathlib import Path
import bpy
import tidy3d as td
import tidy3d.plugins.dispersion as td_dispersion
from ......utils import logger
from .... import contracts as ct
from .... import sockets
from .... import managed_objs, sockets
from ... import base, events
log = logger.get(__name__)
TD_FILE_EXTS = {
VALID_FILE_EXTS = {
'SIMULATION_DATA': {
'.hdf5.gz',
'.hdf5',
},
'SIMULATION': {
'.hdf5.gz',
'.hdf5',
'.json',
'.yaml',
},
'MEDIUM': {
'.hdf5.gz',
'.hdf5',
'.json',
'.yaml',
},
'EXPERIM_DISP_MEDIUM': {
'.txt',
},
}
CACHE = {}
####################
# - Node
@ -29,25 +47,69 @@ 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='',
),
}
####################
# - Properties
####################
tidy3d_type: bpy.props.EnumProperty(
name='Tidy3D Type',
description='Type of Tidy3D object to load',
items=[
(
'SIMULATION_DATA',
'Simulation Data',
'Sim Data',
'Data from Completed Tidy3D Simulation',
),
('SIMULATION', 'Simulation', 'Tidy3D Simulation'),
('SIMULATION', 'Sim', 'Tidy3D Simulation'),
('MEDIUM', 'Medium', 'A Tidy3D Medium'),
(
'EXPERIM_DISP_MEDIUM',
'Experim Disp Medium',
'A pole-residue fit of experimental dispersive medium data, described by a .txt file specifying wl, n, k',
),
],
default='SIMULATION_DATA',
update=lambda self, context: self.sync_prop('tidy3d_type', context),
)
disp_fit__min_poles: bpy.props.IntProperty(
name='min Poles',
description='Min. # poles to fit to the experimental dispersive medium data',
default=1,
)
disp_fit__max_poles: bpy.props.IntProperty(
name='max Poles',
description='Max. # poles to fit to the experimental dispersive medium data',
default=5,
)
## TODO: Bool of whether to fit eps_inf, with conditional choice of eps_inf as socket
disp_fit__tolerance_rms: bpy.props.FloatProperty(
name='Max RMS',
description='The RMS error threshold, below which the fit should be considered converged',
default=0.001,
precision=5,
)
## TODO: "AdvanceFastFitterParam" options incl. loss_bounds, weights, show_progress, show_unweighted_rms, relaxed, smooth, logspacing, numiters, passivity_num_iters, and slsqp_constraint_scale
def draw_props(self, _: bpy.types.Context, col: bpy.types.UILayout):
col.prop(self, 'tidy3d_type', text='')
if self.tidy3d_type == 'EXPERIM_DISP_MEDIUM':
row = col.row(align=True)
row.alignment = 'CENTER'
row.label(text='Pole-Residue Fit')
col.prop(self, 'disp_fit__min_poles')
col.prop(self, 'disp_fit__max_poles')
col.prop(self, 'disp_fit__tolerance_rms')
####################
# - Event Methods: Compute Output Data
# - Event Methods: Output Data
####################
def _compute_sim_data_for(
self, output_socket_name: str, file_path: Path
@ -79,6 +141,39 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode):
def compute_medium(self, input_sockets: dict) -> td.Medium:
return self._compute_sim_data_for('Medium', input_sockets['File Path'])
####################
# - Event Methods: Output Data | Dispersive Media
####################
@events.computes_output_socket(
'Experim Disp Medium',
input_sockets={'File Path'},
)
def compute_experim_disp_medium(self, input_sockets: dict) -> td.Medium:
if CACHE.get(self.bl_label) is not None:
log.debug('Reusing Cached Dispersive Medium')
return CACHE[self.bl_label]['model']
log.info('Loading Experimental Data')
dispersion_fitter = td_dispersion.FastDispersionFitter.from_file(
str(input_sockets['File Path'])
)
log.info('Computing Fast Dispersive Fit of Experimental Data...')
pole_residue_medium, rms_error = dispersion_fitter.fit(
min_num_poles=self.disp_fit__min_poles,
max_num_poles=self.disp_fit__max_poles,
tolerance_rms=self.disp_fit__tolerance_rms,
)
log.info('Fit Succeeded w/RMS "%s"!', f'{rms_error:.5f}')
# Populate Cache
CACHE[self.bl_label] = {}
CACHE[self.bl_label]['model'] = pole_residue_medium
CACHE[self.bl_label]['fitter'] = dispersion_fitter
CACHE[self.bl_label]['rms_error'] = rms_error
return pole_residue_medium
####################
# - Event Methods: Setup Output Socket
####################
@ -89,8 +184,14 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode):
props={'tidy3d_type'},
)
def on_file_changed(self, input_sockets: dict, props: dict):
if CACHE.get(self.bl_label) is not None:
del CACHE[self.bl_label]
file_ext = ''.join(input_sockets['File Path'].suffixes)
if not (input_sockets['File Path'].is_file() and file_ext in TD_FILE_EXTS):
if not (
input_sockets['File Path'].is_file()
and file_ext in VALID_FILE_EXTS[props['tidy3d_type']]
):
self.loose_output_sockets = {}
else:
self.loose_output_sockets = {
@ -99,8 +200,43 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode):
},
'SIMULATION': {'Sim': sockets.MaxwellFDTDSimDataSocketDef()},
'MEDIUM': {'Medium': sockets.MaxwellMediumSocketDef()},
'EXPERIM_DISP_MEDIUM': {
'Experim Disp Medium': sockets.MaxwellMediumSocketDef()
},
}[props['tidy3d_type']]
####################
# - Event Methods: Plot
####################
@events.on_show_plot(
managed_objs={'plot'},
props={'tidy3d_type'},
)
def on_show_plot(
self,
props: dict,
managed_objs: dict,
):
"""When the filetype is 'Experimental Dispersive Medium', plot the computed model against the input data."""
if props['tidy3d_type'] == 'EXPERIM_DISP_MEDIUM':
# Populate Cache
if CACHE.get(self.bl_label) is None:
model_medium = self.compute_experim_disp_medium()
disp_fitter = CACHE[self.bl_label]['fitter']
else:
model_medium = CACHE[self.bl_label]['model']
disp_fitter = CACHE[self.bl_label]['fitter']
# Plot
log.debug(disp_fitter)
managed_objs['plot'].mpl_plot_to_image(
lambda ax: disp_fitter.plot(
medium=model_medium,
ax=ax,
),
bl_select=True,
)
####################
# - Blender Registration

View File

@ -23,7 +23,6 @@ class ConsoleViewOperator(bpy.types.Operator):
def execute(self, context):
node = context.node
print('Executing Operator')
node.print_data_to_console()
return {'FINISHED'}
@ -39,7 +38,7 @@ class RefreshPlotViewOperator(bpy.types.Operator):
def execute(self, context):
node = context.node
node.trigger_action('value_changed', 'Data')
node.on_changed_plot_preview()
return {'FINISHED'}
@ -67,7 +66,7 @@ class ViewerNode(base.MaxwellSimNode):
auto_3d_preview: bpy.props.BoolProperty(
name='Auto 3D Preview',
description="Whether to auto-preview anything 3D, that's plugged into the viewer node",
default=False,
default=True,
update=lambda self, context: self.sync_prop('auto_3d_preview', context),
)
@ -126,18 +125,14 @@ class ViewerNode(base.MaxwellSimNode):
# - Event Methods
####################
@events.on_value_changed(
socket_name='Data',
prop_name='auto_plot',
props={'auto_plot'},
)
def on_changed_2d_data(self, props):
# Show Plot
## Don't have to un-show other plots.
def on_changed_plot_preview(self, props):
if self.inputs['Data'].is_linked and props['auto_plot']:
log.info('Enabling 2D Plot from "%s"', self.name)
self.trigger_action('show_plot')
####################
# - Event Methods: 3D Preview
####################
@events.on_value_changed(
prop_name='auto_3d_preview',
props={'auto_3d_preview'},
@ -159,6 +154,7 @@ class ViewerNode(base.MaxwellSimNode):
# Just Linked / Just Unlinked: Preview/Unpreview
if self.inputs['Data'].is_linked ^ self.cache__data_socket_linked:
self.on_changed_3d_preview()
self.on_changed_plot_preview()
self.cache__data_socket_linked = self.inputs['Data'].is_linked