From f644845cf02475caea1c070d56736eac660d337b Mon Sep 17 00:00:00 2001 From: Hiroki Date: Fri, 15 Jul 2022 10:09:03 -0700 Subject: [PATCH] T674129: Add point data (#49) * T674129: Add point data * T674129: PointData fix --- .flake8 | 2 +- protos/ansys/api/edb/v1/arc_data.proto | 55 ++++ protos/ansys/api/edb/v1/point_data.proto | 20 +- src/ansys/edb/core/geometry/__init__.py | 1 + src/ansys/edb/core/geometry/point_data.py | 283 ++++++++++++++++++ src/ansys/edb/core/geometry/polygon_data.py | 2 +- src/ansys/edb/core/interface/grpc/messages.py | 32 +- src/ansys/edb/core/interface/grpc/parser.py | 15 + src/ansys/edb/core/session.py | 12 +- src/ansys/edb/core/terminal/terminals.py | 7 +- src/ansys/edb/core/typing/__init__.py | 9 + src/ansys/edb/core/utility/__init__.py | 1 + src/ansys/edb/core/utility/conversions.py | 48 +++ src/ansys/edb/core/utility/edb_errors.py | 4 +- src/ansys/edb/core/utility/edb_logging.py | 2 +- src/ansys/edb/core/utility/value.py | 198 +++++++++--- src/ansys/edb/core/utility/variable_server.py | 6 +- tests/examples/test_spiral_inductor.py | 2 +- tests/test_point_data.py | 74 +++++ tests/test_terminals.py | 89 +++--- tests/utils/fixtures.py | 11 + 21 files changed, 753 insertions(+), 120 deletions(-) create mode 100644 src/ansys/edb/core/geometry/point_data.py create mode 100644 src/ansys/edb/core/interface/grpc/parser.py create mode 100644 src/ansys/edb/core/typing/__init__.py create mode 100644 src/ansys/edb/core/utility/conversions.py create mode 100644 tests/test_point_data.py diff --git a/.flake8 b/.flake8 index 41573c4a45..1d81323b26 100644 --- a/.flake8 +++ b/.flake8 @@ -3,5 +3,5 @@ exclude = venv, __init__.py, doc/_build, .venv select = W191, W291, W293, W391, E115, E117, E122, E124, E125, E225, E231, E301, E303, E501, F401, F403 count = True max-complexity = 10 -max-line-length = 100 +max-line-length = 120 statistics = True diff --git a/protos/ansys/api/edb/v1/arc_data.proto b/protos/ansys/api/edb/v1/arc_data.proto index e720d98cce..bd7842ae6d 100644 --- a/protos/ansys/api/edb/v1/arc_data.proto +++ b/protos/ansys/api/edb/v1/arc_data.proto @@ -7,7 +7,62 @@ package ansys.api.edb.v1; import "edb_messages.proto"; import "point_data.proto"; +enum RotationDirection { + ROTATION_DIRECTION_CCW = 0; + ROTATION_DIRECTION_CW = 1; + ROTATION_DIRECTION_COLINEAR = 2; +} + +service ArcDataService { + // Get height of an arc + rpc GetHeight (ArcMessage) returns (google.protobuf.FloatValue) {} + + // Get center point of an arc + rpc GetCenter (ArcMessage) returns (PointMessage) {} + + // Get midpoint of an arc + rpc GetMidpoint (ArcMessage) returns (PointMessage) {} + + // Get radius of an arc + rpc GetRadius (ArcMessage) returns (google.protobuf.FloatValue) {} + + // Get bounding box of an arc + rpc GetBoundingBox (ArcMessage) returns (ArcDataTwoPointsMessage) {} + + // Get angle between two arcs. + rpc GetAngle (ArcMessage) returns (google.protobuf.FloatValue) {} + + // Get closest points between two arcs. + rpc ClosestPoints (ArcDataTwoArcsMessage) returns (ArcDataTwoPointsMessage) {} +} + message ArcMessage { + message RadiusMessage { + float radius = 1; + RotationDirection dir = 2; + bool is_big = 3; + } + message CenterMessage { + PointMessage point = 1; + RotationDirection dir = 2; + } + PointMessage start = 1; PointMessage end = 2; + oneof option { + google.protobuf.FloatValue height = 3; + PointMessage thru = 4; + RadiusMessage radius = 5; + CenterMessage center = 6; + } +} + +message ArcDataTwoArcsMessage { + ArcMessage arc1 = 1; + ArcMessage arc2 = 2; +} + +message ArcDataTwoPointsMessage { + PointMessage point1 = 1; + PointMessage point2 = 2; } diff --git a/protos/ansys/api/edb/v1/point_data.proto b/protos/ansys/api/edb/v1/point_data.proto index 67df7ce7b0..f47e45d548 100644 --- a/protos/ansys/api/edb/v1/point_data.proto +++ b/protos/ansys/api/edb/v1/point_data.proto @@ -12,6 +12,12 @@ enum PolySense { SENSE_CCW = 2; } +service PointDataService { + rpc Rotate (PointDataRotateMessage) returns (PointMessage) {} + rpc ClosestPoint (PointDataWithLineMessage) returns (PointMessage) {} + rpc Distance (PointDataWithLineMessage) returns (google.protobuf.FloatValue) {} +} + message PointMessage { ValueMessage x = 1; ValueMessage y = 2; @@ -33,4 +39,16 @@ message PointsMessage { message PointPropertyMessage { EDBObjMessage target = 1; PointMessage point = 2; -} \ No newline at end of file +} + +message PointDataRotateMessage { + PointMessage point = 1; + PointMessage rotate_center = 2; + float rotate_angle = 3; +} + +message PointDataWithLineMessage { + PointMessage point = 1; + PointMessage line_start = 2; + PointMessage line_end = 3; +} diff --git a/src/ansys/edb/core/geometry/__init__.py b/src/ansys/edb/core/geometry/__init__.py index 45db3befd7..799d0eb3a1 100644 --- a/src/ansys/edb/core/geometry/__init__.py +++ b/src/ansys/edb/core/geometry/__init__.py @@ -1,4 +1,5 @@ """Import geometry classes.""" from ansys.edb.core.geometry.arc_data import ArcData +from ansys.edb.core.geometry.point_data import PointData from ansys.edb.core.geometry.polygon_data import PolygonData, PolygonSenseType diff --git a/src/ansys/edb/core/geometry/point_data.py b/src/ansys/edb/core/geometry/point_data.py new file mode 100644 index 0000000000..94d83e5df4 --- /dev/null +++ b/src/ansys/edb/core/geometry/point_data.py @@ -0,0 +1,283 @@ +"""Point Data.""" +from functools import reduce +import operator + +from ansys.api.edb.v1 import point_data_pb2_grpc + +from ansys.edb.core import session +from ansys.edb.core.interface.grpc import messages, parser +from ansys.edb.core.utility import conversions, value + + +class PointData: + """Represent arbitrary (x, y) coordinates that exist on 2D space.""" + + __stub: point_data_pb2_grpc.PointDataServiceStub = session.StubAccessor( + session.StubType.point_data + ) + + def __init__(self, *data): + """Initialize a point data from list of coordinates. + + Parameters + ---------- + data : Iterable[Iterable[ansys.edb.core.typing.ValueLike], ansys.edb.core.typing.ValueLike] + """ + self._x = self._y = self._arc_h = None + + if len(data) == 1: + # try to expand the argument. + try: + iter(data[0]) + data = data[0] + except TypeError: + pass + + if len(data) == 1: + self._arc_h = conversions.to_value(data[0]) + elif len(data) == 2: + self._x, self._y = [conversions.to_value(val) for val in data] + else: + raise TypeError( + "PointData must receive either one value representing arc height or " + f"two values representing x and y coordinates. - Received '{data}'" + ) + + def __eq__(self, other): + """Compare if two objects represent the same coordinates. + + Parameters + ---------- + other : PointData + + Returns + ------- + bool + """ + if isinstance(other, self.__class__): + return self.__dict__ == other.__dict__ + return False + + def __len__(self): + """Return the number of coordinates present. + + Returns + ------- + int + """ + return len(self._matrix_values) + + def __add__(self, other): + """Perform matrix addition of two points. + + Parameters + ---------- + other : ansys.edb.core.typing.PointLike + + Returns + ------- + PointData + """ + return self.__class__(self._map_reduce(other, operator.__add__)) + + def __sub__(self, other): + """Perform matrix subtraction of two points. + + Parameters + ---------- + other : ansys.edb.core.typing.PointLike + + Returns + ------- + PointData + """ + return self.__class__(self._map_reduce(other, operator.__sub__)) + + def __str__(self): + """Generate unique name for point object. + + Returns + ------- + str + """ + coord = ",".join([str(v) for v in self._matrix_values]) + return f"<{coord}>" if self.is_arc else f"({coord})" + + @property + def _matrix_values(self): + """Return coordinates of this point as a list of Value. + + Returns + ------- + list of ansys.edb.core.utility.Value + """ + return [self.arc_height] if self.is_arc else [self.x, self.y] + + def _map_reduce(self, other, op): + other = conversions.to_point(other) + return [reduce(op, values) for values in zip(self._matrix_values, other._matrix_values)] + + @property + def is_arc(self): + """Return if the point represents an arc. + + Returns + ------- + bool + """ + return self._arc_h is not None + + @property + def arc_height(self): + """Return the height of arc. + + Returns + ------- + ansys.edb.core.utility.Value + """ + return self._arc_h + + @property + def x(self): + """Return the x coordinate. + + Returns + ------- + ansys.edb.core.utility.Value + """ + return self._x + + @property + def y(self): + """Return the y coordinate. + + Returns + ------- + ansys.edb.core.utility.Value + """ + return self._y + + @property + def is_parametric(self): + """Return if this point contains parametric values (variable expressions). + + Returns + ------- + bool + """ + return any(val.is_parametric for val in self._matrix_values) + + @property + def magnitude(self): + """Return the magnitude of point vector. + + Returns + ------- + float + """ + if self.is_arc: + return 0 + return sum([v**2 for v in self._matrix_values], value.Value(0)).sqrt + + @property + def normalized(self): + """Normalize the point vector. + + Returns + ------- + PointData + """ + mag = self.magnitude + n = [0] * len(self) if mag == 0 else [v / mag for v in self._matrix_values] + return self.__class__(n) + + def closest(self, start, end): + """Return the closest point on the line segment [start, end] from the point. + + Return None if either point is an arc. + + Parameters + ---------- + start : ansys.edb.core.typing.PointLike + end : ansys.edb.core.typing.PointLike + + Returns + ------- + typing.Optional[PointData] + """ + if not self.is_arc: + pm = self.__stub.ClosestPoint(messages.point_data_with_line_message(self, start, end)) + return parser.to_point_data(pm) + + def distance(self, start, end=None): + """Compute the shortest distance from the point to the line segment [start, end] when end point is given, \ + otherwise the distance between the point and another. + + Parameters + ---------- + start : ansys.edb.core.typing.PointLike + end : ansys.edb.core.typing.PointLike, optional + + Returns + ------- + float + """ + if end is None: + return (self - start).magnitude + else: + return self.__stub.Distance( + messages.point_data_with_line_message(self, start, end) + ).value + + def cross(self, other): + """Compute the cross product of the point vector with another. + + Return None if either point is an arc. + + Parameters + ---------- + other : ansys.edb.core.typing.PointLike + + Returns + ------- + typing.Optional[value.Value] + """ + other = conversions.to_point(other) + if not self.is_arc and not other.is_arc: + return self.x * other.y - self.y * other.x + + def move(self, vector): + """Move the point by a vector. + + Return None if either point is an arc. + + Parameters + ---------- + vector : ansys.edb.core.typing.PointLike + + Returns + ------- + typing.Optional[PointData] + """ + vector = conversions.to_point(vector) + if not self.is_arc and not vector.is_arc: + return self + vector + + def rotate(self, angle, center): + """Rotate a point at the specified center by the specified angle. + + Return None if either point is an arc. + + Parameters + ---------- + angle : float + in radians. + center : ansys.edb.core.typing.PointLike + + Returns + ------- + typing.Optional[PointData] + """ + if not self.is_arc: + pm = self.__stub.Rotate(messages.point_data_rotate_message(self, center, angle)) + return parser.to_point_data(pm) diff --git a/src/ansys/edb/core/geometry/polygon_data.py b/src/ansys/edb/core/geometry/polygon_data.py index 22216b7673..13b569f69a 100644 --- a/src/ansys/edb/core/geometry/polygon_data.py +++ b/src/ansys/edb/core/geometry/polygon_data.py @@ -26,7 +26,7 @@ def create(points, closed, sense=PolygonSenseType.SENSE_CCW): Parameters ---------- - points : list of tuple of float, float + points : list[ansys.edb.core.typing.PointLike] closed : bool sense : PolygonSenseType, optional diff --git a/src/ansys/edb/core/interface/grpc/messages.py b/src/ansys/edb/core/interface/grpc/messages.py index 38393ac26a..c43bca243a 100644 --- a/src/ansys/edb/core/interface/grpc/messages.py +++ b/src/ansys/edb/core/interface/grpc/messages.py @@ -63,6 +63,8 @@ from ansys.api.edb.v1.point_data_pb2 import ( SENSE_CCW, PathPointsMessage, + PointDataRotateMessage, + PointDataWithLineMessage, PointMessage, PointPropertyMessage, PointsMessage, @@ -97,7 +99,7 @@ from ansys.api.edb.v1.transform_pb2 import TransformMessage, TransformPropertyMessage from google.protobuf.wrappers_pb2 import BoolValue, Int64Value, StringValue -from ansys.edb.core.utility.value import Value +from ansys.edb.core.utility import conversions def str_message(s: str): @@ -137,10 +139,8 @@ def points_data_message(points): def point_message(point): """Convert to PointMessage.""" - if point is None: - return None - else: - return PointMessage(x=value_message(point[0]), y=value_message(point[1])) + point = conversions.to_point(point) + return PointMessage(x=value_message(point.x), y=value_message(point.y)) def point_property_message(target, point): @@ -148,6 +148,22 @@ def point_property_message(target, point): return PointPropertyMessage(target=target.msg, point=point_message(point)) +def point_data_rotate_message(point, center, angle): + """Convert to PointRotateMessage.""" + return PointDataRotateMessage( + point=point_message(point), rotation_center=point_message(center), angle=angle + ) + + +def point_data_with_line_message(point, line_start, line_end): + """Convert to PointDataWithLineMessage.""" + return PointDataWithLineMessage( + point=point_message(point), + line_start=point_message(line_start), + line_end=point_message(line_end), + ) + + def arc_message(arc): """Convert to ArcMessage.""" if isinstance(arc, tuple) and len(arc) == 2: @@ -306,13 +322,13 @@ def point_term_set_params_message(term, layer, point): return PointTermSetParamsMessage(term=term.msg, params=point_term_params_message(layer, point)) -def point_term_creation_message(layout, net, layer, name, x, y): +def point_term_creation_message(layout, net, layer, name, point): """Convert to PointTermCreationMessage.""" return PointTermCreationMessage( layout=layout.msg, net=net_ref_message(net), name=name, - params=point_term_params_message(layer, (x, y)), + params=point_term_params_message(layer, point), ) @@ -687,7 +703,7 @@ def value_message(val): ------- ValueMessage """ - if isinstance(val, Value): + if hasattr(val, "msg") and isinstance(val.msg, ValueMessage): return val.msg msg = ValueMessage() diff --git a/src/ansys/edb/core/interface/grpc/parser.py b/src/ansys/edb/core/interface/grpc/parser.py new file mode 100644 index 0000000000..8180312403 --- /dev/null +++ b/src/ansys/edb/core/interface/grpc/parser.py @@ -0,0 +1,15 @@ +"""This module parses message back to client data types.""" + + +def to_point_data(point_message): + """Convert PointMessage to PointData. + + Parameters + ---------- + point_message : ansys.api.edb.v1.point_data_pb2.PointMessage + + Returns + ------- + ansys.edb.core.models.geometries.point_data.PointData + """ + return ansys.edb.core.geometry.point_data.PointData([point_message.x, point_message.y]) diff --git a/src/ansys/edb/core/session.py b/src/ansys/edb/core/session.py index 73166718bc..7488788084 100644 --- a/src/ansys/edb/core/session.py +++ b/src/ansys/edb/core/session.py @@ -37,6 +37,7 @@ from ansys.api.edb.v1.path_pb2_grpc import PathServiceStub from ansys.api.edb.v1.pin_group_pb2_grpc import PinGroupServiceStub from ansys.api.edb.v1.pin_group_term_pb2_grpc import PinGroupTerminalServiceStub +from ansys.api.edb.v1.point_data_pb2_grpc import PointDataServiceStub from ansys.api.edb.v1.point_term_pb2_grpc import PointTerminalServiceStub from ansys.api.edb.v1.polygon_data_pb2_grpc import PolygonDataServiceStub from ansys.api.edb.v1.polygon_pb2_grpc import PolygonServiceStub @@ -272,6 +273,7 @@ class StubType(Enum): group = GroupServiceStub netclass = NetClassServiceStub layer_map = LayerMapServiceStub + point_data = PointDataServiceStub # Dictionary for storing local server error code exception messages @@ -549,16 +551,6 @@ def get_apd_bondwire_def_stub(): return StubAccessor(StubType.apd_bondwire_def).__get__() -def get_value_stub(): - """Get Value stub. - - Returns - ------- - ValueServiceStub - """ - return StubAccessor(StubType.value).__get__() - - def get_variable_server_stub(): """Get VariableServer stub. diff --git a/src/ansys/edb/core/terminal/terminals.py b/src/ansys/edb/core/terminal/terminals.py index 87efbc4bdd..bcdb82ca27 100644 --- a/src/ansys/edb/core/terminal/terminals.py +++ b/src/ansys/edb/core/terminal/terminals.py @@ -815,7 +815,7 @@ class PointTerminal(Terminal): @classmethod @handle_grpc_exception - def create(cls, layout, net, layer, name, x, y): + def create(cls, layout, net, layer, name, point): """ Create a point terminal. @@ -825,15 +825,14 @@ def create(cls, layout, net, layer, name, x, y): net : str or Net layer : str or Layer name : str - x : Value - y : Value + point : PointLike Returns ------- PointTerminal """ return PointTerminal( - cls.__stub.Create(messages.point_term_creation_message(layout, net, layer, name, x, y)) + cls.__stub.Create(messages.point_term_creation_message(layout, net, layer, name, point)) ) @property diff --git a/src/ansys/edb/core/typing/__init__.py b/src/ansys/edb/core/typing/__init__.py new file mode 100644 index 0000000000..fff91f9781 --- /dev/null +++ b/src/ansys/edb/core/typing/__init__.py @@ -0,0 +1,9 @@ +"""This package contains common type definitions used throughout edb codebase.""" + +from typing import Iterable, Union + +import ansys.edb.core.geometry.point_data as point_data +import ansys.edb.core.utility.value as value + +ValueLike = Union[int, float, complex, str, value.Value] +PointLike = Union[point_data.PointData, Iterable[ValueLike]] diff --git a/src/ansys/edb/core/utility/__init__.py b/src/ansys/edb/core/utility/__init__.py index 8ad73d8da0..e1745a8e58 100644 --- a/src/ansys/edb/core/utility/__init__.py +++ b/src/ansys/edb/core/utility/__init__.py @@ -3,5 +3,6 @@ import logging from ansys.edb.core.utility.edb_logging import EDBLogger +from ansys.edb.core.utility.value import Value LOGGER = EDBLogger(level=logging.DEBUG, to_file=False, to_stdout=True) diff --git a/src/ansys/edb/core/utility/conversions.py b/src/ansys/edb/core/utility/conversions.py new file mode 100644 index 0000000000..39bafe8466 --- /dev/null +++ b/src/ansys/edb/core/utility/conversions.py @@ -0,0 +1,48 @@ +"""This module performs conversions from arbitrary user input to explicit types.""" + +from ansys.edb.core import geometry, utility + + +def to_value(val): + """Take a value implicitly convertible to Value and return as Value. + + Parameters + ---------- + val : ansys.edb.core.typing.ValueLike + + Returns + ------- + utility.Value + """ + if isinstance(val, utility.Value): + return val + elif type(val) in [int, float, complex, str]: + return utility.Value(val) + else: + raise TypeError( + f"value-like objects must be either of type Value or int/float/complex/str. - Received '{val}'" + ) + + +def to_point(val): + """Take a value implicitly convertible to PointData and return as PointData. + + Parameters + ---------- + val : ansys.edb.core.typing.PointLike + + Returns + ------- + geometry.PointData + """ + if isinstance(val, geometry.PointData): + return val + try: + if len(val) == 2: + return geometry.PointData(val) + except TypeError: + return geometry.PointData(val) + + raise TypeError( + "point-like objects must be either of type PointData or a list/tuple containing (start, end) or (arc_height)." + ) diff --git a/src/ansys/edb/core/utility/edb_errors.py b/src/ansys/edb/core/utility/edb_errors.py index ce5fcff122..e5cb1fdc5d 100644 --- a/src/ansys/edb/core/utility/edb_errors.py +++ b/src/ansys/edb/core/utility/edb_errors.py @@ -5,7 +5,7 @@ from grpc import StatusCode from grpc._channel import _InactiveRpcError, _MultiThreadedRendezvous -from ansys.edb.core.utility import LOGGER +from ansys.edb.core import utility def handle_grpc_exception(func): @@ -23,7 +23,7 @@ def wrapper(*args, **kwargs): if code == StatusCode.UNAVAILABLE: msg = "Cannot communicate with EDB Server" - LOGGER.error(msg) + utility.LOGGER.error(msg) # rethrow the exception raise return out diff --git a/src/ansys/edb/core/utility/edb_logging.py b/src/ansys/edb/core/utility/edb_logging.py index c8b63d6983..27bd72ab7b 100644 --- a/src/ansys/edb/core/utility/edb_logging.py +++ b/src/ansys/edb/core/utility/edb_logging.py @@ -98,7 +98,7 @@ def __init__(self, level=logging.DEBUG, to_file=False, to_stdout=True, filename= Parameters ---------- - level : str, optional + level : int, optional Level of logging as defined in the package ``logging``. By default 'DEBUG'. to_file : bool, optional To record the logs in a file, by default ``False``. diff --git a/src/ansys/edb/core/utility/value.py b/src/ansys/edb/core/utility/value.py index ce440d6bd6..b752611210 100644 --- a/src/ansys/edb/core/utility/value.py +++ b/src/ansys/edb/core/utility/value.py @@ -1,17 +1,20 @@ """Value Class.""" -from ansys.api.edb.v1.edb_messages_pb2 import EDBObjMessage, ValueMessage -import ansys.api.edb.v1.value_pb2 as value_msgs +from ansys.api.edb.v1 import value_pb2, value_pb2_grpc +from ansys.api.edb.v1.edb_messages_pb2 import ValueMessage -from ansys.edb.core.session import get_value_stub -from ansys.edb.core.utility.edb_errors import handle_grpc_exception +from ansys.edb.core import session +from ansys.edb.core.interface.grpc import messages +from ansys.edb.core.utility import conversions, edb_errors class Value: """Class representing a number or an expression.""" - @handle_grpc_exception - def __init__(self, val): + __stub: value_pb2_grpc.ValueServiceStub = session.StubAccessor(session.StubType.value) + + @edb_errors.handle_grpc_exception + def __init__(self, val, _owner=None): """Initialize Value object. Parameters @@ -24,8 +27,10 @@ def __init__(self, val): elif isinstance(val, Value): self.msg = val.msg elif isinstance(val, str): - temp = value_msgs.ValueTextMessage(text=val, variable_owner=EDBObjMessage(id=0)) - self.msg = get_value_stub().CreateValue(temp) + temp = value_pb2.ValueTextMessage( + text=val, variable_owner=messages.edb_obj_message(_owner) + ) + self.msg = self.__stub.CreateValue(temp) elif isinstance(val, float) or isinstance(val, int): self.msg.constant.real = val self.msg.constant.imag = 0 @@ -35,6 +40,120 @@ def __init__(self, val): else: assert False, "Invalid Value" + def __str__(self): + """Generate a readable string for the value. + + Returns + ------- + str + """ + if self.msg.text: + return self.msg.text + elif self.msg.constant.imag == 0: + return str(self.msg.constant.real) + else: + return str(complex(self.msg.constant.real, self.msg.constant.imag)) + + def __eq__(self, other): + """Compare if two values are equivalent by evaluated value. + + Parameters + ---------- + other : Value + + Returns + ------- + bool + """ + try: + other = conversions.to_value(other) + return self._value == other._value + except TypeError: + return False + + def __add__(self, other): + """Perform addition of two values. + + Parameters + ---------- + other : ansys.edb.typing.ValueLike + + Returns + ------- + Value + """ + other = conversions.to_value(other) + return self.__class__(self._value + other._value) + + def __sub__(self, other): + """Perform subtraction of two values. + + Parameters + ---------- + other : ansys.edb.typing.ValueLike + + Returns + ------- + Value + """ + other = conversions.to_value(other) + return self.__class__(self._value - other._value) + + def __mul__(self, other): + """Perform multiplication of two values. + + Parameters + ---------- + other : ansys.edb.typing.ValueLike + + Returns + ------- + Value + """ + other = conversions.to_value(other) + return self.__class__(self._value * other._value) + + def __truediv__(self, other): + """Perform floating-point division of two values. + + Parameters + ---------- + other : ansys.edb.typing.ValueLike + + Returns + ------- + Value + """ + other = conversions.to_value(other) + return self.__class__(self._value / other._value) + + def __floordiv__(self, other): + """Perform division of two values and return its floor (integer part). + + Parameters + ---------- + other : ansys.edb.typing.ValueLike + + Returns + ------- + Value + """ + other = conversions.to_value(other) + return self.__class__(self._value // other._value) + + def __pow__(self, power, modulo=None): + """Raise a value to the power of another value. + + Parameters + ---------- + power : int, float + + Returns + ------- + Value + """ + return self.__class__(self._value**power) + @property def is_parametric(self): """Is Value object parametric (dependent on variables). @@ -53,57 +172,58 @@ def is_complex(self): ------- bool """ - c = self.complex - return c.imag != 0 + return type(self._value) == complex @property - @handle_grpc_exception def double(self): """Get double from Value object. - A complex number will return the real part - Returns ------- double """ - if self.msg.HasField("constant"): - return self.msg.constant.real - else: - temp = value_msgs.ValueTextMessage( - text=self.msg.text, variable_owner=self.msg.variable_owner - ) - return get_value_stub().GetDouble(temp) + evaluated = self._value + return evaluated.real if type(evaluated) == complex else evaluated @property - @handle_grpc_exception def complex(self): - """Get complex number from Value object. + """Get imaginary value from Value object. Returns ------- - complex + double """ - if self.msg.HasField("constant"): - return complex(self.msg.constant.real, self.msg.constant.imag) - else: - temp = value_msgs.ValueTextMessage( - text=self.msg.text, variable_owner=self.msg.variable_owner - ) - msg = get_value_stub().GetComplex(temp) - return complex(msg.real, msg.imag) + return complex(self._value) @property - def text(self): - """Get text from Value object. + @edb_errors.handle_grpc_exception + def _value(self): + """Evaluate parametric value, if any, and return as number. Returns ------- - str + complex, float """ - if self.msg.text: - return self.msg.text - elif self.msg.constant.imag == 0: - return str(self.msg.constant.real) + if self.is_parametric: + evaluated = self.__stub.GetComplex( + value_pb2.ValueTextMessage( + text=self.msg.text, variable_owner=self.msg.variable_owner + ) + ) else: - return str(complex(self.msg.constant.real, self.msg.constant.imag)) + evaluated = self.msg.constant + + if evaluated.imag == 0: + return evaluated.real + else: + return complex(evaluated.real, evaluated.imag) + + @property + def sqrt(self): + """Compute square root of this value. + + Returns + ------- + Value + """ + return self**0.5 diff --git a/src/ansys/edb/core/utility/variable_server.py b/src/ansys/edb/core/utility/variable_server.py index f169a5ef4d..f0bc6f9626 100644 --- a/src/ansys/edb/core/utility/variable_server.py +++ b/src/ansys/edb/core/utility/variable_server.py @@ -1,11 +1,10 @@ """VariableServer Class.""" from ansys.api.edb.v1.edb_messages_pb2 import EDBObjMessage -import ansys.api.edb.v1.value_pb2 as value_server_msgs import ansys.api.edb.v1.variable_server_pb2 as variable_server_msgs from ..interface.grpc.messages import value_message -from ..session import get_value_stub, get_variable_server_stub +from ..session import get_variable_server_stub from ..utility.edb_errors import handle_grpc_exception from ..utility.value import Value @@ -186,7 +185,6 @@ def create_value(self, val): Value """ if isinstance(val, str): - temp = value_server_msgs.ValueTextMessage(text=val, variable_owner=self.msg) - return Value(get_value_stub().CreateValue(temp)) + return Value(val, self) return Value(val) diff --git a/tests/examples/test_spiral_inductor.py b/tests/examples/test_spiral_inductor.py index 991a8ade9d..40ab79d1bc 100644 --- a/tests/examples/test_spiral_inductor.py +++ b/tests/examples/test_spiral_inductor.py @@ -126,7 +126,7 @@ def create_path(self, layer_name, net_name, width, vertices): ) def create_point_terminal(self, net_name, name, x, y, layer_name): - return PointTerminal.create(self.layout, self.net(net_name), layer_name, name, x, y) + return PointTerminal.create(self.layout, self.net(net_name), layer_name, name, (x, y)) def set_hfss_extents(self, **extents): self.cell.set_hfss_extent_info(**extents) diff --git a/tests/test_point_data.py b/tests/test_point_data.py new file mode 100644 index 0000000000..ab2e7ccdf0 --- /dev/null +++ b/tests/test_point_data.py @@ -0,0 +1,74 @@ +import ansys.api.edb.v1.edb_messages_pb2 as edb_messages_pb2 + +import ansys.edb.core.geometry.point_data as point_data +import ansys.edb.core.utility.value as value +from utils.fixtures import * # noqa + + +@pytest.mark.parametrize( + "p2, expected", + [ + [(1, 2), (2, 3)], + [[1, 2], (2, 3)], + [point_data.PointData((1, 2)), (2, 3)], + [(1 + 2j, 3), (2 + 2j, 4)], + ], +) +def test_addition(p2, expected): + p1 = point_data.PointData((1, 1)) + p3 = p1 + p2 + assert p3.x.complex == expected[0] + assert p3.y.complex == expected[1] + + +@pytest.mark.parametrize( + "p2, expected", + [ + [(1, 2), (4, 3)], + [[1, 2], (4, 3)], + [point_data.PointData((1, 2)), (4, 3)], + [(1 + 2j, 3), (4 - 2j, 2)], + ], +) +def test_subtraction(p2, expected): + p1 = point_data.PointData((5, 5)) + p3 = p1 - p2 + assert p3.x.complex == expected[0] + assert p3.y.complex == expected[1] + + +@pytest.mark.parametrize( + "coord, expected", + [ + [1, False], + [(1, 2), False], + [value.Value(1), False], + [(value.Value(1), value.Value(2)), False], + ["h", True], + [("x", 2), True], + [(1, "y"), True], + ], +) +def test_is_parametric(mocked_stub, coord, expected): + mock = mocked_stub(value, value.Value) + mock.CreateValue.side_effect = lambda payload: edb_messages_pb2.ValueMessage(text=payload.text) + p = point_data.PointData(coord) + assert p.is_parametric == expected + + +@pytest.mark.parametrize("coord, magnitude", [[(3, 4), 5], [1, 0]]) +def test_magnitude(coord, magnitude): + p = point_data.PointData(coord) + assert p.magnitude == magnitude + + +@pytest.mark.parametrize("coord, normalized_coord", [[(3, 4), (3 / 5, 4 / 5)], [1, 0]]) +def test_normalized(coord, normalized_coord): + p = point_data.PointData(coord) + assert p.normalized == point_data.PointData(normalized_coord) + + +@pytest.mark.parametrize("coord1, coord2, dist", [[(1, 2), (4, 6), 5], [1, (1, 1), 0]]) +def test_distance(coord1, coord2, dist): + p1, p2 = point_data.PointData(coord1), point_data.PointData(coord2) + assert p1.distance(p2) == dist diff --git a/tests/test_terminals.py b/tests/test_terminals.py index da724dc38b..ed27932d7b 100644 --- a/tests/test_terminals.py +++ b/tests/test_terminals.py @@ -1,14 +1,7 @@ import ansys.api.edb.v1.term_pb2 as term_pb2 -import ansys.edb.core.interface.grpc.messages as messages -from ansys.edb.core.terminal import ( - BoundaryType, - BundleTerminal, - HfssPIType, - PointTerminal, - SourceTermToGroundType, - Terminal, -) +from ansys.edb.core import terminal +from ansys.edb.core.interface.grpc import messages from ansys.edb.core.utility.port_post_processing_prop import PortPostProcessingProp from ansys.edb.core.utility.rlc import Rlc from utils.fixtures import * # noqa @@ -28,24 +21,24 @@ def _patch(cls): @pytest.fixture def bundle_terminal(edb_obj_msg): - return BundleTerminal(edb_obj_msg) + return terminal.BundleTerminal(edb_obj_msg) @pytest.fixture def point_terminal(edb_obj_msg): - return PointTerminal(edb_obj_msg) + return terminal.PointTerminal(edb_obj_msg) -def test_bundle_terminal_create(patch, point_terminal, bundle_terminal, edb_obj_msg): - mock = patch(BundleTerminal).Create +def test_bundle_terminal_create(mocked_stub, point_terminal, bundle_terminal, edb_obj_msg): + mock = mocked_stub(terminal, terminal.BundleTerminal).Create mock.return_value = edb_obj_msg - bt = BundleTerminal.create([point_terminal, bundle_terminal]) + bt = terminal.BundleTerminal.create([point_terminal, bundle_terminal]) mock.assert_called_once_with( messages.edb_obj_collection_message([point_terminal, bundle_terminal]) ) - assert isinstance(bt, BundleTerminal) + assert isinstance(bt, terminal.BundleTerminal) assert not bt.is_null() assert bt.id == edb_obj_msg.id @@ -53,29 +46,29 @@ def test_bundle_terminal_create(patch, point_terminal, bundle_terminal, edb_obj_ @pytest.mark.parametrize( "term_type, term_cls", [ - (term_pb2.TerminalType.POINT_TERM, PointTerminal), - (term_pb2.TerminalType.BUNDLE_TERM, BundleTerminal), + (term_pb2.TerminalType.POINT_TERM, terminal.PointTerminal), + (term_pb2.TerminalType.BUNDLE_TERM, terminal.BundleTerminal), ], ) -def test_bundle_terminal_get_terminals(patch, bundle_terminal, term_type, term_cls): - get_terminals = patch(BundleTerminal).GetTerminals +def test_bundle_terminal_get_terminals(mocked_stub, bundle_terminal, term_type, term_cls): + get_terminals = mocked_stub(terminal, terminal.BundleTerminal).GetTerminals get_terminals.return_value = expected = create_edb_obj_msgs(2) - get_params = patch(Terminal).GetParams + get_params = mocked_stub(terminal, terminal.Terminal).GetParams get_params.return_value = term_pb2.TermParamsMessage(term_type=term_type) - terminals = bundle_terminal.terminals + terms = bundle_terminal.terminals get_terminals.assert_called_once_with(bundle_terminal.msg) get_params.assert_called() - assert len(terminals) == 2 - for t in terminals: + assert len(terms) == 2 + for t in terms: assert isinstance(t, term_cls) - assert sorted([t.id for t in terminals]) == sorted([msg.id for msg in expected]) + assert sorted([t.id for t in terms]) == sorted([msg.id for msg in expected]) -def test_bundle_terminal_ungroup(patch, bundle_terminal): +def test_bundle_terminal_ungroup(mocked_stub, bundle_terminal): expected = bundle_terminal.msg - mock = patch(BundleTerminal).Ungroup + mock = mocked_stub(terminal, terminal.BundleTerminal).Ungroup mock.return_value = None bundle_terminal.ungroup() @@ -83,23 +76,23 @@ def test_bundle_terminal_ungroup(patch, bundle_terminal): assert bundle_terminal.is_null() -def test_point_terminal_create(patch, layout, net, layer, edb_obj_msg): - mock = patch(PointTerminal).Create +def test_point_terminal_create(mocked_stub, layout, net, layer, edb_obj_msg): + mock = mocked_stub(terminal, terminal.PointTerminal).Create mock.return_value = edb_obj_msg - pt = PointTerminal.create(layout, net, layer, "test-point-term", 1, 2) + pt = terminal.PointTerminal.create(layout, net, layer, "test-point-term", (1, 2)) mock.assert_called_once_with( - messages.point_term_creation_message(layout, net, layer, "test-point-term", 1, 2) + messages.point_term_creation_message(layout, net, layer, "test-point-term", (1, 2)) ) - assert isinstance(pt, PointTerminal) + assert isinstance(pt, terminal.PointTerminal) assert not pt.is_null() assert pt.id == edb_obj_msg.id -def test_point_terminal_get_params(patch, point_terminal, layer): +def test_point_terminal_get_params(mocked_stub, point_terminal, layer): point = (1e-9, 2e-9) - mock = patch(PointTerminal).GetParameters + mock = mocked_stub(terminal, terminal.PointTerminal).GetParameters mock.return_value = messages.point_term_params_message(layer, point) res = point_terminal.params @@ -122,8 +115,8 @@ def test_point_terminal_get_params(patch, point_terminal, layer): ], indirect=["layer_ref"], ) -def test_point_terminal_set_params(patch, point_terminal, layer_ref, point, success): - mock = patch(PointTerminal).SetParameters +def test_point_terminal_set_params(mocked_stub, point_terminal, layer_ref, point, success): + mock = mocked_stub(terminal, terminal.PointTerminal).SetParameters mock.return_value = messages.bool_message(success) point_terminal.params = (layer_ref, point) @@ -133,13 +126,13 @@ def test_point_terminal_set_params(patch, point_terminal, layer_ref, point, succ ) -def test_terminal_get_params(patch, point_terminal, bundle_terminal, layer): - mock = patch(Terminal) +def test_terminal_get_params(mocked_stub, point_terminal, bundle_terminal, layer): + mock = mocked_stub(terminal, terminal.Terminal) mock.GetParams.return_value = term_pb2.TermParamsMessage( term_type=term_pb2.TerminalType.POINT_TERM, - boundary_type=BoundaryType.RLC.value, - term_to_ground=SourceTermToGroundType.POSITIVE.value, - hfss_pi_type=HfssPIType.COAXIAL_SHORTENED.value, + boundary_type=terminal.BoundaryType.RLC.value, + term_to_ground=terminal.SourceTermToGroundType.POSITIVE.value, + hfss_pi_type=terminal.HfssPIType.COAXIAL_SHORTENED.value, ref_layer=layer.msg, bundle_term=bundle_terminal.msg, is_interface=True, @@ -158,9 +151,9 @@ def test_terminal_get_params(patch, point_terminal, bundle_terminal, layer): ), ) - assert point_terminal.boundary_type == BoundaryType.RLC - assert point_terminal.term_to_ground == SourceTermToGroundType.POSITIVE - assert point_terminal.hfss_pi_type == HfssPIType.COAXIAL_SHORTENED + assert point_terminal.boundary_type == terminal.BoundaryType.RLC + assert point_terminal.term_to_ground == terminal.SourceTermToGroundType.POSITIVE + assert point_terminal.hfss_pi_type == terminal.HfssPIType.COAXIAL_SHORTENED assert point_terminal.reference_terminal is None assert equals(point_terminal.reference_layer, layer) assert equals(point_terminal.bundle_terminal, bundle_terminal) @@ -188,13 +181,13 @@ def test_terminal_get_params(patch, point_terminal, bundle_terminal, layer): assert mock.GetParams.call_count == 18 -def test_terminal_set_params(patch, point_terminal, bundle_terminal, layer): - mock = patch(Terminal).SetParams +def test_terminal_set_params(mocked_stub, point_terminal, bundle_terminal, layer): + mock = mocked_stub(terminal, terminal.Terminal).SetParams mock.return_value = None - point_terminal.boundary_type = BoundaryType.RLC - point_terminal.term_to_ground = SourceTermToGroundType.NO_GROUND - point_terminal.hfss_pi_type = HfssPIType.COAXIAL_OPEN + point_terminal.boundary_type = terminal.BoundaryType.RLC + point_terminal.term_to_ground = terminal.SourceTermToGroundType.NO_GROUND + point_terminal.hfss_pi_type = terminal.HfssPIType.COAXIAL_OPEN point_terminal.is_circuit_port = True point_terminal.is_auto_port = False point_terminal.use_reference_from_hierarchy = True diff --git a/tests/utils/fixtures.py b/tests/utils/fixtures.py index 10b6a4f58b..0ed0fffdcf 100644 --- a/tests/utils/fixtures.py +++ b/tests/utils/fixtures.py @@ -10,6 +10,17 @@ from .test_utils import create_edb_obj_msg, generate_random_int +@pytest.fixture +def mocked_stub(mocker): + def _stub(mod, cls): + mock = mocker.Mock() + path = f"{mod.__name__}.{cls.__name__}._{cls.__name__}__stub" + mocker.patch(path, mock) + return mock + + return _stub + + @pytest.fixture(params=[True, False]) def bool_val(request): """Parameterized fixture that returns True and False values