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
|
||||
- Implement Material Import for Maxim Data
|
||||
- [x] Implement Material Import for Maxim Data
|
||||
- Move preview GN trees to the asset library.
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {
|
||||
'.hdf5.gz',
|
||||
'.hdf5',
|
||||
'.json',
|
||||
'.yaml',
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue