Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
- Improved performance of antenna metrics calculation by utilizing cached wave amplitude calculations instead of recomputing wave amplitudes for each port excitation in the `TerminalComponentModelerData`.
- Fixed: In `Tidy3dBaseModel` the hash (and cached `.json_string`) are now sensitive to changes in `.attrs`.
- Changed hashing method in `Tidy3dBaseModel` from sha256 to md5.

### Fixed
- More robust `Sellmeier` and `Debye` material model, and prevent very large pole parameters in `PoleResidue` material model.
Expand Down
15 changes: 15 additions & 0 deletions tests/test_components/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,18 @@ def test_scientific_notation(min_val, max_val, min_digits, expected):
"""Test the _scientific_notation method with various inputs."""
result = Tidy3dBaseModel._scientific_notation(min_val, max_val, min_digits=min_digits)
assert result == expected


def test_updated_hash_and_json_with_changed_attr():
obj = td.Medium(attrs={"foo": "attr"})

old_hash = obj._hash_self()
json_old = obj._json_string

obj.attrs["foo"] = "changed"

new_hash = obj._hash_self()
json_new = obj._json_string

assert new_hash != old_hash
assert json_old != json_new
73 changes: 60 additions & 13 deletions tidy3d/components/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from autograd.builtins import dict as dict_ag
from autograd.tracer import isbox
from pydantic.v1.fields import ModelField
from pydantic.v1.json import custom_pydantic_encoder

from tidy3d.exceptions import FileError
from tidy3d.log import log
Expand Down Expand Up @@ -68,13 +69,47 @@ def cached_property(cached_property_getter):
return property(cache(cached_property_getter))


def cached_property_guarded(key_func):
"""Like cached_property, but invalidates when the key_func(self) changes."""

def _decorator(getter):
prop_name = getter.__name__

@wraps(getter)
def _guarded(self):
cache_store = self._cached_properties.get(prop_name)
current_key = key_func(self)
if cache_store is not None:
cached_key, cached_value = cache_store
if cached_key == current_key:
return cached_value
value = getter(self)
self._cached_properties[prop_name] = (current_key, value)
return value

return property(_guarded)

return _decorator


def ndarray_encoder(val):
"""How a ``np.ndarray`` gets handled before saving to json."""
if np.any(np.iscomplex(val)):
return {"real": val.real.tolist(), "imag": val.imag.tolist()}
return val.real.tolist()


def make_json_compatible(json_string: str) -> str:
"""Makes the string compatible with json standards, notably for infinity."""

tmp_string = "<<TEMPORARY_INFINITY_STRING>>"
json_string = json_string.replace("-Infinity", tmp_string)
json_string = json_string.replace('""-Infinity""', tmp_string)
json_string = json_string.replace("Infinity", '"Infinity"')
json_string = json_string.replace('""Infinity""', '"Infinity"')
return json_string.replace(tmp_string, '"-Infinity"')


def _get_valid_extension(fname: str) -> str:
"""Return the file extension from fname, validated to accepted ones."""
valid_extensions = [".json", ".yaml", ".hdf5", ".h5", ".hdf5.gz"]
Expand Down Expand Up @@ -139,7 +174,7 @@ def _hash_self(self) -> str:
"""Hash this component with ``hashlib`` in a way that is the same every session."""
bf = io.BytesIO()
self.to_hdf5(bf)
return hashlib.sha256(bf.getvalue()).hexdigest()
return hashlib.md5(bf.getvalue()).hexdigest()

def __init__(self, **kwargs):
"""Init method, includes post-init validators."""
Expand Down Expand Up @@ -217,6 +252,24 @@ def _special_characters_not_in_name(cls, values):
"by calling ``obj.json()``.",
)

def _attrs_digest(self) -> str:
"""Stable digest of `attrs` using the same JSON encoding rules as pydantic .json()."""
encoders = getattr(self.__config__, "json_encoders", {}) or {}

def _default(o):
return custom_pydantic_encoder(encoders, o)

json_str = json.dumps(
self.attrs,
default=_default,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
)
json_str = make_json_compatible(json_str)

return hashlib.sha256(json_str.encode("utf-8")).hexdigest()

def copy(self, deep: bool = True, validate: bool = True, **kwargs) -> Tidy3dBaseModel:
"""Copy a Tidy3dBaseModel. With ``deep=True`` and ``validate=True`` as default."""
kwargs.update(deep=deep)
Expand Down Expand Up @@ -719,7 +772,11 @@ def from_hdf5(
)
return cls.parse_obj(model_dict, **parse_obj_kwargs)

def to_hdf5(self, fname: str, custom_encoders: Optional[list[Callable]] = None) -> None:
def to_hdf5(
self,
fname: str,
custom_encoders: Optional[list[Callable]] = None,
) -> None:
"""Exports :class:`Tidy3dBaseModel` instance to .hdf5 file.

Parameters
Expand Down Expand Up @@ -931,7 +988,7 @@ def check_equal(dict1: dict, dict2: dict) -> bool:

return check_equal(self.dict(), other.dict())

@cached_property
@cached_property_guarded(lambda self: self._attrs_digest())
def _json_string(self) -> str:
"""Returns string representation of a :class:`Tidy3dBaseModel`.

Expand All @@ -955,16 +1012,6 @@ def _json(self, indent=INDENT, exclude_unset=False, **kwargs) -> str:
Json-formatted string holding :class:`Tidy3dBaseModel` data.
"""

def make_json_compatible(json_string: str) -> str:
"""Makes the string compatible with json standards, notably for infinity."""

tmp_string = "<<TEMPORARY_INFINITY_STRING>>"
json_string = json_string.replace("-Infinity", tmp_string)
json_string = json_string.replace('""-Infinity""', tmp_string)
json_string = json_string.replace("Infinity", '"Infinity"')
json_string = json_string.replace('""Infinity""', '"Infinity"')
return json_string.replace(tmp_string, '"-Infinity"')

json_string = self.json(indent=indent, exclude_unset=exclude_unset, **kwargs)
json_string = make_json_compatible(json_string)
return json_string
Expand Down