feat: Added Tidy3D file import node

main
Sofus Albert Høgsbro Rose 2024-04-08 10:50:47 +02:00
parent 627457ff40
commit fd6df15b62
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
14 changed files with 412 additions and 29 deletions

View File

@ -0,0 +1,3 @@
*.html
*.pdf
*_files/

View File

@ -0,0 +1,7 @@
title: Interlinks
author: Michael Chow
version: 1.1.0
quarto-required: ">=1.2.0"
contributes:
filters:
- interlinks.lua

View File

@ -0,0 +1,254 @@
local function read_inv_text(filename)
-- read file
local file = io.open(filename, "r")
if file == nil then
return nil
end
local str = file:read("a")
file:close()
local project = str:match("# Project: (%S+)")
local version = str:match("# Version: (%S+)")
local data = {project = project, version = version, items = {}}
local ptn_data =
"^" ..
"(.-)%s+" .. -- name
"([%S:]-):" .. -- domain
"([%S]+)%s+" .. -- role
"(%-?%d+)%s+" .. -- priority
"(%S*)%s+" .. -- uri
"(.-)\r?$" -- dispname
-- Iterate through each line in the file content
for line in str:gmatch("[^\r\n]+") do
if not line:match("^#") then
-- Match each line against the pattern
local name, domain, role, priority, uri, dispName = line:match(ptn_data)
-- if name is nil, raise an error
if name == nil then
error("Error parsing line: " .. line)
end
data.items[#data.items + 1] = {
name = name,
domain = domain,
role = role,
priority = priority,
uri = uri,
dispName = dispName
}
end
end
return data
end
local function read_json(filename)
local file = io.open(filename, "r")
if file == nil then
return nil
end
local str = file:read("a")
file:close()
local decoded = quarto.json.decode(str)
return decoded
end
local function read_inv_text_or_json(base_name)
local file = io.open(base_name .. ".txt", "r")
if file then
-- TODO: refactors so we don't just close the file immediately
io.close(file)
json = read_inv_text(base_name .. ".txt")
else
json = read_json(base_name .. ".json")
end
return json
end
local inventory = {}
local function lookup(search_object)
local results = {}
for _, inv in ipairs(inventory) do
for _, item in ipairs(inv.items) do
-- e.g. :external+<inv_name>:<domain>:<role>:`<name>`
if item.inv_name and item.inv_name ~= search_object.inv_name then
goto continue
end
if item.name ~= search_object.name then
goto continue
end
if search_object.role and item.role ~= search_object.role then
goto continue
end
if search_object.domain and item.domain ~= search_object.domain then
goto continue
else
if search_object.domain or item.domain == "py" then
table.insert(results, item)
end
goto continue
end
::continue::
end
end
if #results == 1 then
return results[1]
end
if #results > 1 then
quarto.log.warning("Found multiple matches for " .. search_object.name .. ", using the first match.")
return results[1]
end
if #results == 0 then
quarto.log.warning("Found no matches for object:\n", search_object)
end
return nil
end
local function mysplit (inputstr, sep)
if sep == nil then
sep = "%s"
end
local t={}
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
table.insert(t, str)
end
return t
end
local function normalize_role(role)
if role == "func" then
return "function"
end
return role
end
local function build_search_object(str)
local starts_with_colon = str:sub(1, 1) == ":"
local search = {}
if starts_with_colon then
local t = mysplit(str, ":")
if #t == 2 then
-- e.g. :py:func:`my_func`
search.role = normalize_role(t[1])
search.name = t[2]:match("%%60(.*)%%60")
elseif #t == 3 then
-- e.g. :py:func:`my_func`
search.domain = t[1]
search.role = normalize_role(t[2])
search.name = t[3]:match("%%60(.*)%%60")
elseif #t == 4 then
-- e.g. :ext+inv:py:func:`my_func`
search.external = true
search.inv_name = t[1]:match("external%+(.*)")
search.domain = t[2]
search.role = normalize_role(t[3])
search.name = t[4]:match("%%60(.*)%%60")
else
quarto.log.warning("couldn't parse this link: " .. str)
return {}
end
else
search.name = str:match("%%60(.*)%%60")
end
if search.name == nil then
quarto.log.warning("couldn't parse this link: " .. str)
return {}
end
if search.name:sub(1, 1) == "~" then
search.shortened = true
search.name = search.name:sub(2, -1)
end
return search
end
local function report_broken_link(link, search_object, replacement)
-- TODO: how to unescape html elements like [?
return pandoc.Code(pandoc.utils.stringify(link.content))
end
function Link(link)
-- do not process regular links ----
if not link.target:match("%%60") then
return link
end
-- lookup item ----
local search = build_search_object(link.target)
local item = lookup(search)
-- determine replacement, used if no link text specified ----
local original_text = pandoc.utils.stringify(link.content)
local replacement = search.name
if search.shortened then
local t = mysplit(search.name, ".")
replacement = t[#t]
end
-- set link text ----
if original_text == "" and replacement ~= nil then
link.content = pandoc.Code(replacement)
end
-- report broken links ----
if item == nil then
return report_broken_link(link, search)
end
link.target = item.uri:gsub("%$$", search.name)
return link
end
local function fixup_json(json, prefix)
for _, item in ipairs(json.items) do
item.uri = prefix .. item.uri
end
table.insert(inventory, json)
end
return {
{
Meta = function(meta)
local json
local prefix
if meta.interlinks and meta.interlinks.sources then
for k, v in pairs(meta.interlinks.sources) do
local base_name = quarto.project.offset .. "/_inv/" .. k .. "_objects"
json = read_inv_text_or_json(base_name)
prefix = pandoc.utils.stringify(v.url)
if json ~= nil then
fixup_json(json, prefix)
end
end
end
json = read_inv_text_or_json(quarto.project.offset .. "/objects")
if json ~= nil then
fixup_json(json, "/")
end
end
},
{
Link = Link
}
}

1
doc/index.qmd 100644
View File

@ -0,0 +1 @@
The root of the website.

View File

@ -204,7 +204,6 @@ def _writable_bl_socket_value(
unit_system: dict | None = None, unit_system: dict | None = None,
allow_unit_not_in_unit_system: bool = False, allow_unit_not_in_unit_system: bool = False,
) -> typ.Any: ) -> typ.Any:
log.debug('Writing BL Socket Value (%s)', str(value))
socket_type = _socket_type_from_bl_socket(description, bl_socket_type) socket_type = _socket_type_from_bl_socket(description, bl_socket_type)
# Retrieve Unit-System Unit # Retrieve Unit-System Unit

View File

@ -16,9 +16,12 @@ class NodeType(BlenderTypeEnum):
## Inputs / Scene ## Inputs / Scene
Time = enum.auto() Time = enum.auto()
## Inputs / Importers ## Inputs / Web Importers
Tidy3DWebImporter = enum.auto() Tidy3DWebImporter = enum.auto()
## Inputs / File Importers
Tidy3DFileImporter = enum.auto()
## Inputs / Parameters ## Inputs / Parameters
NumberParameter = enum.auto() NumberParameter = enum.auto()
PhysicalParameter = enum.auto() PhysicalParameter = enum.auto()
@ -34,8 +37,6 @@ class NodeType(BlenderTypeEnum):
RealList = enum.auto() RealList = enum.auto()
ComplexList = enum.auto() ComplexList = enum.auto()
## Inputs /
InputFile = enum.auto()
# Outputs # Outputs
## Outputs / Viewers ## Outputs / Viewers

View File

@ -1,8 +1,9 @@
from . import ( from . import (
constants, constants,
unit_system, file_importers,
wave_constant, unit_system,
web_importers, wave_constant,
web_importers,
) )
# from . import file_importers # from . import file_importers
@ -12,12 +13,12 @@ BL_REGISTER = [
*unit_system.BL_REGISTER, *unit_system.BL_REGISTER,
*constants.BL_REGISTER, *constants.BL_REGISTER,
*web_importers.BL_REGISTER, *web_importers.BL_REGISTER,
# *file_importers.BL_REGISTER, *file_importers.BL_REGISTER,
] ]
BL_NODES = { BL_NODES = {
**wave_constant.BL_NODES, **wave_constant.BL_NODES,
**unit_system.BL_NODES, **unit_system.BL_NODES,
**constants.BL_NODES, **constants.BL_NODES,
**web_importers.BL_NODES, **web_importers.BL_NODES,
# *file_importers.BL_REGISTER, **file_importers.BL_NODES,
} }

