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 # Acute Tasks
- Implement Material Import for Maxim Data - [x] Implement Material Import for Maxim Data
- Move preview GN trees to the asset library. - 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 numpy as np
import typing_extensions as typx import typing_extensions as typx
from ....utils import logger
from .. import contracts as ct from .. import contracts as ct
log = logger.get(__name__)
AREA_TYPE = 'IMAGE_EDITOR' AREA_TYPE = 'IMAGE_EDITOR'
SPACE_TYPE = 'IMAGE_EDITOR' SPACE_TYPE = 'IMAGE_EDITOR'
@ -163,6 +166,7 @@ class ManagedBLImage(ct.schemas.ManagedObj):
# Compute Plot Dimensions # Compute Plot Dimensions
aspect_ratio = _width_inches / _height_inches 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 # Create MPL Figure, Axes, and Compute Figure Geometry
fig, ax = plt.subplots( fig, ax = plt.subplots(
figsize=[_width_inches, _height_inches], figsize=[_width_inches, _height_inches],
@ -171,6 +175,7 @@ class ManagedBLImage(ct.schemas.ManagedObj):
ax.set_aspect(aspect_ratio) ax.set_aspect(aspect_ratio)
cmp_width_px, cmp_height_px = fig.canvas.get_width_height() cmp_width_px, cmp_height_px = fig.canvas.get_width_height()
## Use computed pixel w/h to preempt off-by-one size errors. ## Use computed pixel w/h to preempt off-by-one size errors.
ax.set_aspect('auto') ## Workaround aspect-ratio bugs
# Plot w/User Parameter # Plot w/User Parameter
func_plotter(ax) func_plotter(ax)

View File

@ -3,21 +3,39 @@ from pathlib import Path
import bpy import bpy
import tidy3d as td import tidy3d as td
import tidy3d.plugins.dispersion as td_dispersion
from ......utils import logger from ......utils import logger
from .... import contracts as ct from .... import contracts as ct
from .... import sockets from .... import managed_objs, sockets
from ... import base, events from ... import base, events
log = logger.get(__name__) log = logger.get(__name__)
TD_FILE_EXTS = { VALID_FILE_EXTS = {
'.hdf5.gz', 'SIMULATION_DATA': {
'.hdf5', '.hdf5.gz',
'.json', '.hdf5',
'.yaml', },
'SIMULATION': {
'.hdf5.gz',
'.hdf5',
'.json',
'.yaml',
},
'MEDIUM': {
'.hdf5.gz',
'.hdf5',
'.json',
'.yaml',
},
'EXPERIM_DISP_MEDIUM': {
'.txt',
},
} }
CACHE = {}
#################### ####################
# - Node # - Node
@ -29,25 +47,69 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode):
input_sockets: typ.ClassVar = { input_sockets: typ.ClassVar = {
'File Path': sockets.FilePathSocketDef(), '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( tidy3d_type: bpy.props.EnumProperty(
name='Tidy3D Type', name='Tidy3D Type',
description='Type of Tidy3D object to load', description='Type of Tidy3D object to load',
items=[ items=[
( (
'SIMULATION_DATA', 'SIMULATION_DATA',
'Simulation Data', 'Sim Data',
'Data from Completed Tidy3D Simulation', 'Data from Completed Tidy3D Simulation',
), ),
('SIMULATION', 'Simulation', 'Tidy3D Simulation'), ('SIMULATION', 'Sim', 'Tidy3D Simulation'),
('MEDIUM', 'Medium', 'A Tidy3D Medium'), ('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', default='SIMULATION_DATA',
update=lambda self, context: self.sync_prop('tidy3d_type', context), 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( def _compute_sim_data_for(
self, output_socket_name: str, file_path: Path 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: def compute_medium(self, input_sockets: dict) -> td.Medium:
return self._compute_sim_data_for('Medium', input_sockets['File Path']) 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 # - Event Methods: Setup Output Socket
#################### ####################
@ -89,8 +184,14 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode):
props={'tidy3d_type'}, props={'tidy3d_type'},
) )
def on_file_changed(self, input_sockets: dict, props: dict): 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) 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 = {} self.loose_output_sockets = {}
else: else:
self.loose_output_sockets = { self.loose_output_sockets = {
@ -99,8 +200,43 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode):
}, },
'SIMULATION': {'Sim': sockets.MaxwellFDTDSimDataSocketDef()}, 'SIMULATION': {'Sim': sockets.MaxwellFDTDSimDataSocketDef()},
'MEDIUM': {'Medium': sockets.MaxwellMediumSocketDef()}, 'MEDIUM': {'Medium': sockets.MaxwellMediumSocketDef()},
'EXPERIM_DISP_MEDIUM': {
'Experim Disp Medium': sockets.MaxwellMediumSocketDef()
},
}[props['tidy3d_type']] }[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 # - Blender Registration

View File

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