From 5b56162f1df2c87ea8f3fb44db5da219260037c7 Mon Sep 17 00:00:00 2001 From: tylerflex Date: Mon, 15 Nov 2021 09:52:30 -0800 Subject: [PATCH 1/3] data has attributes --- tests/test_material_library.py | 5 ++++- tidy3d/components/data.py | 41 +++++++++++++++++----------------- tidy3d/convert.py | 10 +++++---- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/tests/test_material_library.py b/tests/test_material_library.py index 36afc8a3cb..410d1196a8 100644 --- a/tests/test_material_library.py +++ b/tests/test_material_library.py @@ -8,6 +8,9 @@ def test_library(): """for each member of material library, ensure that it evaluates eps_model correctly""" for material_name, variants in material_library.items(): for _, variant in variants.items(): - fmin, fmax = variant.frequency_range + if variant.frequency_range: + fmin, fmax = variant.frequency_range + else: + fmin, fmax = 100e12, 300e12 freqs = np.linspace(fmin, fmax, 10011) eps_complex = variant.eps_model(freqs) diff --git a/tidy3d/components/data.py b/tidy3d/components/data.py index 5fa475e3fe..ca7fb56848 100644 --- a/tidy3d/components/data.py +++ b/tidy3d/components/data.py @@ -48,14 +48,14 @@ def decode_bytes_array(array_of_bytes: Numpy) -> List[str]: # mapping of data coordinates to units for assigning .attrs to the xarray objects -DIM_UNITS = { - 'x': 'um', - 'y': 'um', - 'z': 'um', - 'f': 'um', - 't': 'um', - 'direction': None, - 'mode_index': None, +DIM_ATTRS = { + "x": {"units": "um", "long_name": "x position"}, + "y": {"units": "um", "long_name": "y position"}, + "z": {"units": "um", "long_name": "z position"}, + "f": {"units": "Hz", "long_name": "frequeny"}, + "t": {"units": "sec", "long_name": "time"}, + "direction": {"units": None, "long_name": "propagation direction"}, + "mode_index": {"units": None, "long_name": "mode index"}, } @@ -63,6 +63,7 @@ def decode_bytes_array(array_of_bytes: Numpy) -> List[str]: # TODO: make this work for Dataset items, which get converted to xr.DataArray + class Tidy3dDataArray(xr.DataArray): """Subclass of xarray's DataArray that implements some custom functions.""" @@ -107,9 +108,9 @@ def load_from_group(cls, hdf5_grp): class MonitorData(Tidy3dData, ABC): """Abstract base class for objects storing individual data from simulation.""" - values : Union[Array[float], Array[complex]] - val_units : str = None - type : str = None + values: Union[Array[float], Array[complex]] + data_attrs: Dict[str, str] = None + type: str = None """ explanation of values `values` is a numpy array that stores the raw data associated with each @@ -151,11 +152,11 @@ def data(self) -> Tidy3dDataArray: coords = {dim: data_dict[dim] for dim in self._dims} data_array = Tidy3dDataArray(self.values, coords=coords) - # assign units - data_array.attrs = {'units': self.val_units} + # assign attrs for xarray + if self.data_attrs: + data_array.attrs = self.data_attrs for name, coord in data_array.coords.items(): - coord_units = DIM_UNITS.get(name) - coord[name].attrs = {'units': coord_units} + coord[name].attrs = DIM_ATTRS.get(name) return data_array @@ -348,7 +349,7 @@ class ScalarFieldData(AbstractScalarFieldData, FreqData): """ values: Array[complex] - val_units : str = '[E] = V/um, [H] = A/um' + data_attrs: Dict[str, str] = None # {'units': '[E] = V/um, [H] = A/um'} type: Literal["ScalarFieldData"] = "ScalarFieldData" _dims = ("x", "y", "z", "f") @@ -381,7 +382,7 @@ class ScalarFieldTimeData(AbstractScalarFieldData, TimeData): """ values: Array[float] - val_units : str = '[E] = V/um, [H] = A/um' + data_attrs: Dict[str, str] = None # {'units': '[E] = V/m, [H] = A/m'} type: Literal["ScalarFieldTimeData"] = "ScalarFieldTimeData" _dims = ("x", "y", "z", "t") @@ -434,7 +435,7 @@ class FluxData(AbstractFluxData, FreqData): """ values: Array[float] - val_units : str = 'W' + data_attrs: Dict[str, str] = {"units": "W", "long_name": "flux"} type: Literal["FluxData"] = "FluxData" _dims = ("f",) @@ -459,7 +460,7 @@ class FluxTimeData(AbstractFluxData, TimeData): """ values: Array[float] - val_units: str = 'W' + data_attrs: Dict[str, str] = {"units": "W", "long_name": "flux"} type: Literal["FluxTimeData"] = "FluxTimeData" _dims = ("t",) @@ -493,7 +494,7 @@ class ModeData(PlanarData, FreqData): direction: List[Direction] = ["+", "-"] mode_index: Array[int] values: Array[complex] - val_units : str = 'V*m' + data_attrs: Dict[str, str] = {"units": "sqrt(W)", "long_name": "mode amplitudes"} type: Literal["ModeData"] = "ModeData" _dims = ("direction", "mode_index", "f") diff --git a/tidy3d/convert.py b/tidy3d/convert.py index 91b776c203..e75622dd41 100644 --- a/tidy3d/convert.py +++ b/tidy3d/convert.py @@ -490,11 +490,13 @@ def load_old_monitor_data(simulation: Simulation, data_file: str) -> SolverDataD for field_name in monitor.fields: comp = ["x", "y", "z"].index(field_name[1]) field_vals = np.array(f_handle[name][field_name[0]][comp, ...]) - x, y, z = discretize_monitor(field_vals, monitor) + # x, y, z = discretize_monitor(field_vals, monitor) + subgrid = simulation.discretize(monitor.geometry) + yee_locs = subgrid[field_name] data_dict[name][field_name] = { - "x": x, - "y": y, - "z": z, + "x": yee_locs.x, + "y": yee_locs.y, + "z": yee_locs.z, "values": field_vals, sampler_label: sampler_values, } From 0dc251325671b50ca0eef8695b55f0587d625b6e Mon Sep 17 00:00:00 2001 From: tylerflex Date: Mon, 15 Nov 2021 16:33:40 -0800 Subject: [PATCH 2/3] reorganized field data --- README.md | 6 +- tests/test_IO.py | 24 +- tests/test_components.py | 12 +- tests/test_material_library.py | 1 - tests/test_plugins.py | 6 +- tests/test_web.py | 6 +- tests/utils.py | 2 +- tidy3d/__init__.py | 4 +- tidy3d/__main__.py | 6 +- tidy3d/components/README.md | 8 +- tidy3d/components/__init__.py | 5 +- tidy3d/components/base.py | 34 +- tidy3d/components/data.py | 329 ++++++++++++++------ tidy3d/components/geometry.py | 21 +- tidy3d/components/medium.py | 233 +++++++------- tidy3d/components/simulation.py | 15 +- tidy3d/convert.py | 9 +- tidy3d/log.py | 20 +- tidy3d/plugins/dispersion/fit.py | 8 +- tidy3d/plugins/mode/mode_solver.py | 6 +- tidy3d/plugins/smatrix/component_modeler.py | 2 +- tidy3d/web/__init__.py | 2 +- tidy3d/web/container.py | 16 +- tidy3d/web/webapi.py | 12 +- 24 files changed, 479 insertions(+), 308 deletions(-) diff --git a/README.md b/README.md index f06a844999..e8ebccc81b 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ git push origin x.x.x - [x] Get rid of pydantic? <- Not doing it - [x] Simplify MonitorData - [x] remove MonitorData.monitor and MonitorData.monitor_name attributes. - - [x] remove MonitorData.load() and MonitorData.export() + - [x] remove MonitorData.from_file() and MonitorData.to_file() - [x] Make monitordata.load_from_group aware of lists - [x] Use shapely for geometry ops / plotting? `Geometry.geo(x=0)` -> shapely representation. - [x] Fix all tests. @@ -239,8 +239,8 @@ git push origin x.x.x After integration is complete, need to flesh out details and make them compatible with the main package. - [ ] Make webAPI work without conversion (1 hour) - - [ ] Use native `Simulation.export()` or `Simulation.json()` for `upload()`. - - [ ] Use native `SimulationData.load()` for `load()`. + - [ ] Use native `Simulation.to_file()` or `Simulation.json()` for `upload()`. + - [ ] Use native `SimulationData.from_file()` for `load()`. - [ ] Flesh out Mode solver details (discussion, then implemeent in 1 hour) - [ ] Change API? - [ ] Flesh out Symmetry details (3 days?) diff --git a/tests/test_IO.py b/tests/test_IO.py index 90f90c63bc..d880bedca6 100644 --- a/tests/test_IO.py +++ b/tests/test_IO.py @@ -12,8 +12,8 @@ @clear_tmp def test_simulation_load_export(): path = "tests/tmp/simulation.json" - SIM.export(path) - SIM2 = Simulation.load(path) + SIM.to_file(path) + SIM2 = Simulation.from_file(path) assert SIM == SIM2, "original and loaded simulations are not the same" @@ -59,8 +59,8 @@ def test_simulation_preserve_types(): ) path = "tests/tmp/simulation.json" - sim_all.export(path) - sim_2 = Simulation.load(path) + sim_all.to_file(path) + sim_2 = Simulation.from_file(path) assert sim_all == sim_2 M_types = [type(s.medium) for s in sim_2.structures] @@ -82,8 +82,8 @@ def test_simulation_preserve_types(): def test_1a_simulation_load_export2(): path = "tests/tmp/simulation.json" - SIM2.export(path) - SIM3 = Simulation.load(path) + SIM2.to_file(path) + SIM3 = Simulation.from_file(path) assert SIM2 == SIM3, "original and loaded simulations are not the same" @@ -108,9 +108,9 @@ def test_validation_speed(): new_structures.append(new_structure) S.structures = new_structures - S.export(path) + S.to_file(path) time_start = time() - _S = Simulation.load(path) + _S = Simulation.from_file(path) time_validate = time() - time_start times_sec.append(time_validate) assert S == _S @@ -124,9 +124,9 @@ def test_validation_speed(): @clear_tmp def test_yaml(): path = "tests/tmp/simulation.json" - SIM.export(path) - sim = Simulation.load(path) + SIM.to_file(path) + sim = Simulation.from_file(path) path1 = "tests/tmp/simulation.yaml" - sim.export_yaml(path1) - sim1 = Simulation.load_yaml(path1) + sim.to_yaml(path1) + sim1 = Simulation.from_yaml(path1) assert sim1 == sim diff --git a/tests/test_components.py b/tests/test_components.py index c8f606bf45..1622ea92b6 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -64,7 +64,7 @@ def _test_version(): grid_size=(0.1, 0.1, 0.1), ) path = "tests/tmp/simulation.json" - sim.export("tests/tmp/simulation.json") + sim.to_file("tests/tmp/simulation.json") with open(path, "r") as f: s = f.read() assert '"version": ' in s @@ -196,15 +196,15 @@ def test_medium_conversions(): freq = 3.0 # test medium creation - medium = nk_to_medium(n, k, freq) + medium = Medium.from_nk(n, k, freq) # test consistency - eps_z = nk_to_eps_complex(n, k) - eps, sig = nk_to_eps_sigma(n, k, freq) - eps_z_ = eps_sigma_to_eps_complex(eps, sig, freq) + eps_z = AbstractMedium.nk_to_eps_complex(n, k) + eps, sig = AbstractMedium.nk_to_eps_sigma(n, k, freq) + eps_z_ = AbstractMedium.eps_sigma_to_eps_complex(eps, sig, freq) assert np.isclose(eps_z, eps_z_) - n_, k_ = eps_complex_to_nk(eps_z) + n_, k_ = AbstractMedium.eps_complex_to_nk(eps_z) assert np.isclose(n, n_) assert np.isclose(k, k_) diff --git a/tests/test_material_library.py b/tests/test_material_library.py index 410d1196a8..489950d0a9 100644 --- a/tests/test_material_library.py +++ b/tests/test_material_library.py @@ -1,4 +1,3 @@ -import pytest import numpy as np from tidy3d.material_library import material_library diff --git a/tests/test_plugins.py b/tests/test_plugins.py index a147946649..6e31649380 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -80,17 +80,17 @@ def test_dispersion(): fitter = DispersionFitter(wvls, n_data) medium, rms = fitter.fit_single() medium, rms = fitter.fit(num_tries=2) - medium.export("tests/tmp/medium_fit.json") + medium.to_file("tests/tmp/medium_fit.json") def test_dispersion_load(): """loads dispersion model from nk data file""" - fitter = DispersionFitter.load("tests/data/nk_data.csv", skiprows=1, delimiter=",") + fitter = DispersionFitter.from_file("tests/data/nk_data.csv", skiprows=1, delimiter=",") medium, rms = fitter.fit(num_tries=20) def test_dispersion_plot(): """plots a medium fit from file""" - fitter = DispersionFitter.load("tests/data/nk_data.csv", skiprows=1, delimiter=",") + fitter = DispersionFitter.from_file("tests/data/nk_data.csv", skiprows=1, delimiter=",") medium, rms = fitter.fit(num_tries=20) fitter.plot(medium) diff --git a/tests/test_web.py b/tests/test_web.py index 72ef7cf87b..cce09b10d0 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -63,7 +63,7 @@ def test_webapi_5_download(): def test_webapi_6_load(): """load the results into sim_data""" task_id = _get_gloabl_task_id() - sim_data = web.load_data(task_id, simulation=sim_original, path=PATH_SIM_DATA) + sim_data = web.load(task_id, simulation=sim_original, path=PATH_SIM_DATA) first_monitor_name = list(sim_original.monitors.keys())[0] _ = sim_data[first_monitor_name] @@ -130,7 +130,7 @@ def test_job_5_download(): def test_job_6_load(): """load the results into sim_data""" job = _get_gloabl_job() - sim_data = job.load_data(path=PATH_SIM_DATA) + sim_data = job.load(path=PATH_SIM_DATA) first_monitor_name = list(sim_original.monitors.keys())[0] _ = sim_data[first_monitor_name] @@ -201,7 +201,7 @@ def test_batch_5_download(): def test_batch_6_load(): """load the results into sim_data""" batch = _get_gloabl_batch() - sim_data_dict = batch.load_data(path_dir=PATH_DIR_SIM_DATA) + sim_data_dict = batch.load(path_dir=PATH_DIR_SIM_DATA) first_monitor_name = list(sim_original.monitors.keys())[0] for _, sim_data in sim_data_dict.items(): _ = sim_data[first_monitor_name] diff --git a/tests/utils.py b/tests/utils.py index 62df6148d0..8604534363 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -112,7 +112,7 @@ def prepend_tmp(path): structures=[ td.Structure( geometry=td.Box(center=[0, 0, 0], size=[1.5, 1.5, 1.5]), - medium=td.nk_to_medium(n=2, k=0, freq=3e14), + medium=td.Medium.from_nk(n=2, k=0, freq=3e14), ) ], sources=[ diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 94a06bde58..1902d2a196 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -19,8 +19,6 @@ # medium from .components import Medium, PoleResidue, AnisotropicMedium, PEC from .components import Sellmeier, Debye, Drude, Lorentz -from .components import nk_to_eps_complex, nk_to_eps_sigma, eps_complex_to_nk -from .components import nk_to_medium, eps_sigma_to_eps_complex # structures from .components import Structure @@ -41,7 +39,7 @@ # data from .components import SimulationData, FieldData, FluxData, ModeData, FluxTimeData -from .components import data_type_map, ScalarFieldData, ScalarFieldTimeData +from .components import DATA_TYPE_MAP, ScalarFieldData, ScalarFieldTimeData # constants imported as `C_0 = td.C_0` or `td.constants.C_0` from .constants import inf, C_0, ETA_0 diff --git a/tidy3d/__main__.py b/tidy3d/__main__.py index ee248cf251..2bfdfb9a42 100644 --- a/tidy3d/__main__.py +++ b/tidy3d/__main__.py @@ -61,9 +61,9 @@ # load the simulation if ".yaml" in sim_file or ".yml" in sim_file: - simulation = Simulation.load_yaml(sim_file) + simulation = Simulation.from_yaml(sim_file) else: - simulation = Simulation.load(sim_file) + simulation = Simulation.from_file(sim_file) # inspect the simulation if inspect_sim: @@ -92,7 +92,7 @@ # run the simulation and load results job.start() job.monitor() -sim_data = job.load_data(path=out_file) +sim_data = job.load(path=out_file) # visualize results if viz_results: diff --git a/tidy3d/components/README.md b/tidy3d/components/README.md index 5d66707e6f..c3e11fbaa0 100644 --- a/tidy3d/components/README.md +++ b/tidy3d/components/README.md @@ -150,10 +150,10 @@ The `AbstractMedium()` base class define the properties of the medium of which t A Dispersionless medium is created with `Medium(permittivity, conductivity)`. The following functions are useful for defining a dispersionless medium using other possible inputs: -- `nk_to_eps_sigma` (convert refractive index parameters (n, k) to permittivity and conductivity). -- `nk_to_medium` (convert refractive index parameters (n, k) to a `Medium()` directly). -- `nk_to_eps_complex` (convert refractive index parameters (n, k) to a complex-valued permittivity). -- `eps_sigma_to_eps_complex` (convert permittivity and conductivity to complex-valued permittiviy) +- `AbstractMedium.nk_to_eps_sigma` (convert refractive index parameters (n, k) to permittivity and conductivity). +- `Medium.from_nk` (convert refractive index parameters (n, k) to a `Medium()` directly). +- `AbstractMedium.nk_to_eps_complex` (convert refractive index parameters (n, k) to a complex-valued permittivity). +- `AbstractMedium.eps_sigma_to_eps_complex` (convert permittivity and conductivity to complex-valued permittiviy) ### Dispersive Media diff --git a/tidy3d/components/__init__.py b/tidy3d/components/__init__.py index c65ffbde7f..7849196032 100644 --- a/tidy3d/components/__init__.py +++ b/tidy3d/components/__init__.py @@ -14,8 +14,7 @@ # medium from .medium import Medium, PoleResidue, Sellmeier, Debye, Drude, Lorentz, AnisotropicMedium, PEC -from .medium import nk_to_eps_complex, nk_to_eps_sigma, eps_complex_to_nk -from .medium import nk_to_medium, eps_sigma_to_eps_complex +from .medium import AbstractMedium # structure from .structure import Structure @@ -36,4 +35,4 @@ # data from .data import SimulationData, FieldData, FluxData, ModeData, FluxTimeData -from .data import ScalarFieldData, ScalarFieldTimeData, data_type_map +from .data import ScalarFieldData, ScalarFieldTimeData, DATA_TYPE_MAP diff --git a/tidy3d/components/base.py b/tidy3d/components/base.py index 5619c0c631..1ca83d92a5 100644 --- a/tidy3d/components/base.py +++ b/tidy3d/components/base.py @@ -67,7 +67,7 @@ def help(self, methods: bool = False) -> None: rich.inspect(self, methods=methods) @classmethod - def load(cls, fname: str): + def from_file(cls, fname: str): """Loads a :class:`Tidy3dBaseModel` from .yaml or .json file. Parameters @@ -82,15 +82,15 @@ def load(cls, fname: str): Example ------- - >>> simulation = Simulation.load(fname='folder/sim.json') + >>> simulation = Simulation.from_file(fname='folder/sim.json') """ if ".json" in fname: - return cls.load_json(fname=fname) + return cls.from_json(fname=fname) if ".yaml" in fname: - return cls.load_yaml(fname=fname) + return cls.from_yaml(fname=fname) raise FileError(f"File must be .json or .yaml, given {fname}") - def export(self, fname: str) -> None: + def to_file(self, fname: str) -> None: """Exports :class:`Tidy3dBaseModel` instance to .yaml or .json file Parameters @@ -100,16 +100,16 @@ def export(self, fname: str) -> None: Example ------- - >>> simulation.export(fname='folder/sim.json') + >>> simulation.to_file(fname='folder/sim.json') """ if ".json" in fname: - return self.export_json(fname=fname) + return self.to_json(fname=fname) if ".yaml" in fname: - return self.export_yaml(fname=fname) + return self.to_yaml(fname=fname) raise FileError(f"File must be .json or .yaml, given {fname}") @classmethod - def load_json(cls, fname: str): + def from_json(cls, fname: str): """Load a :class:`Tidy3dBaseModel` from .json file. Parameters @@ -124,11 +124,11 @@ def load_json(cls, fname: str): Example ------- - >>> simulation = Simulation.load_json(fname='folder/sim.json') + >>> simulation = Simulation.from_json(fname='folder/sim.json') """ return cls.parse_file(fname) - def export_json(self, fname: str) -> None: + def to_json(self, fname: str) -> None: """Exports :class:`Tidy3dBaseModel` instance to .json file Parameters @@ -138,14 +138,14 @@ def export_json(self, fname: str) -> None: Example ------- - >>> simulation.export_json(fname='folder/sim.json') + >>> simulation.to_json(fname='folder/sim.json') """ json_string = self._json_string() with open(fname, "w", encoding="utf-8") as file_handle: file_handle.write(json_string) @classmethod - def load_yaml(cls, fname: str): + def from_yaml(cls, fname: str): """Loads :class:`Tidy3dBaseModel` from .yaml file. Parameters @@ -156,18 +156,18 @@ def load_yaml(cls, fname: str): Returns ------- :class:`Tidy3dBaseModel` - An instance of the component class calling `load_yaml`. + An instance of the component class calling `from_yaml`. Example ------- - >>> simulation = Simulation.load_yaml(fname='folder/sim.yaml') + >>> simulation = Simulation.from_yaml(fname='folder/sim.yaml') """ with open(fname, "r", encoding="utf-8") as yaml_in: json_dict = yaml.safe_load(yaml_in) json_raw = json.dumps(json_dict, indent=INDENT) return cls.parse_raw(json_raw) - def export_yaml(self, fname: str) -> None: + def to_yaml(self, fname: str) -> None: """Exports :class:`Tidy3dBaseModel` instance to .yaml file. Parameters @@ -177,7 +177,7 @@ def export_yaml(self, fname: str) -> None: Example ------- - >>> simulation.export_yaml(fname='folder/sim.yaml') + >>> simulation.to_yaml(fname='folder/sim.yaml') """ json_string = self._json_string() json_dict = json.loads(json_string) diff --git a/tidy3d/components/data.py b/tidy3d/components/data.py index ca7fb56848..ec02236ec1 100644 --- a/tidy3d/components/data.py +++ b/tidy3d/components/data.py @@ -17,35 +17,6 @@ # TODO: add warning if fields didnt fully decay -""" Helper functions """ - - -def save_string(hdf5_grp, string_key: str, string_value: str) -> None: - """Save a string to an hdf5 group.""" - str_type = h5py.special_dtype(vlen=str) - hdf5_grp.create_dataset(string_key, (1,), dtype=str_type) - hdf5_grp[string_key][0] = string_value - - -def decode_bytes(bytes_dataset) -> str: - """Decode an hdf5 dataset containing bytes to a string.""" - return bytes_dataset[0].decode("utf-8") - - -def load_string(hdf5_grp, string_key: str) -> str: - """Load a string from an hdf5 group.""" - string_value_bytes = hdf5_grp.get(string_key) - if not string_value_bytes: - return None - return decode_bytes(string_value_bytes) - - -def decode_bytes_array(array_of_bytes: Numpy) -> List[str]: - """Convert numpy array containing bytes to list of strings.""" - list_of_bytes = array_of_bytes.tolist() - list_of_str = [v.decode("utf-8") for v in list_of_bytes] - return list_of_str - # mapping of data coordinates to units for assigning .attrs to the xarray objects DIM_ATTRS = { @@ -104,6 +75,33 @@ def add_to_group(self, hdf5_grp): def load_from_group(cls, hdf5_grp): """Load data contents from an hdf5 group.""" + @staticmethod + def save_string(hdf5_grp, string_key: str, string_value: str) -> None: + """Save a string to an hdf5 group.""" + str_type = h5py.special_dtype(vlen=str) + hdf5_grp.create_dataset(string_key, (1,), dtype=str_type) + hdf5_grp[string_key][0] = string_value + + @staticmethod + def decode_bytes(bytes_dataset) -> str: + """Decode an hdf5 dataset containing bytes to a string.""" + return bytes_dataset[0].decode("utf-8") + + @staticmethod + def load_string(hdf5_grp, string_key: str) -> str: + """Load a string from an hdf5 group.""" + string_value_bytes = hdf5_grp.get(string_key) + if not string_value_bytes: + return None + return Tidy3dData.decode_bytes(string_value_bytes) + + @staticmethod + def decode_bytes_array(array_of_bytes: Numpy) -> List[str]: + """Convert numpy array containing bytes to list of strings.""" + list_of_bytes = array_of_bytes.tolist() + list_of_str = [v.decode("utf-8") for v in list_of_bytes] + return list_of_str + class MonitorData(Tidy3dData, ABC): """Abstract base class for objects storing individual data from simulation.""" @@ -136,16 +134,14 @@ class MonitorData(Tidy3dData, ABC): @property def data(self) -> Tidy3dDataArray: - # pylint:disable=line-too-long """Returns an xarray representation of the montitor data. Returns ------- xarray.DataArray Representation of the monitor data using xarray. - For more details refer to `xarray's Documentaton `_. + For more details refer to `xarray's Documentaton `_. """ - # pylint:enable=line-too-long # make DataArray data_dict = self.dict() @@ -180,7 +176,7 @@ def add_to_group(self, hdf5_grp) -> None: """Add data contents to an hdf5 group.""" # save the type information of MonitorData to the group - save_string(hdf5_grp, "type", self.type) + Tidy3dData.save_string(hdf5_grp, "type", self.type) for data_name, data_value in self.dict().items(): # for each data member in self._dims (+ values), add to group. @@ -201,12 +197,7 @@ def load_from_group(cls, hdf5_grp): # handle data stored as np.array() of bytes instead of strings for str_kwarg in ("direction",): if kwargs.get(str_kwarg) is not None: - kwargs[str_kwarg] = decode_bytes_array(kwargs[str_kwarg]) - - # handle data stored as np.array() of bytes instead of strings - # for str_kwarg in ("x", "y", "z"): - # if kwargs.get(str_kwarg) is not None: - # kwargs[str_kwarg] = kwargs[str_kwarg].tolist() + kwargs[str_kwarg] = Tidy3dData.decode_bytes_array(kwargs[str_kwarg]) # ignore the "type" dataset as it's used for finding type for loading kwargs.pop("type") @@ -215,7 +206,8 @@ def load_from_group(cls, hdf5_grp): class CollectionData(Tidy3dData): - """Abstract base class. Stores a collection of data with same dimension types (such as field). + """Abstract base class. + Stores a collection of data with same dimension types (such as a field with many components). Parameters ---------- @@ -227,26 +219,22 @@ class CollectionData(Tidy3dData): type: str = None @property - def data(self) -> xr.Dataset: - # pylint:disable=line-too-long + def data(self) -> Dict[str, xr.DataArray]: """For field quantities, store a single xarray DataArray for each ``field``. These all go in a single xarray Dataset, which keeps track of the shared coords. Returns ------- - xarray.Dataset - Representation of the underlying data using xarray. - For more details refer to `xarray's Documentaton `_. + Dict[str, xarray.DataArray] + Mapping of data dict keys to corresponding DataArray from .data property. + For more details refer to `xarray's Documentaton `_. """ - # pylint:enable=line-too-long - data_arrays = {name: arr.data for name, arr in self.data_dict.items()} - # make an xarray dataset - return xr.Dataset(data_arrays) - # return data_arrays + data_arrays = {name: arr.data for name, arr in self.data_dict.items()} + return data_arrays def __eq__(self, other): - """Check for equality against other :class:`CollectionData` object.""" + """Check for equality against other :class:`AbstractFieldData` object.""" # same keys? if not all(k in other.data_dict.keys() for k in self.data_dict.keys()): @@ -260,10 +248,10 @@ def __eq__(self, other): return True def add_to_group(self, hdf5_grp) -> None: - """Add data from a :class:`CollectionData` to an hdf5 group .""" + """Add data from a :class:`AbstractFieldData` to an hdf5 group .""" # put collection's type information into the group - save_string(hdf5_grp, "type", self.type) + Tidy3dData.save_string(hdf5_grp, "type", self.type) for data_name, data_value in self.data_dict.items(): # create a new group for each member of collection and add its data @@ -272,7 +260,7 @@ def add_to_group(self, hdf5_grp) -> None: @classmethod def load_from_group(cls, hdf5_grp): - """Load a :class:`CollectionData` from hdf5 group containing data.""" + """Load a :class:`AbstractFieldData` from hdf5 group containing data.""" data_dict = {} for data_name, data_value in hdf5_grp.items(): @@ -281,13 +269,112 @@ def load_from_group(cls, hdf5_grp): continue # get the type from MonitorData.type and add instance to dict - data_type = data_type_map[load_string(data_value, "type")] + data_type = DATA_TYPE_MAP[Tidy3dData.load_string(data_value, "type")] data_dict[data_name] = data_type.load_from_group(data_value) return cls(data_dict=data_dict) + def ensure_member_exists(self, member_name: str): + """make sure a member of collection is present in data""" + if member_name not in self.data_dict: + raise DataError(f"member_name '{member_name}' not found.") + + +""" Subclasses of MonitorData and CollectionData """ + + +class AbstractFieldData(CollectionData, ABC): + """Sores a collection of EM fields either in freq or time domain.""" + + """ Get the standard EM components from the dict using convenient "dot" syntax.""" + + @property + def Ex(self): + """Get Ex component of field using '.Ex' syntax.""" + scalar_data = self.data_dict.get("Ex") + if scalar_data: + return scalar_data.data + return None + + @property + def Ey(self): + """Get Ey component of field using '.Ey' syntax.""" + scalar_data = self.data_dict.get("Ey") + if scalar_data: + return scalar_data.data + return None + + @property + def Ez(self): + """Get Ez component of field using '.Ez' syntax.""" + scalar_data = self.data_dict.get("Ez") + if scalar_data: + return scalar_data.data + return None + + @property + def Hx(self): + """Get Hx component of field using '.Hx' syntax.""" + scalar_data = self.data_dict.get("Hx") + if scalar_data: + return scalar_data.data + return None + + @property + def Hy(self): + """Get Hy component of field using '.Hy' syntax.""" + scalar_data = self.data_dict.get("Hy") + if scalar_data: + return scalar_data.data + return None + + @property + def Hz(self): + """Get Hz component of field using '.Hz' syntax.""" + scalar_data = self.data_dict.get("Hz") + if scalar_data: + return scalar_data.data + return None + + def colocate(self, x, y, z) -> xr.Dataset: + """colocate all of the data at a set of x, y, z coordinates. -""" Classes of Monitor Data """ + Parameters + ---------- + x : np.array + x coordinates of locations. + y coordinates of locations. + z coordinates of locations. + + Returns + ------- + xr.Dataset + Dataset containing all fields at the same spatial locations. + For more details refer to `xarray's Documentaton `_. + + Note + ---- + For many operations (such as flux calculations and plotting), + it is important that the fields are colocated at the same spatial locations. + Be sure to apply this method to your field data in those cases. + """ + coord_val_map = {"x": x, "y": y, "z": z} + centered_data_dict = {} + for field_name, field_data in self.data_dict.items(): + centered_data_array = field_data.data + for coord_name in "xyz": + if len(centered_data_array.coords[coord_name]) <= 1: + # centered_data_array = centered_data_array.isel(**{coord_name:0}) + coord_val = coord_val_map[coord_name] + coord_kwargs = {coord_name: coord_val} + centered_data_array = centered_data_array.assign_coords(**coord_kwargs) + centered_data_array = centered_data_array.isel(**{coord_name: 0}) + else: + coord_vals = coord_val_map[coord_name] + centered_data_array = centered_data_array.interp(**{coord_name: coord_vals}) + centered_data_dict[field_name] = centered_data_array + # import pdb; pdb.set_trace() + return xr.Dataset(centered_data_dict) class FreqData(MonitorData, ABC): @@ -308,7 +395,6 @@ class AbstractScalarFieldData(MonitorData, ABC): x: Array[float] y: Array[float] z: Array[float] - # values: Union[Array[complex], Array[float]] class PlanarData(MonitorData, ABC): @@ -388,34 +474,52 @@ class ScalarFieldTimeData(AbstractScalarFieldData, TimeData): _dims = ("x", "y", "z", "t") -class FieldData(CollectionData): - """Stores a collection of scalar fields - from a :class:`FieldMonitor` or :class:`FieldTimeMonitor`. +class FieldData(AbstractFieldData): + """Stores a collection of scalar fields in the frequency domain from a :class:`FieldMonitor`. Parameters ---------- - data_dict : Dict[str, :class:`ScalarFieldData`] or Dict[str, :class:`ScalarFieldTimeData`] - Mapping of field name to its scalar field data. + data_dict : Dict[str, :class:`ScalarFieldData`] + Mapping of field name (eg. 'Ex') to its scalar field data. Example ------- >>> f = np.linspace(1e14, 2e14, 1001) - >>> t = np.linspace(0, 1e-12, 1001) >>> x = np.linspace(-1, 1, 10) >>> y = np.linspace(-2, 2, 20) >>> z = np.linspace(0, 0, 1) - >>> values_f = (1+1j) * np.random.random((len(x), len(y), len(z), len(f))) - >>> values_t = np.random.random((len(x), len(y), len(z), len(t))) - >>> field_f = ScalarFieldData(values=values_f, x=x, y=y, z=z, f=f) - >>> field_t = ScalarFieldTimeData(values=values_t, x=x, y=y, z=z, t=t) - >>> data_f = FieldData(data_dict={'Ex': field_f, 'Ey': field_f}) - >>> data_t = FieldData(data_dict={'Ex': field_t, 'Ey': field_t}) + >>> values = (1+1j) * np.random.random((len(x), len(y), len(z), len(f))) + >>> field = ScalarFieldData(values=values, x=x, y=y, z=z, f=f) + >>> data = FieldData(data_dict={'Ex': field, 'Ey': field}) """ - data_dict: Union[Dict[str, ScalarFieldData], Dict[str, ScalarFieldTimeData]] + data_dict: Dict[str, ScalarFieldData] type: Literal["FieldData"] = "FieldData" +class FieldTimeData(AbstractFieldData): + """Stores a collection of scalar fields in the time domain from a :class:`FieldTimeMonitor`. + + Parameters + ---------- + data_dict : Dict[str, :class:`ScalarFieldTimeData`] + Mapping of field name to its scalar field data. + + Example + ------- + >>> t = np.linspace(0, 1e-12, 1001) + >>> x = np.linspace(-1, 1, 10) + >>> y = np.linspace(-2, 2, 20) + >>> z = np.linspace(0, 0, 1) + >>> values = np.random.random((len(x), len(y), len(z), len(t))) + >>> field = ScalarFieldTimeData(values=values, x=x, y=y, z=z, t=t) + >>> data = FieldTimeData(data_dict={'Ex': field, 'Ey': field}) + """ + + data_dict: Dict[str, ScalarFieldTimeData] + type: Literal["FieldTimeData"] = "FieldTimeData" + + class FluxData(AbstractFluxData, FreqData): """Stores frequency-domain power flux data from a :class:`FluxMonitor`. @@ -500,11 +604,12 @@ class ModeData(PlanarData, FreqData): _dims = ("direction", "mode_index", "f") -# maps MonitorData.type string to the actual type, for MonitorData.load() -data_type_map = { +# maps MonitorData.type string to the actual type, for MonitorData.from_file() +DATA_TYPE_MAP = { "ScalarFieldData": ScalarFieldData, "ScalarFieldTimeData": ScalarFieldTimeData, "FieldData": FieldData, + "FieldTimeData": FieldTimeData, "FluxData": FluxData, "FluxTimeData": FluxTimeData, "ModeData": ModeData, @@ -525,7 +630,7 @@ class SimulationData(Tidy3dBaseModel): """ simulation: Simulation - monitor_data: Dict[str, Union[MonitorData, FieldData]] + monitor_data: Dict[str, Tidy3dData] log_string: str = None @property @@ -545,13 +650,58 @@ def __getitem__(self, monitor_name: str) -> Union[Tidy3dDataArray, xr.Dataset]: Returns ------- - xarray.DataArray or xarray.Dataset - The xarray representation of the data. + xarray.DataArray or CollectionData + Data from the supplied monitor. + If the monitor corresponds to collection-like data (such as fields), + a collection data instance is returned. + Otherwise, if it is a MonitorData instance, the xarray representation is returned. """ monitor_data = self.monitor_data.get(monitor_name) if not monitor_data: raise DataError(f"monitor {monitor_name} not found") - return monitor_data.data + if isinstance(monitor_data, MonitorData): + return monitor_data.data + return monitor_data + + def ensure_monitor_exists(self, monitor_name: str) -> None: + """Raise exception if monitor isn't in the simulation data""" + if monitor_name not in self.monitor_data: + raise DataError(f"Data for monitor '{monitor_name}' not found in simulation data.") + + def ensure_field_monitor(self, data_obj: Tidy3dData) -> None: + """Raise exception if monitor isn't a field monitor.""" + if not isinstance(data_obj, (FieldData, FieldTimeData)): + raise DataError(f"data_obj '{data_obj}' " "not a FieldData or FieldTimeData instance.") + + def at_centers(self, field_monitor_name: str) -> xr.Dataset: + """return xarray.Dataset representation of field monitor data + co-located at Yee cell centers. + + Parameters + ---------- + field_monitor_name : str + Name of field monitor used in the original :class:`Simulation`. + + Returns + ------- + xarray.Dataset + Dataset containing all of the fields in the data + interpolated to center locations on Yee grid. + """ + + # get the data + self.ensure_monitor_exists(field_monitor_name) + field_monitor_data = self.monitor_data.get(field_monitor_name) + self.ensure_field_monitor(field_monitor_data) + + # get the monitor, discretize, and get center locations + monitor = self.simulation.get_monitor_by_name(field_monitor_name) + sub_grid = self.simulation.discretize(monitor) + centers = sub_grid.centers + + # colocate each of the field components at centers + field_dataset = field_monitor_data.colocate(x=centers.x, y=centers.y, z=centers.z) + return field_dataset @add_ax_if_none def plot_field( @@ -605,15 +755,12 @@ def plot_field( """ # get the monitor data - if field_monitor_name not in self.monitor_data: - raise DataError(f"Monitor named '{field_monitor_name}' not found.") + self.ensure_monitor_exists(field_monitor_name) monitor_data = self.monitor_data.get(field_monitor_name) - if not isinstance(monitor_data, FieldData): - raise DataError(f"field_monitor_name '{field_monitor_name}' not a FieldData instance.") + self.ensure_field_monitor(monitor_data) # get the field data component - if field_name not in monitor_data.data_dict: - raise DataError(f"field_name {field_name} not found in {field_monitor_name}.") + monitor_data.ensure_member_exists(field_name) xr_data = monitor_data.data_dict.get(field_name).data # select the frequency or time value @@ -650,12 +797,12 @@ def plot_field( # plot the field xy_coord_labels = list("xyz") xy_coord_labels.pop(axis) - x_coord_label, y_coord_label = xy_coord_labels + x_coord_label, y_coord_label = xy_coord_labels # pylint:disable=unbalanced-tuple-unpacking field_data.plot(ax=ax, x=x_coord_label, y=y_coord_label) # plot the simulation epsilon ax = self.simulation.plot_structures_eps( - freq=freq, cbar=False, x=x, y=y, z=z, alpha=eps_alpha, ax=ax + freq=freq, cbar=False, x=x, y=y, z=z, alpha=eps_alpha, ax=ax, **kwargs ) # set the limits based on the xarray coordinates min and max @@ -666,7 +813,7 @@ def plot_field( return ax - def export(self, fname: str) -> None: + def to_file(self, fname: str) -> None: """Export :class:`SimulationData` to single hdf5 file including monitor data. Parameters @@ -678,10 +825,10 @@ def export(self, fname: str) -> None: with h5py.File(fname, "a") as f_handle: # save json string as an attribute - save_string(f_handle, "sim_json", self.simulation.json()) + Tidy3dData.save_string(f_handle, "sim_json", self.simulation.json()) if self.log_string: - save_string(f_handle, "log_string", self.log_string) + Tidy3dData.save_string(f_handle, "log_string", self.log_string) # make a group for monitor_data mon_data_grp = f_handle.create_group("monitor_data") @@ -692,7 +839,7 @@ def export(self, fname: str) -> None: mon_data.add_to_group(mon_grp) @classmethod - def load(cls, fname: str): + def from_file(cls, fname: str): """Load :class:`SimulationData` from .hdf5 file. Parameters @@ -710,18 +857,18 @@ def load(cls, fname: str): with h5py.File(fname, "r") as f_handle: # construct the original simulation from the json string - sim_json = load_string(f_handle, "sim_json") + sim_json = Tidy3dData.load_string(f_handle, "sim_json") simulation = Simulation.parse_raw(sim_json) # get the log if exists - log_string = load_string(f_handle, "log_string") + log_string = Tidy3dData.load_string(f_handle, "log_string") # loop through monitor dataset and create all MonitorData instances monitor_data_dict = {} for monitor_name, monitor_data in f_handle["monitor_data"].items(): # load this MonitorData instance, add to monitor_data dict - data_type = data_type_map[load_string(monitor_data, "type")] + data_type = DATA_TYPE_MAP[Tidy3dData.load_string(monitor_data, "type")] monitor_data_instance = data_type.load_from_group(monitor_data) monitor_data_dict[monitor_name] = monitor_data_instance diff --git a/tidy3d/components/geometry.py b/tidy3d/components/geometry.py index d748256759..b61a699f67 100644 --- a/tidy3d/components/geometry.py +++ b/tidy3d/components/geometry.py @@ -510,6 +510,25 @@ class Box(Geometry): size: Size + @classmethod + def from_bounds(cls, rmin: Coordinate, rmax: Coordinate): + """Constructs a :class:`Box` from minimum and maximum coordinate bounds + + Parameters + ---------- + rmin : Tuple[float, float, float] + (x, y, z) coordinate of the minimum values. + rmax : Tuple[float, float, float] + (x, y, z) coordinate of the maximum values. + + Example + ------- + >>> b = Box.from_bounds(rmin=(-1, -2, -3), rmax=(3, 2, 1)) + """ + center = tuple((pt_min + pt_max / 2.0) for pt_min, pt_max in zip(rmin, rmax)) + size = tuple((pt_max - pt_max) for pt_min, pt_max in zip(rmin, rmax)) + return cls(center=center, size=size) + def intersections(self, x: float = None, y: float = None, z: float = None): """Returns shapely geometry at plane specified by one non None value of x,y,z. @@ -827,7 +846,7 @@ def set_center(cls, val, values): return val @classmethod - def from_gdspy( # pylint:disable=too-many-arguments + def from_gds( # pylint:disable=too-many-arguments cls, gds_cell, axis: Axis, diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index 67078d6359..e2f4a69005 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -70,7 +70,7 @@ def plot(self, freqs: float, ax: Ax = None) -> Ax: # pylint: disable=invalid-na freqs = np.array(freqs) eps_complex = self.eps_model(freqs) - n, k = eps_complex_to_nk(eps_complex) + n, k = AbstractMedium.eps_complex_to_nk(eps_complex) freqs_thz = freqs / 1e12 ax.plot(freqs_thz, n, label="n") @@ -81,6 +81,93 @@ def plot(self, freqs: float, ax: Ax = None) -> Ax: # pylint: disable=invalid-na ax.set_aspect("auto") return ax + """ Conversion helper functions """ + + @staticmethod + def nk_to_eps_complex(n: float, k: float = 0.0) -> complex: + """Convert n, k to complex permittivity. + + Parameters + ---------- + n : float + Real part of refractive index. + k : float = 0.0 + Imaginary part of refrative index. + + Returns + ------- + complex + Complex-valued relative permittivty. + """ + eps_real = n ** 2 - k ** 2 + eps_imag = 2 * n * k + return eps_real + 1j * eps_imag + + @staticmethod + def eps_complex_to_nk(eps_c: complex) -> Tuple[float, float]: + """Convert complex permittivity to n, k values. + + Parameters + ---------- + eps_c : complex + Complex-valued relative permittivity. + + Returns + ------- + Tuple[float, float] + Real and imaginary parts of refractive index (n & k). + """ + ref_index = np.sqrt(eps_c) + return ref_index.real, ref_index.imag + + @staticmethod + def nk_to_eps_sigma(n: float, k: float, freq: float) -> Tuple[float, float]: + """Convert ``n``, ``k`` at frequency ``freq`` to permittivity and conductivity values. + + Parameters + ---------- + n : float + Real part of refractive index. + k : float = 0.0 + Imaginary part of refrative index. + frequency : float + Frequency to evaluate permittivity at (Hz). + + Returns + ------- + Tuple[float, float] + Real part of relative permittivity & electric conductivity. + """ + eps_complex = AbstractMedium.nk_to_eps_complex(n, k) + eps_real, eps_imag = eps_complex.real, eps_complex.imag + omega = 2 * np.pi * freq + sigma = omega * eps_imag + return eps_real, sigma + + @staticmethod + def eps_sigma_to_eps_complex(eps_real: float, sigma: float, freq: float) -> complex: + """convert permittivity and conductivity to complex permittivity at freq + + Parameters + ---------- + eps_real : float + Real-valued relative permittivity. + sigma : float + Conductivity. + freq : float + Frequency to evaluate permittivity at (Hz). + If not supplied, returns real part of permittivity (limit as frequency -> infinity.) + + Returns + ------- + complex + Complex-valued relative permittivity. + """ + if not freq: + return eps_real + omega = 2 * np.pi * freq + return eps_real + 1j * sigma / omega + def ensure_freq_in_range(eps_model: Callable[[float], complex]) -> Callable[[float], complex]: """Decorate ``eps_model`` to log warning if frequency supplied is out of bounds.""" @@ -180,17 +267,31 @@ def eps_model(self, frequency: float) -> complex: complex Complex-valued relative permittivity evaluated at ``frequency``. """ - return eps_sigma_to_eps_complex(self.permittivity, self.conductivity, frequency) - - def __str__(self) -> str: - """string representation.""" - return ( - f"td.Medium(" - f"permittivity={self.permittivity}," - f"conductivity={self.conductivity}," - f"frequency_range={self.frequency_range})" + return AbstractMedium.eps_sigma_to_eps_complex( + self.permittivity, self.conductivity, frequency ) + @classmethod + def from_nk(cls, n: float, k: float, freq: float): + """Convert ``n`` and ``k`` values at frequency ``freq`` to :class:`Medium`. + + Parameters + ---------- + n : float + Real part of refractive index. + k : float = 0 + Imaginary part of refrative index. + frequency : float + Frequency to evaluate permittivity at (Hz). + + Returns + ------- + :class:`Medium` + medium containing the corresponding ``permittivity`` and ``conductivity``. + """ + eps, sigma = AbstractMedium.nk_to_eps_sigma(n, k, freq) + return cls(permittivity=eps, conductivity=sigma) + class AnisotropicMedium(AbstractMedium): """Diagonally anisotripic medium. @@ -264,7 +365,7 @@ def plot(self, freqs: float, ax: Ax = None) -> Ax: for label, medium_component in zip(("xx", "yy", "zz"), (self.xx, self.yy, self.zz)): eps_complex = medium_component.eps_model(freqs) - n, k = eps_complex_to_nk(eps_complex) + n, k = AbstractMedium.eps_complex_to_nk(eps_complex) ax.plot(freqs_thz, n, label=f"n, eps_{label}") ax.plot(freqs_thz, k, label=f"k, eps_{label}") @@ -416,7 +517,7 @@ def eps_model(self, frequency: float) -> complex: Complex-valued relative permittivity evaluated at the frequency. """ n = self._n_model(frequency) - return nk_to_eps_complex(n) + return AbstractMedium.nk_to_eps_complex(n) @property def pole_residue(self): @@ -668,111 +769,3 @@ def pole_residue(self): MediumType = Union[ Literal[PEC], Medium, AnisotropicMedium, PoleResidue, Sellmeier, Lorentz, Debye, Drude ] - -""" Conversion helper functions """ - - -def nk_to_eps_complex(n: float, k: float = 0.0) -> complex: - """Convert n, k to complex permittivity. - - Parameters - ---------- - n : float - Real part of refractive index. - k : float = 0.0 - Imaginary part of refrative index. - - Returns - ------- - complex - Complex-valued relative permittivty. - """ - eps_real = n ** 2 - k ** 2 - eps_imag = 2 * n * k - return eps_real + 1j * eps_imag - - -def eps_complex_to_nk(eps_c: complex) -> Tuple[float, float]: - """Convert complex permittivity to n, k values. - - Parameters - ---------- - eps_c : complex - Complex-valued relative permittivity. - - Returns - ------- - Tuple[float, float] - Real and imaginary parts of refractive index (n & k). - """ - ref_index = np.sqrt(eps_c) - return ref_index.real, ref_index.imag - - -def nk_to_eps_sigma(n: float, k: float, freq: float) -> Tuple[float, float]: - """Convert ``n``, ``k`` at frequency ``freq`` to permittivity and conductivity values. - - Parameters - ---------- - n : float - Real part of refractive index. - k : float = 0.0 - Imaginary part of refrative index. - frequency : float - Frequency to evaluate permittivity at (Hz). - - Returns - ------- - Tuple[float, float] - Real part of relative permittivity & electric conductivity. - """ - eps_complex = nk_to_eps_complex(n, k) - eps_real, eps_imag = eps_complex.real, eps_complex.imag - omega = 2 * np.pi * freq - sigma = omega * eps_imag - return eps_real, sigma - - -def nk_to_medium(n: float, k: float, freq: float) -> Medium: - """Convert ``n`` and ``k`` values at frequency ``freq`` to :class:`Medium`. - - Parameters - ---------- - n : float - Real part of refractive index. - k : float = 0 - Imaginary part of refrative index. - frequency : float - Frequency to evaluate permittivity at (Hz). - - Returns - ------- - :class:`Medium` - medium containing the corresponding ``permittivity`` and ``conductivity``. - """ - eps, sigma = nk_to_eps_sigma(n, k, freq) - return Medium(permittivity=eps, conductivity=sigma) - - -def eps_sigma_to_eps_complex(eps_real: float, sigma: float, freq: float) -> complex: - """convert permittivity and conductivity to complex permittivity at freq - - Parameters - ---------- - eps_real : float - Real-valued relative permittivity. - sigma : float - Conductivity. - freq : float - Frequency to evaluate permittivity at (Hz). - If not supplied, returns real part of permittivity (limit as frequency -> infinity.) - - Returns - ------- - complex - Complex-valued relative permittivity. - """ - if not freq: - return eps_real - omega = 2 * np.pi * freq - return eps_real + 1j * sigma / omega diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index da56e6fcea..59b5df302e 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -13,19 +13,19 @@ from .geometry import Box from .types import Symmetry, Ax, Shapely, FreqBound from .grid import Coords1D, Grid, Coords -from .medium import Medium, MediumType, eps_complex_to_nk +from .medium import Medium, MediumType, AbstractMedium from .structure import Structure from .source import SourceType from .monitor import MonitorType from .pml import PMLTypes, PML from .viz import StructMediumParams, StructEpsParams, PMLParams, SymParams, add_ax_if_none from ..constants import inf, C_0 -from ..log import log +from ..log import log, Tidy3dKeyError # for docstring examples from .geometry import Sphere, Cylinder, PolySlab # pylint:disable=unused-import from .source import VolumeSource, GaussianPulse # pylint:disable=unused-import -from .monitor import FieldMonitor, FluxMonitor # pylint:disable=unused-import +from .monitor import FieldMonitor, FluxMonitor, Monitor # pylint:disable=unused-import # technically this is creating a circular import issue because it calls tidy3d/__init__.py # from .. import __version__ as version_number @@ -224,6 +224,13 @@ def medium_map(self) -> Dict[MediumType, pydantic.NonNegativeInt]: return {medium: index for index, medium in enumerate(self.mediums)} + def get_monitor_by_name(self, name: str) -> Monitor: + """Return monitor named 'name'.""" + for monitor in self.monitors: + if monitor.name == name: + return monitor + raise Tidy3dKeyError(f"No monitor named '{name}'") + """ Plotting """ @add_ax_if_none @@ -869,7 +876,7 @@ def wvl_mat_min(self) -> float: freq_max = max(source.source_time.freq0 for source in self.sources) wvl_min = C_0 / min(freq_max) eps_max = max(abs(structure.medium.get_eps(freq_max)) for structure in self.structures) - n_max, _ = eps_complex_to_nk(eps_max) + n_max, _ = AbstractMedium.eps_complex_to_nk(eps_max) return wvl_min / n_max def discretize(self, box: Box) -> Grid: diff --git a/tidy3d/convert.py b/tidy3d/convert.py index e75622dd41..cc769a0048 100644 --- a/tidy3d/convert.py +++ b/tidy3d/convert.py @@ -4,7 +4,7 @@ import numpy as np import h5py -from tidy3d import Simulation, SimulationData, FieldData, data_type_map +from tidy3d import Simulation, SimulationData, DATA_TYPE_MAP from tidy3d import Box, Sphere, Cylinder, PolySlab from tidy3d import Medium, AnisotropicMedium from tidy3d.components.medium import DispersiveMedium, PECMedium @@ -15,7 +15,7 @@ from tidy3d.components.monitor import AbstractFieldMonitor, AbstractFluxMonitor, ModeMonitor from tidy3d.components.monitor import FreqMonitor, TimeMonitor from tidy3d.components.types import Numpy - +from tidy3d.components.data import FieldData, FieldTimeData # maps monitor name to dictionary mapping data label to data value MonitorDataDict = Dict[str, Union[Numpy, Dict[str, Numpy]]] @@ -456,12 +456,13 @@ def load_solver_results( for monitor in simulation.monitors: name = monitor.name monitor_data_dict = solver_data_dict[name] - monitor_data_type = data_type_map[monitor.data_type] + monitor_data_type = DATA_TYPE_MAP[monitor.data_type] if monitor.type in ("FieldMonitor", "FieldTimeMonitor"): field_data = {} for field_name, data_dict in monitor_data_dict.items(): field_data[field_name] = monitor_data_type(**data_dict) - monitor_data[name] = FieldData(data_dict=field_data) + field_data_type = FieldData if monitor.type == "FieldMonitor" else FieldTimeData + monitor_data[name] = field_data_type(data_dict=field_data) else: monitor_data[name] = monitor_data_type(**monitor_data_dict) return SimulationData(simulation=simulation, monitor_data=monitor_data, log_string=log_string) diff --git a/tidy3d/log.py b/tidy3d/log.py index b77f796345..7132ed5326 100644 --- a/tidy3d/log.py +++ b/tidy3d/log.py @@ -10,15 +10,20 @@ logging.basicConfig(level=DEFAULT_LEVEL, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]) -log = logging.getLogger("rich") - -level_map = { +# maps level string to level integer for python logging package +LEVEL_MAP = { "error": 40, "warning": 30, "info": 20, "debug": 10, } +# importable logger +log = logging.getLogger("rich") + + +""" Tidy3d custom exceptions """ + class Tidy3DError(Exception): """Any error in tidy3d""" @@ -61,14 +66,17 @@ class DataError(Tidy3DError): """Error accessing data.""" +""" Logging functions """ + + def _get_level_int(level: str) -> int: """Get the integer corresponding to the level string.""" level = level.lower() - if level not in level_map: + if level not in LEVEL_MAP: raise ConfigError( - f"logging level {level} not supported, " f"must be in {list(level_map.keys())}." + f"logging level {level} not supported, " f"must be in {list(LEVEL_MAP.keys())}." ) - return level_map[level] + return LEVEL_MAP[level] def set_logging_level(level: str = DEFAULT_LEVEL.lower()) -> None: diff --git a/tidy3d/plugins/dispersion/fit.py b/tidy3d/plugins/dispersion/fit.py index 32df967660..0e34e7518b 100644 --- a/tidy3d/plugins/dispersion/fit.py +++ b/tidy3d/plugins/dispersion/fit.py @@ -7,7 +7,7 @@ import numpy as np from rich.progress import Progress -from ...components import PoleResidue, nk_to_eps_complex, eps_complex_to_nk +from ...components import PoleResidue, AbstractMedium from ...constants import C_0, HBAR from ...components.viz import add_ax_if_none from ...components.types import Ax, Numpy @@ -159,7 +159,7 @@ def __init__(self, wvl_um: Numpy, n_data: Numpy, k_data: Numpy = None): if k_data is None: self.k_data = np.zeros_like(n_data) self.lossy = False - self.eps_data = nk_to_eps_complex(n=self.n_data, k=self.k_data) + self.eps_data = AbstractMedium.nk_to_eps_complex(n=self.n_data, k=self.k_data) self.freqs = C_0 / wvl_um self.frequency_range = (np.min(self.freqs), np.max(self.freqs)) @@ -405,7 +405,7 @@ def plot( freqs = C_0 / wvl_um eps_model = medium.eps_model(freqs) - n_model, k_model = eps_complex_to_nk(eps_model) + n_model, k_model = AbstractMedium.eps_complex_to_nk(eps_model) dot_sizes = 25 linewidth = 3 @@ -424,7 +424,7 @@ def plot( return ax @classmethod - def load(cls, fname, **loadtxt_kwargs): + def from_file(cls, fname, **loadtxt_kwargs): """Loads ``DispersionFitter`` from file contining wavelength, n, k data. Parameters diff --git a/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index c64ab5af57..094c0c167e 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -22,7 +22,7 @@ """ Stage: Simulation Mode Specs Outputs Viz Export ---------- + ---------- -> ----------- -> ---------- -> ---------- -Method: __init__() .solve() .plot() .export() +Method: __init__() .solve() .plot() .to_file() td Objects: Simulation Mode -> FieldData -> image -> ModeSource Plane ^ | ModeMonitor @@ -42,8 +42,8 @@ mon = ms.export_monitor(mode=mode) # if we're happy with results, return td.ModeMonitor src = ms.export_src(mode=mode, src_time=...) # or as a td.ModeSource -src.export('data/my_source.json') # this source /monitor can be saved to file -src = ModeSource.load('data/my_source.json') # and loaded in our script +src.to_file('data/my_source.json') # this source /monitor can be saved to file +src = ModeSource.from_file('data/my_source.json') # and loaded in our script """ diff --git a/tidy3d/plugins/smatrix/component_modeler.py b/tidy3d/plugins/smatrix/component_modeler.py index 17847fe4c8..da2780d343 100644 --- a/tidy3d/plugins/smatrix/component_modeler.py +++ b/tidy3d/plugins/smatrix/component_modeler.py @@ -55,7 +55,7 @@ # self.batch.monitor() # def load(self): -# self.batch.load() +# self.batch.from_file() # def compute_S_matrix(self): # """ compute S-matrix from the batch results (to-do) """ diff --git a/tidy3d/web/__init__.py b/tidy3d/web/__init__.py index df493a11d5..287c528228 100644 --- a/tidy3d/web/__init__.py +++ b/tidy3d/web/__init__.py @@ -1,5 +1,5 @@ """ imports interfaces for interacting with server """ import sys -from .webapi import run, upload, get_info, start, monitor, delete, download, load_data +from .webapi import run, upload, get_info, start, monitor, delete, download, load from .container import Job, Batch diff --git a/tidy3d/web/container.py b/tidy3d/web/container.py index e8ea3db1e2..d8161cf626 100644 --- a/tidy3d/web/container.py +++ b/tidy3d/web/container.py @@ -47,7 +47,7 @@ def run(self, path: str = DEFAULT_DATA_PATH) -> SimulationData: self.upload() self.start() self.monitor() - return self.load_data(path=path) + return self.load(path=path) def upload(self) -> None: """Upload simulation to server without running. @@ -104,7 +104,7 @@ def monitor(self) -> None: Note ---- To load the output of completed simulation into :class:`.SimulationData`objets, - call :meth:`Job.load_data`. + call :meth:`Job.load`. """ status = self.status @@ -129,11 +129,11 @@ def download(self, path: str = DEFAULT_DATA_PATH) -> None: Note ---- - To load the data into :class:`.SimulationData`objets, can call :meth:`Job.load_data`. + To load the data into :class:`.SimulationData`objets, can call :meth:`Job.load`. """ web.download(task_id=self.task_id, simulation=self.simulation, path=path) - def load_data(self, path: str = DEFAULT_DATA_PATH) -> SimulationData: + def load(self, path: str = DEFAULT_DATA_PATH) -> SimulationData: """Download results from simulation (if not already) and load them into ``SimulationData`` object. @@ -147,7 +147,7 @@ def load_data(self, path: str = DEFAULT_DATA_PATH) -> SimulationData: :class:`.SimulationData` Object containing data about simulation. """ - return web.load_data(task_id=self.task_id, simulation=self.simulation, path=path) + return web.load(task_id=self.task_id, simulation=self.simulation, path=path) def delete(self): """Delete server-side data associated with :class:`Job`.""" @@ -333,7 +333,7 @@ def download(self, path_dir: str = DEFAULT_DATA_DIR) -> None: job_path = self._job_data_path(task_name, path_dir) job.download(path=job_path) - def load_data(self, path_dir: str = DEFAULT_DATA_DIR) -> Dict[TaskName, SimulationData]: + def load(self, path_dir: str = DEFAULT_DATA_DIR) -> Dict[TaskName, SimulationData]: """Download results and load them into :class:`.SimulationData` object. Parameters @@ -359,7 +359,7 @@ def load_data(self, path_dir: str = DEFAULT_DATA_DIR) -> Dict[TaskName, Simulati self.download(path_dir=path_dir) for task_name, job in self.jobs.items(): job_path = self._job_data_path(task_id=job.task_id, path_dir=path_dir) - sim_data = job.load_data(path=job_path) + sim_data = job.load(path=job_path) sim_data_dir[task_name] = sim_data return sim_data_dir @@ -386,5 +386,5 @@ def items(self, path_dir: str = DEFAULT_DATA_DIR) -> Generator: """ for task_name, job in self.jobs.items(): job_path = self._job_data_path(task_id=job.task_id, path_dir=path_dir) - sim_data = job.load_data(path=job_path) + sim_data = job.load(path=job_path) yield task_name, sim_data diff --git a/tidy3d/web/webapi.py b/tidy3d/web/webapi.py index 556f6b3273..62ba6aa199 100644 --- a/tidy3d/web/webapi.py +++ b/tidy3d/web/webapi.py @@ -54,7 +54,7 @@ def run( task_id = upload(simulation=simulation, task_name=task_name, folder_name=folder_name) start(task_id) monitor(task_id) - return load_data(task_id=task_id, simulation=simulation, path=path) + return load(task_id=task_id, simulation=simulation, path=path) def upload(simulation: Simulation, task_name: str, folder_name: str = "default") -> TaskId: @@ -159,7 +159,7 @@ def monitor(task_id: TaskId) -> None: Note ---- - To load results when finished, may call :meth:`load_data`. + To load results when finished, may call :meth:`load`. """ task_info = get_info(task_id) @@ -253,7 +253,7 @@ def download(task_id: TaskId, simulation: Simulation, path: str = "simulation_da log.debug("loading old monitor data to data dict") # TODO: we cant convert old simulation file to new, so we'll ask for original as input instead. - # simulation = Simulation.load(sim_file) + # simulation = Simulation.from_file(sim_file) mon_data_dict = load_old_monitor_data(simulation=simulation, data_file=mon_file) log.debug("creating SimulationData from monitor data dict") @@ -264,7 +264,7 @@ def download(task_id: TaskId, simulation: Simulation, path: str = "simulation_da ) log.info(f"exporting SimulationData to {path}") - sim_data.export(path) + sim_data.to_file(path) log.debug("clearing extraneous files") _rm_file(sim_file) @@ -272,7 +272,7 @@ def download(task_id: TaskId, simulation: Simulation, path: str = "simulation_da _rm_file(log_file) -def load_data( +def load( task_id: TaskId, simulation: Simulation, path: str = "simulation_data.hdf5", @@ -300,7 +300,7 @@ def load_data( download(task_id=task_id, simulation=simulation, path=path) log.info(f"loading SimulationData from {path}") - return SimulationData.load(path) + return SimulationData.from_file(path) def delete(task_id: TaskId) -> TaskInfo: From 7ec3b692e1d9e63045e0a7c8dace079d8a0a4729 Mon Sep 17 00:00:00 2001 From: tylerflex Date: Mon, 15 Nov 2021 16:34:34 -0800 Subject: [PATCH 3/3] fixed pylint --- tidy3d/components/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidy3d/components/data.py b/tidy3d/components/data.py index ec02236ec1..5da4a6b592 100644 --- a/tidy3d/components/data.py +++ b/tidy3d/components/data.py @@ -704,7 +704,7 @@ def at_centers(self, field_monitor_name: str) -> xr.Dataset: return field_dataset @add_ax_if_none - def plot_field( + def plot_field( #pylint:disable=too-many-arguments, too-many-locals self, field_monitor_name: str, field_name: str,