View File

@ -0,0 +1,8 @@
from . import tidy_3d_file_importer
BL_REGISTER = [
*tidy_3d_file_importer.BL_REGISTER,
]
BL_NODES = {
**tidy_3d_file_importer.BL_NODES,
}

View File

@ -0,0 +1,113 @@
import typing as typ
from pathlib import Path
import bpy
import tidy3d as td
from ......utils import logger
from .... import contracts as ct
from .... import sockets
from ... import base, events
log = logger.get(__name__)
TD_FILE_EXTS = {
'.hdf5.gz',
'.hdf5',
'.json',
'.yaml',
}
####################
# - Node
####################
class Tidy3DFileImporterNode(base.MaxwellSimNode):
node_type = ct.NodeType.Tidy3DFileImporter
bl_label = 'Tidy3D File Importer'
input_sockets: typ.ClassVar = {
'File Path': sockets.FilePathSocketDef(),
}
tidy3d_type: bpy.props.EnumProperty(
name='Tidy3D Type',
description='Type of Tidy3D object to load',
items=[
(
'SIMULATION_DATA',
'Simulation Data',
'Data from Completed Tidy3D Simulation',
),
('SIMULATION', 'Simulation', 'Tidy3D Simulation'),
('MEDIUM', 'Medium', 'A Tidy3D Medium'),
],
default='SIMULATION_DATA',
update=lambda self, context: self.sync_prop('tidy3d_type', context),
)
####################
# - Event Methods: Compute Output Data
####################
def _compute_sim_data_for(
self, output_socket_name: str, file_path: Path
) -> td.components.base.Tidy3dBaseModel:
return {
'Sim': td.Simulation,
'Sim Data': td.SimulationData,
'Medium': td.Medium,
}[output_socket_name].from_file(str(file_path))
@events.computes_output_socket(
'Sim',
input_sockets={'File Path'},
)
def compute_sim(self, input_sockets: dict) -> td.Simulation:
return self._compute_sim_data_for('Sim', input_sockets['File Path'])
@events.computes_output_socket(
'Sim Data',
input_sockets={'File Path'},
)
def compute_sim_data(self, input_sockets: dict) -> td.SimulationData:
return self._compute_sim_data_for('Sim Data', input_sockets['File Path'])
@events.computes_output_socket(
'Medium',
input_sockets={'File Path'},
)
def compute_medium(self, input_sockets: dict) -> td.Medium:
return self._compute_sim_data_for('Medium', input_sockets['File Path'])
####################
# - Event Methods: Setup Output Socket
####################
@events.on_value_changed(
socket_name='File Path',
prop_name='tidy3d_type',
input_sockets={'File Path'},
props={'tidy3d_type'},
)
def on_file_changed(self, input_sockets: dict, props: dict):
file_ext = ''.join(input_sockets['File Path'].suffixes)
if not (input_sockets['File Path'].is_file() and file_ext in TD_FILE_EXTS):
self.loose_output_sockets = {}
else:
self.loose_output_sockets = {
'SIMULATION_DATA': {
'Sim Data': sockets.MaxwellFDTDSimSocketDef(),
},
'SIMULATION': {'Sim': sockets.MaxwellFDTDSimDataSocketDef()},
'MEDIUM': {'Medium': sockets.MaxwellMediumSocketDef()},
}[props['tidy3d_type']]
####################
# - Blender Registration
####################
BL_REGISTER = [
Tidy3DFileImporterNode,
]
BL_NODES = {
ct.NodeType.Tidy3DFileImporter: (ct.NodeCategory.MAXWELLSIM_INPUTS_IMPORTERS)
}

