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
parent
fd6df15b62
commit
54dc46290a
2
TODO.md
2
TODO.md
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue