diff --git a/doc/_extensions/machow/interlinks/.gitignore b/doc/_extensions/machow/interlinks/.gitignore new file mode 100644 index 0000000..5a1bf0b --- /dev/null +++ b/doc/_extensions/machow/interlinks/.gitignore @@ -0,0 +1,3 @@ +*.html +*.pdf +*_files/ diff --git a/doc/_extensions/machow/interlinks/_extension.yml b/doc/_extensions/machow/interlinks/_extension.yml new file mode 100644 index 0000000..c8a8121 --- /dev/null +++ b/doc/_extensions/machow/interlinks/_extension.yml @@ -0,0 +1,7 @@ +title: Interlinks +author: Michael Chow +version: 1.1.0 +quarto-required: ">=1.2.0" +contributes: + filters: + - interlinks.lua diff --git a/doc/_extensions/machow/interlinks/interlinks.lua b/doc/_extensions/machow/interlinks/interlinks.lua new file mode 100644 index 0000000..47aa61f --- /dev/null +++ b/doc/_extensions/machow/interlinks/interlinks.lua @@ -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+:::`` + 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 + } +} diff --git a/doc/index.qmd b/doc/index.qmd new file mode 100644 index 0000000..85cb45f --- /dev/null +++ b/doc/index.qmd @@ -0,0 +1 @@ +The root of the website. diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/bl_socket_map.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/bl_socket_map.py index 26a8202..795f3c8 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/bl_socket_map.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/bl_socket_map.py @@ -204,7 +204,6 @@ def _writable_bl_socket_value( unit_system: dict | None = None, allow_unit_not_in_unit_system: bool = False, ) -> typ.Any: - log.debug('Writing BL Socket Value (%s)', str(value)) socket_type = _socket_type_from_bl_socket(description, bl_socket_type) # Retrieve Unit-System Unit diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py index 0883fc6..754cf23 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py @@ -16,9 +16,12 @@ class NodeType(BlenderTypeEnum): ## Inputs / Scene Time = enum.auto() - ## Inputs / Importers + ## Inputs / Web Importers Tidy3DWebImporter = enum.auto() + ## Inputs / File Importers + Tidy3DFileImporter = enum.auto() + ## Inputs / Parameters NumberParameter = enum.auto() PhysicalParameter = enum.auto() @@ -34,8 +37,6 @@ class NodeType(BlenderTypeEnum): RealList = enum.auto() ComplexList = enum.auto() - ## Inputs / - InputFile = enum.auto() # Outputs ## Outputs / Viewers diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/__init__.py index 7c4d237..53c55f2 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/__init__.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/__init__.py @@ -1,8 +1,9 @@ from . import ( - constants, - unit_system, - wave_constant, - web_importers, + constants, + file_importers, + unit_system, + wave_constant, + web_importers, ) # from . import file_importers @@ -12,12 +13,12 @@ BL_REGISTER = [ *unit_system.BL_REGISTER, *constants.BL_REGISTER, *web_importers.BL_REGISTER, - # *file_importers.BL_REGISTER, + *file_importers.BL_REGISTER, ] BL_NODES = { **wave_constant.BL_NODES, **unit_system.BL_NODES, **constants.BL_NODES, **web_importers.BL_NODES, - # *file_importers.BL_REGISTER, + **file_importers.BL_NODES, } diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/__init__.py new file mode 100644 index 0000000..5dae0f8 --- /dev/null +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/__init__.py @@ -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, +} diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/tidy_3d_file_importer.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/tidy_3d_file_importer.py new file mode 100644 index 0000000..c5b40d4 --- /dev/null +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/tidy_3d_file_importer.py @@ -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) +} diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py index 73b84a3..65897ff 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py @@ -112,15 +112,6 @@ class ViewerNode(base.MaxwellSimNode): # - Methods #################### 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: return diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py index 49917ff..813d924 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py @@ -51,7 +51,7 @@ class SimDomainNode(base.MaxwellSimNode): 'Size': 'Tidy3DUnits', }, ) - def compute_output(self, input_sockets: dict) -> sp.Expr: + def compute_domain(self, input_sockets: dict) -> sp.Expr: return { 'run_time': input_sockets['Duration'], 'center': input_sockets['Center'], diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py index 182029c..42a2d29 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py @@ -42,9 +42,9 @@ class GeoNodesStructureNode(base.MaxwellSimNode): @events.computes_output_socket( 'Structure', input_sockets={'Medium'}, - managed_objs={'geometry'}, + managed_objs={'mesh'}, ) - def compute_output( + def compute_structure( self, input_sockets: dict[str, typ.Any], managed_objs: dict[str, ct.schemas.ManagedObj], diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py index 9bfffd6..afb0b45 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py @@ -50,7 +50,7 @@ class BoxStructureNode(base.MaxwellSimNode): '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( geometry=td.Box( center=input_sockets['Center'], diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py index dd0fa90..deae400 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py @@ -6,8 +6,11 @@ import sympy as sp import sympy.physics.units as spu import typing_extensions as typx +from ....utils import logger from .. import contracts as ct +log = logger.get(__name__) + class MaxwellSimSocket(bpy.types.NodeSocket): # Fundamentals @@ -280,10 +283,12 @@ class MaxwellSimSocket(bpy.types.NodeSocket): self, kind: ct.DataFlowKind = ct.DataFlowKind.Value, ): - """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. - - If output socket, ask node for data. + """Computes the value of this socket, including all relevant factors. + + Notes: + - 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 ## 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 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) 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) self.active_unit = matching_unit_names[0]