View File

@ -112,15 +112,6 @@ class ViewerNode(base.MaxwellSimNode):
# - Methods # - Methods
#################### ####################
def print_data_to_console(self): def print_data_to_console(self):
import sys
for module_name, module in sys.modules.copy().items():
if module_name == '__mp_main__':
print(
'Anything, even repr(), with this module just crashes:', module_name
)
print(module) ## Crash
if not self.inputs['Data'].is_linked: if not self.inputs['Data'].is_linked:
return return

View File

@ -51,7 +51,7 @@ class SimDomainNode(base.MaxwellSimNode):
'Size': 'Tidy3DUnits', 'Size': 'Tidy3DUnits',
}, },
) )
def compute_output(self, input_sockets: dict) -> sp.Expr: def compute_domain(self, input_sockets: dict) -> sp.Expr:
return { return {
'run_time': input_sockets['Duration'], 'run_time': input_sockets['Duration'],
'center': input_sockets['Center'], 'center': input_sockets['Center'],

View File

@ -42,9 +42,9 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
@events.computes_output_socket( @events.computes_output_socket(
'Structure', 'Structure',
input_sockets={'Medium'}, input_sockets={'Medium'},
managed_objs={'geometry'}, managed_objs={'mesh'},
) )
def compute_output( def compute_structure(
self, self,
input_sockets: dict[str, typ.Any], input_sockets: dict[str, typ.Any],
managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs: dict[str, ct.schemas.ManagedObj],

View File

@ -50,7 +50,7 @@ class BoxStructureNode(base.MaxwellSimNode):
'Size': 'Tidy3DUnits', 'Size': 'Tidy3DUnits',
}, },
) )
def compute_output(self, input_sockets: dict, unit_systems: dict) -> td.Box: def compute_structure(self, input_sockets: dict, unit_systems: dict) -> td.Box:
return td.Structure( return td.Structure(
geometry=td.Box( geometry=td.Box(
center=input_sockets['Center'], center=input_sockets['Center'],

View File

@ -6,8 +6,11 @@ import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
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__)
class MaxwellSimSocket(bpy.types.NodeSocket): class MaxwellSimSocket(bpy.types.NodeSocket):
# Fundamentals # Fundamentals
@ -280,10 +283,12 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
self, self,
kind: ct.DataFlowKind = ct.DataFlowKind.Value, kind: ct.DataFlowKind = ct.DataFlowKind.Value,
): ):
"""Computes the value of this socket, including all relevant factors: """Computes the value of this socket, including all relevant factors.
- If input socket, and unlinked, compute internal data.
- If input socket, and linked, compute linked socket data. Notes:
- If output socket, ask node for data. - If input socket, and unlinked, compute internal data.
- If input socket, and linked, compute linked socket data.
- If output socket, ask node for data.
""" """
# Compute Output Socket # Compute Output Socket
## List-like sockets guarantee that a list of a thing is passed. ## List-like sockets guarantee that a list of a thing is passed.
@ -339,11 +344,11 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
if value == unit_sympy if value == unit_sympy
] ]
if len(matching_unit_names) == 0: if len(matching_unit_names) == 0:
msg = f"Tried to set unit for socket {self} with value {value}, but it is not one of possible units {''.join(possible.units.values())} for this socket (as defined in `contracts.SOCKET_UNITS`)" msg = f"Tried to set unit for socket {self} with value {value}, but it is not one of possible units {''.join(self.possible_units.values())} for this socket (as defined in `contracts.SOCKET_UNITS`)"
raise ValueError(msg) raise ValueError(msg)
if len(matching_unit_names) > 1: if len(matching_unit_names) > 1:
msg = f"Tried to set unit for socket {self} with value {value}, but multiple possible matching units {''.join(possible.units.values())} for this socket (as defined in `contracts.SOCKET_UNITS`); there may only be one" msg = f"Tried to set unit for socket {self} with value {value}, but multiple possible matching units {''.join(self.possible_units.values())} for this socket (as defined in `contracts.SOCKET_UNITS`); there may only be one"
raise RuntimeError(msg) raise RuntimeError(msg)
self.active_unit = matching_unit_names[0] self.active_unit = matching_unit_names[0]