From ab80778e93f3e5d159f9303eca6718c73fe5e39e Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Thu, 13 Jul 2023 23:10:00 +0200 Subject: [PATCH 01/68] introduce first draft of __json_dump__ and __json_load__ --- src/compas/data/data.py | 228 ++++++++++++++---------------------- src/compas/data/encoders.py | 72 ++++++------ src/compas/data/json.py | 10 +- 3 files changed, 132 insertions(+), 178 deletions(-) diff --git a/src/compas/data/data.py b/src/compas/data/data.py index 5d52603a023..7c6836b4cd6 100644 --- a/src/compas/data/data.py +++ b/src/compas/data/data.py @@ -2,7 +2,7 @@ from __future__ import absolute_import from __future__ import division -import json +# import json import hashlib from uuid import uuid4 from uuid import UUID @@ -128,6 +128,25 @@ def __setstate__(self, state): if "guid" in state: self._guid = UUID(state["guid"]) + def __json_dump__(self, minimal=True): + """Return the required information for serialization with the COMPAS JSON serializer.""" + state = { + "dtype": self.dtype, + "data": self.data, + } + if minimal: + return state + state["guid"] = str(self.guid) + return state + + @classmethod + def __json_load__(cls, data, guid=None): + """Construct an object of this type from the provided data to support COMPAS JSON serialization.""" + obj = cls.from_data(data) + if guid: + obj._guid = UUID(guid) + return obj + @property def dtype(self): return "{}/{}".format(".".join(self.__class__.__module__.split(".")[:2]), self.__class__.__name__) @@ -152,10 +171,6 @@ def ToString(self): connected to a panel or other type of string output.""" return str(self) - @property - def jsonstring(self): - return compas.json_dumps(self.data) - @property def guid(self): if not self._guid: @@ -202,79 +217,6 @@ def to_data(self): """ return self.data - @classmethod - def from_json(cls, filepath): - """Construct an object from serialized data contained in a JSON file. - - Parameters - ---------- - filepath : path string | file-like object | URL string - The path, file or URL to the file for serialization. - - Returns - ------- - :class:`~compas.data.Data` - An instance of this object type if the data contained in the JSON file has the correct schema. - - """ - data = compas.json_load(filepath) - return cls.from_data(data) - - def to_json(self, filepath, pretty=False, compact=False): - """Serialize the data representation of an object to a JSON file. - - Parameters - ---------- - filepath : path string or file-like object - The path or file-like object to the file containing the data. - pretty : bool, optional - If True, serialize to a "pretty", human-readable representation. - compact : bool, optional - If True, serialize to a compact representation without any whitespace. - - Returns - ------- - None - - """ - compas.json_dump(self.data, filepath, pretty=pretty, compact=compact) - - @classmethod - def from_jsonstring(cls, string): - """Construct an object from serialized data contained in a JSON string. - - Parameters - ---------- - string : str - The object as a JSON string. - - Returns - ------- - :class:`~compas.data.Data` - An instance of this object type if the data contained in the JSON file has the correct schema. - - """ - data = compas.json_loads(string) - return cls.from_data(data) - - def to_jsonstring(self, pretty=False, compact=False): - """Serialize the data representation of an object to a JSON string. - - Parameters - ---------- - pretty : bool, optional - If True serialize a pretty representation of the data. - compact : bool, optional - If True serialize a compact representation of the data. - - Returns - ------- - str - The object's data dict in JSON string format. - - """ - return compas.json_dumps(self.data, pretty=pretty, compact=compact) - def copy(self, cls=None): """Make an independent copy of the data object. @@ -294,70 +236,6 @@ def copy(self, cls=None): cls = type(self) return cls.from_data(deepcopy(self.data)) - @classmethod - def validate_json(cls, filepath): - """Validate the data contained in the JSON document against the object's JSON data schema. - - Parameters - ---------- - filepath : path string | file-like object | URL string - The path, file or URL to the file for validation. - - Returns - ------- - Any - - """ - from jsonschema import Draft202012Validator - - validator = Draft202012Validator(cls.JSONSCHEMA) # type: ignore - jsondata = json.load(filepath) - validator.validate(jsondata) - return jsondata - - @classmethod - def validate_jsonstring(cls, jsonstring): - """Validate the data contained in the JSON string against the objects's JSON data schema. - - Parameters - ---------- - jsonstring : str - The JSON string for validation. - - Returns - ------- - Any - - """ - from jsonschema import Draft202012Validator - - validator = Draft202012Validator(cls.JSONSCHEMA) # type: ignore - jsondata = json.loads(jsonstring) - validator.validate(jsondata) - return jsondata - - @classmethod - def validate_jsondata(cls, jsondata): - """Validate the JSON data against the objects's JSON data schema. - - The JSON data is the result of parsing a JSON string or a JSON document. - - Parameters - ---------- - jsondata : Any - The JSON data for validation. - - Returns - ------- - Any - - """ - from jsonschema import Draft202012Validator - - validator = Draft202012Validator(cls.JSONSCHEMA) # type: ignore - validator.validate(jsondata) - return jsondata - def sha256(self, as_string=False): """Compute a hash of the data for comparison during version control using the sha256 algorithm. @@ -385,7 +263,71 @@ def sha256(self, as_string=False): """ h = hashlib.sha256() - h.update(self.jsonstring.encode()) + h.update(compas.json_dumps(self).encode()) if as_string: return h.hexdigest() return h.digest() + + # @classmethod + # def validate_json(cls, filepath): + # """Validate the data contained in the JSON document against the object's JSON data schema. + + # Parameters + # ---------- + # filepath : path string | file-like object | URL string + # The path, file or URL to the file for validation. + + # Returns + # ------- + # Any + + # """ + # from jsonschema import Draft202012Validator + + # validator = Draft202012Validator(cls.JSONSCHEMA) # type: ignore + # jsondata = json.load(filepath) + # validator.validate(jsondata) + # return jsondata + + # @classmethod + # def validate_jsonstring(cls, jsonstring): + # """Validate the data contained in the JSON string against the objects's JSON data schema. + + # Parameters + # ---------- + # jsonstring : str + # The JSON string for validation. + + # Returns + # ------- + # Any + + # """ + # from jsonschema import Draft202012Validator + + # validator = Draft202012Validator(cls.JSONSCHEMA) # type: ignore + # jsondata = json.loads(jsonstring) + # validator.validate(jsondata) + # return jsondata + + # @classmethod + # def validate_jsondata(cls, jsondata): + # """Validate the JSON data against the objects's JSON data schema. + + # The JSON data is the result of parsing a JSON string or a JSON document. + + # Parameters + # ---------- + # jsondata : Any + # The JSON data for validation. + + # Returns + # ------- + # Any + + # """ + # from jsonschema import Draft202012Validator + + # validator = Draft202012Validator(cls.JSONSCHEMA) # type: ignore + # validator.validate(jsondata) + # return jsondata diff --git a/src/compas/data/encoders.py b/src/compas/data/encoders.py index f14c449af65..bd7bcf6845e 100644 --- a/src/compas/data/encoders.py +++ b/src/compas/data/encoders.py @@ -96,6 +96,8 @@ class DataEncoder(json.JSONEncoder): """ + minimal = True + def default(self, o): """Return an object in serialized form. @@ -110,29 +112,33 @@ def default(self, o): The serialized object. """ - if hasattr(o, "to_jsondata"): - value = o.to_jsondata() - if hasattr(o, "dtype"): - dtype = o.dtype - else: - dtype = "{}/{}".format( - ".".join(o.__class__.__module__.split(".")[:-1]), - o.__class__.__name__, - ) - - return {"dtype": dtype, "value": value, "guid": str(o.guid)} - - if hasattr(o, "to_data"): - value = o.to_data() - if hasattr(o, "dtype"): - dtype = o.dtype - else: - dtype = "{}/{}".format( - ".".join(o.__class__.__module__.split(".")[:-1]), - o.__class__.__name__, - ) - - return {"dtype": dtype, "value": value, "guid": str(o.guid)} + # if hasattr(o, "to_jsondata"): + # value = o.to_jsondata() + # if hasattr(o, "dtype"): + # dtype = o.dtype + # else: + # dtype = "{}/{}".format( + # ".".join(o.__class__.__module__.split(".")[:-1]), + # o.__class__.__name__, + # ) + + # return {"dtype": dtype, "value": value, "guid": str(o.guid)} + + # if hasattr(o, "to_data"): + # value = o.to_data() + + # if hasattr(o, "dtype"): + # dtype = o.dtype + # else: + # dtype = "{}/{}".format( + # ".".join(o.__class__.__module__.split(".")[:-1]), + # o.__class__.__name__, + # ) + + # return {"dtype": dtype, "value": value, "guid": str(o.guid)} + + if hasattr(o, "__json_dump__"): + return o.__json_dump__(minimal=DataEncoder.minimal) if hasattr(o, "__next__"): return list(o) @@ -154,10 +160,10 @@ def default(self, o): np.uint16, np.uint32, np.uint64, - ), + ), # type: ignore ): return int(o) - if isinstance(o, (np.float_, np.float16, np.float32, np.float64)): + if isinstance(o, (np.float_, np.float16, np.float32, np.float64)): # type: ignore return float(o) if isinstance(o, np.bool_): return bool(o) @@ -243,18 +249,18 @@ def object_hook(self, o): except AttributeError: raise DecoderError("The data type can't be found in the specified module: {}.".format(o["dtype"])) - obj_value = o["value"] + data = o["data"] # Kick-off from_data from a rebuilt Python dictionary instead of the C# data type if IDictionary and isinstance(o, IDictionary[str, object]): - obj_value = {key: obj_value[key] for key in obj_value.Keys} + data = {key: data[key] for key in data.Keys} - if hasattr(cls, "from_jsondata"): - obj = cls.from_jsondata(obj_value) - else: - obj = cls.from_data(obj_value) + # if hasattr(cls, "from_jsondata"): + # obj = cls.from_jsondata(obj_value) + # else: + obj = cls.__json_load__(data, o.get("guid", None)) - if "guid" in o: - obj._guid = uuid.UUID(o["guid"]) + # if "guid" in o: + # obj._guid = uuid.UUID(o["guid"]) return obj diff --git a/src/compas/data/json.py b/src/compas/data/json.py index cb226d70815..cd5579f7bb4 100644 --- a/src/compas/data/json.py +++ b/src/compas/data/json.py @@ -8,7 +8,7 @@ from compas.data import DataDecoder -def json_dump(data, fp, pretty=False, compact=False): +def json_dump(data, fp, pretty=False, compact=False, minimal=True): """Write a collection of COMPAS object data to a JSON file. Parameters @@ -44,18 +44,22 @@ def json_dump(data, fp, pretty=False, compact=False): True """ + DataEncoder.minimal = minimal + with _iotools.open_file(fp, "w") as f: kwargs = {} + if pretty: kwargs["sort_keys"] = True kwargs["indent"] = 4 if compact: kwargs["indent"] = None kwargs["separators"] = (",", ":") + return json.dump(data, f, cls=DataEncoder, **kwargs) -def json_dumps(data, pretty=False, compact=False): +def json_dumps(data, pretty=False, compact=False, minimal=True): """Write a collection of COMPAS objects to a JSON string. Parameters @@ -89,6 +93,8 @@ def json_dumps(data, pretty=False, compact=False): True """ + DataEncoder.minimal = minimal + kwargs = {} if pretty: kwargs["sort_keys"] = True From 55ffe01c5c1cc01e5bb19835720a6ba2ca80eb08 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 17 Jul 2023 20:12:14 +0200 Subject: [PATCH 02/68] test of data data --- src/compas/geometry/primitives/frame.py | 6 +++--- src/compas/geometry/primitives/line.py | 2 +- src/compas/geometry/primitives/plane.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/compas/geometry/primitives/frame.py b/src/compas/geometry/primitives/frame.py index cf055a80a56..54f0f910b2c 100644 --- a/src/compas/geometry/primitives/frame.py +++ b/src/compas/geometry/primitives/frame.py @@ -94,9 +94,9 @@ def __init__(self, point, xaxis, yaxis, **kwargs): def data(self): """dict : The data dictionary that represents the frame.""" return { - "point": self.point, - "xaxis": self.xaxis, - "yaxis": self.yaxis, + "point": self.point.data, + "xaxis": self.xaxis.data, + "yaxis": self.yaxis.data, } @data.setter diff --git a/src/compas/geometry/primitives/line.py b/src/compas/geometry/primitives/line.py index f900a07f2b3..0c9568b4080 100644 --- a/src/compas/geometry/primitives/line.py +++ b/src/compas/geometry/primitives/line.py @@ -72,7 +72,7 @@ def __init__(self, p1, p2, **kwargs): @property def data(self): """dict : The data dictionary that represents the line.""" - return {"start": self.start, "end": self.end} + return {"start": self.start.data, "end": self.end.data} @data.setter def data(self, data): diff --git a/src/compas/geometry/primitives/plane.py b/src/compas/geometry/primitives/plane.py index dc57e6ae2de..1285dabb200 100644 --- a/src/compas/geometry/primitives/plane.py +++ b/src/compas/geometry/primitives/plane.py @@ -66,7 +66,7 @@ def __init__(self, point, normal, **kwargs): @property def data(self): """dict : The data dictionary that represents the plane.""" - return {"point": self.point, "normal": self.normal} + return {"point": self.point.data, "normal": self.normal.data} @data.setter def data(self, data): From fb1ed5c5ca7915c378534a46eff41fc5e08704e6 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Wed, 9 Aug 2023 23:03:31 +0200 Subject: [PATCH 03/68] color data and data schema --- CHANGELOG.md | 3 +++ src/compas/colors/color.py | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f00beeaa86..58f7f6ac4ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.geometry.Surface.point_at`. * Added `compas.geometry.Surface.normal_at`. * Added `compas.geometry.Surface.frame_at`. +* Added `compas.color.Color.DATASCHEMA`. +* Added `compas.color.Color.data`. ### Changed @@ -208,6 +210,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed class attribute `CONTEXT` from `compas.artists.Artist`. * Removed class attribute `AVAILABLE_CONTEXTS` form `compas.artists.Artist`. * Removed `compas.geometry.Primitive`. +* Removed `compas.color.Color.data` property setter, and `compas.color.Color.from_data`. ## [1.17.5] 2023-02-16 diff --git a/src/compas/colors/color.py b/src/compas/colors/color.py index 6d7d3c6b251..a5738536270 100644 --- a/src/compas/colors/color.py +++ b/src/compas/colors/color.py @@ -104,6 +104,17 @@ class Color(Data): """ + DATASCHEMA = { + "type": "object", + "properties": { + "red": {"type": "number", "minimum": 0.0, "maximum": 1.0}, + "green": {"type": "number", "minimum": 0.0, "maximum": 1.0}, + "blue": {"type": "number", "minimum": 0.0, "maximum": 1.0}, + "alpha": {"type": "number", "minimum": 0.0, "maximum": 1.0}, + }, + "required": ["red", "green", "blue", "alpha"], + } + def __init__(self, red, green, blue, alpha=1.0, **kwargs): super(Color, self).__init__(**kwargs) self._r = 1.0 @@ -123,17 +134,6 @@ def __init__(self, red, green, blue, alpha=1.0, **kwargs): def data(self): return {"red": self.r, "green": self.g, "blue": self.b, "alpha": self.a} - @data.setter - def data(self, data): - self.r = data["red"] - self.g = data["green"] - self.b = data["blue"] - self.a = data["alpha"] - - @classmethod - def from_data(cls, data): - return cls(data["red"], data["green"], data["blue"], data["alpha"]) - # -------------------------------------------------------------------------- # properties # -------------------------------------------------------------------------- From 14620fd7f3208c78263449e21834f25deef757ed Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Wed, 9 Aug 2023 23:06:54 +0200 Subject: [PATCH 04/68] vector --- CHANGELOG.md | 5 +++-- src/compas/geometry/vector.py | 34 ++++++++-------------------------- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58f7f6ac4ef..d9a4cb65adb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,7 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.geometry.Sphere.JSONSCHEMA`. * Added `compas.geometry.Torus.JSONSCHEMA`. * Added `compas.geometry.Quaternion.JSONSCHEMA`. -* Added `compas.geometry.Vector.JSONSCHEMA`. +* Added class attribute `compas.geometry.Vector.DATASCHEMA`. * Added `compas.datastructures.Halfedge.halfedge_loop_vertices`. * Added `compas.datastructures.Halfedge.halfedge_strip_faces`. * Added `compas.datastructures.Mesh.vertex_point`. @@ -197,7 +197,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed `compas.geometry.Sphere.DATASCHEMA` and `compas.geometry.Sphere.JSONSCHEMANAME`. * Removed `compas.geometry.Torus.DATASCHEMA` and `compas.geometry.Torus.JSONSCHEMANAME`. * Removed `compas.geometry.Quaternion.DATASCHEMA` and `compas.geometry.Quaternion.JSONSCHEMANAME`. -* Removed `compas.geometry.Vector.DATASCHEMA` and `compas.geometry.Vector.JSONSCHEMANAME`. +* Removed property `compas.geometry.Vector.DATASCHEMA` and property `compas.geometry.Vector.JSONSCHEMANAME`. * Removed `compas.datastructures.Graph.key_index`and `compas.datastructures.Graph.index_key`. * Removed `compas.datastructures.Graph.uv_index`and `compas.datastructures.Graph.index_uv`. * Removed `compas.datastructures.Halfedge.key_index` and `compas.datastructures.Halfedge.index_key`. @@ -211,6 +211,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed class attribute `AVAILABLE_CONTEXTS` form `compas.artists.Artist`. * Removed `compas.geometry.Primitive`. * Removed `compas.color.Color.data` property setter, and `compas.color.Color.from_data`. +* Removed `compas.geometry.Vector.data` property setter. ## [1.17.5] 2023-02-16 diff --git a/src/compas/geometry/vector.py b/src/compas/geometry/vector.py index 215ffb09e1f..534e2cad169 100644 --- a/src/compas/geometry/vector.py +++ b/src/compas/geometry/vector.py @@ -65,7 +65,7 @@ class Vector(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "array", "minItems": 3, "maxItems": 3, @@ -82,7 +82,13 @@ def __init__(self, x, y, z=0.0, **kwargs): self.z = z def __repr__(self): - return "Vector({0:.{3}f}, {1:.{3}f}, {2:.{3}f})".format(self.x, self.y, self.z, PRECISION[:1]) + return "{0}(x={1:.{4}f}, y={2:.{4}f}, z={3:.{4}f})".format( + type(self).__name__, + self.x, + self.y, + self.z, + PRECISION[:1], + ) def __len__(self): return 3 @@ -172,34 +178,10 @@ def __ipow__(self, n): @property def data(self): - """dict : The data dictionary that represents the vector.""" return list(self) - @data.setter - def data(self, data): - self.x = data[0] - self.y = data[1] - self.z = data[2] - @classmethod def from_data(cls, data): - """Construct a vector from a data dict. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Vector` - The vector constructed from the provided data. - - Examples - -------- - >>> Vector.from_data([0.0, 0.0, 1.0]) - Vector(0.000, 0.000, 1.000) - """ return cls(*data) # ========================================================================== From 8f216d9fe45195ec3c3fd036c9d80aedfa4d649c Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Wed, 9 Aug 2023 23:19:19 +0200 Subject: [PATCH 05/68] update changes in log --- CHANGELOG.md | 124 +++++++++++++++++++++++++-------------------------- 1 file changed, 61 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9a4cb65adb..51f73d47b93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,37 +13,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added representation for features in `compas.datastructures.Part`. * Added `split` and `split_by_length` to `compas.geometry.Polyline`. * Added `compas.rpc.XFunc`. -* Added `compas.data.Data.validate_jsonstring`. -* Added `compas.data.Data.validate_jsondata`. -* Added `compas.data.Data.JSONSCHEMA`. -* Added `compas.data.json_validate`. -* Added `compas.datastructures.Graph.JSONSCHEMA`. +* Added attribute `compas.color.Color.DATASCHEMA`. +* Added attribute `compas.data.Data.DATASCHEMA`. +* Added attribute `compas.datastructures.Graph.DATASCHEMA`. +* Added attribute `compas.datastructures.Halfedge.DATASCHEMA`. +* Added attribute `compas.datastructures.Halfface.DATASCHEMA`. +* Added attribute `compas.geometry.Arc.DATASCHEMA`. +* Added attribute `compas.geometry.Bezier.DATASCHEMA`. +* Added attribute `compas.geometry.Box.DATASCHEMA`. +* Added attribute `compas.geometry.Capsule.DATASCHEMA`. +* Added attribute `compas.geometry.Circle.DATASCHEMA`. +* Added attribute `compas.geometry.Cone.DATASCHEMA`. +* Added attribute `compas.geometry.Cylinder.DATASCHEMA`. +* Added attribute `compas.geometry.Ellipse.DATASCHEMA`. +* Added attribute `compas.geometry.Frame.DATASCHEMA`. +* Added attribute `compas.geometry.Line.DATASCHEMA`. +* Added attribute `compas.geometry.NurbsCurve.DATASCHEMA`. +* Added attribute `compas.geometry.NurbsSurface.DATASCHEMA`. +* Added attribute `compas.geometry.Plane.DATASCHEMA`. +* Added attribute `compas.geometry.Point.DATASCHEMA`. +* Added attribute `compas.geometry.Pointcloud.DATASCHEMA`. +* Added attribute `compas.geometry.Polygon.DATASCHEMA`. +* Added attribute `compas.geometry.Polyhedron.DATASCHEMA`. +* Added attribute `compas.geometry.Polyline.DATASCHEMA`. +* Added attribute `compas.geometry.Sphere.DATASCHEMA`. +* Added attribute `compas.geometry.Torus.DATASCHEMA`. +* Added attribute `compas.geometry.Quaternion.DATASCHEMA`. +* Added attribute `compas.geometry.Vector.DATASCHEMA`. +* Added implementation of property `compas.color.Color.data`. +* Added `compas.data.Data.validate_data`. * Added `compas.datastructures.Graph.to_jsondata`. * Added `compas.datastructures.Graph.from_jsondata`. -* Added `compas.datastructures.Halfedge.JSONSCHEMA`. -* Added `compas.datastructures.Halfface.JSONSCHEMA`. -* Added `compas.geometry.Arc.JSONSCHEMA`. -* Added `compas.geometry.Bezier.JSONSCHEMA`. -* Added `compas.geometry.Box.JSONSCHEMA`. -* Added `compas.geometry.Capsule.JSONSCHEMA`. -* Added `compas.geometry.Circle.JSONSCHEMA`. -* Added `compas.geometry.Cone.JSONSCHEMA`. -* Added `compas.geometry.Cylinder.JSONSCHEMA`. -* Added `compas.geometry.Ellipse.JSONSCHEMA`. -* Added `compas.geometry.Frame.JSONSCHEMA`. -* Added `compas.geometry.Line.JSONSCHEMA`. -* Added `compas.geometry.NurbsCurve.JSONSCHEMA`. -* Added `compas.geometry.NurbsSurface.JSONSCHEMA`. -* Added `compas.geometry.Plane.JSONSCHEMA`. -* Added `compas.geometry.Point.JSONSCHEMA`. -* Added `compas.geometry.Pointcloud.JSONSCHEMA`. -* Added `compas.geometry.Polygon.JSONSCHEMA`. -* Added `compas.geometry.Polyhedron.JSONSCHEMA`. -* Added `compas.geometry.Polyline.JSONSCHEMA`. -* Added `compas.geometry.Sphere.JSONSCHEMA`. -* Added `compas.geometry.Torus.JSONSCHEMA`. -* Added `compas.geometry.Quaternion.JSONSCHEMA`. -* Added class attribute `compas.geometry.Vector.DATASCHEMA`. * Added `compas.datastructures.Halfedge.halfedge_loop_vertices`. * Added `compas.datastructures.Halfedge.halfedge_strip_faces`. * Added `compas.datastructures.Mesh.vertex_point`. @@ -59,9 +59,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.datastructures.Graph.node_index` and `compas.datastructures.Graph.index_node`. * Added `compas.datastructures.Graph.edge_index` and `compas.datastructures.Graph.index_edge`. * Added `compas.datastructures.Halfedge.vertex_index` and `compas.datastructures.Halfedge.index_vertex`. -* Added `compas.geometry.trimesh_descent_numpy`. -* Added `compas.geometry.trimesh_gradient_numpy`. -* Added a deprecation warning when using `Artist` for `Plotter`. * Added `compas.geometry.Hyperbola`. * Added `compas.geometry.Parabola`. * Added `compas.geometry.PlanarSurface`. @@ -69,6 +66,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.geometry.SphericalSurface`. * Added `compas.geometry.ConicalSurface`. * Added `compas.geometry.ToroidalSurface`. +* Added `compas.geometry.trimesh_descent_numpy`. +* Added `compas.geometry.trimesh_gradient_numpy`. * Added `compas.geometry.boolean_union_polygon_polygon` pluggable. * Added `compas.geometry.boolean_intersection_polygon_polygon` pluggable. * Added `compas.geometry.boolean_difference_polygon_polygon` pluggable. @@ -91,8 +90,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.geometry.Surface.point_at`. * Added `compas.geometry.Surface.normal_at`. * Added `compas.geometry.Surface.frame_at`. -* Added `compas.color.Color.DATASCHEMA`. -* Added `compas.color.Color.data`. +* Added a deprecation warning when using `Artist` for `Plotter`. ### Changed @@ -168,36 +166,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed `compas.datastructures.Halfedge.get_any_vertices`. * Removed `compas.datastructures.Halfedge.get_any_face`. * Removed "schemas" folder and all contained `.json` files from `compas.data`. -* Removed `compas.data.Data.DATASCHEMA`. -* Removed `compas.data.Data.JSONSCHEMANAME`. * Removed `compas.data.Data.jsondefinititions`. * Removed `compas.data.Data.jsonvalidator`. * Removed `compas.data.Data.validate_data`. -* Removed `compas.datastructures.Graph.DATASCHEMA` and `compas.datastructures.Graph.JSONSCHEMANAME`. -* Removed `compas.datastructures.Halfedge.DATASCHEMA` and `compas.datastructures.Halfedge.JSONSCHEMANAME`. -* Removed `compas.datastructures.Halfface.DATASCHEMA` and `compas.datastructures.Halfface.JSONSCHEMANAME`. -* Removed `compas.geometry.Arc.DATASCHEMA` and `compas.geometry.Arc.JSONSCHEMANAME`. -* Removed `compas.geometry.Bezier.DATASCHEMA` and `compas.geometry.Bezier.JSONSCHEMANAME`. -* Removed `compas.geometry.Box.DATASCHEMA` and `compas.geometry.Box.JSONSCHEMANAME`. -* Removed `compas.geometry.Capsule.DATASCHEMA` and `compas.geometry.Capsule.JSONSCHEMANAME`. -* Removed `compas.geometry.Circle.DATASCHEMA` and `compas.geometry.Circle.JSONSCHEMANAME`. -* Removed `compas.geometry.Cone.DATASCHEMA` and `compas.geometry.Cone.JSONSCHEMANAME`. -* Removed `compas.geometry.Cylinder.DATASCHEMA` and `compas.geometry.Cylinder.JSONSCHEMANAME`. -* Removed `compas.geometry.Ellipse.DATASCHEMA` and `compas.geometry.Ellipse.JSONSCHEMANAME`. -* Removed `compas.geometry.Frame.DATASCHEMA` and `compas.geometry.Frame.JSONSCHEMANAME`. -* Removed `compas.geometry.Line.DATASCHEMA` and `compas.geometry.Line.JSONSCHEMANAME`. -* Removed `compas.geometry.NurbsCurve.DATASCHEMA` and `compas.geometry.NurbsCurve.JSONSCHEMANAME`. -* Removed `compas.geometry.NurbsSurface.DATASCHEMA` and `compas.geometry.NurbsSurface.JSONSCHEMANAME`. -* Removed `compas.geometry.Plane.DATASCHEMA` and `compas.geometry.Plane.JSONSCHEMANAME`. -* Removed `compas.geometry.Point.DATASCHEMA` and `compas.geometry.Point.JSONSCHEMANAME`. -* Removed `compas.geometry.Pointcloud.DATASCHEMA` and `compas.geometry.Pointcloud.JSONSCHEMANAME`. -* Removed `compas.geometry.Polygon.DATASCHEMA` and `compas.geometry.Polygon.JSONSCHEMANAME`. -* Removed `compas.geometry.Polyhedron.DATASCHEMA` and `compas.geometry.Polyhedron.JSONSCHEMANAME`. -* Removed `compas.geometry.Polyline.DATASCHEMA` and `compas.geometry.Polyline.JSONSCHEMANAME`. -* Removed `compas.geometry.Sphere.DATASCHEMA` and `compas.geometry.Sphere.JSONSCHEMANAME`. -* Removed `compas.geometry.Torus.DATASCHEMA` and `compas.geometry.Torus.JSONSCHEMANAME`. -* Removed `compas.geometry.Quaternion.DATASCHEMA` and `compas.geometry.Quaternion.JSONSCHEMANAME`. -* Removed property `compas.geometry.Vector.DATASCHEMA` and property `compas.geometry.Vector.JSONSCHEMANAME`. +* Removed properties `compas.data.Data.DATASCHEMA` and `compas.data.Data.JSONSCHEMANAME`. +* Removed properties `compas.datastructures.Graph.DATASCHEMA` and `compas.datastructures.Graph.JSONSCHEMANAME`. +* Removed properties `compas.datastructures.Halfedge.DATASCHEMA` and `compas.datastructures.Halfedge.JSONSCHEMANAME`. +* Removed properties `compas.datastructures.Halfface.DATASCHEMA` and `compas.datastructures.Halfface.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Arc.DATASCHEMA` and `compas.geometry.Arc.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Bezier.DATASCHEMA` and `compas.geometry.Bezier.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Box.DATASCHEMA` and `compas.geometry.Box.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Capsule.DATASCHEMA` and `compas.geometry.Capsule.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Circle.DATASCHEMA` and `compas.geometry.Circle.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Cone.DATASCHEMA` and `compas.geometry.Cone.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Cylinder.DATASCHEMA` and `compas.geometry.Cylinder.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Ellipse.DATASCHEMA` and `compas.geometry.Ellipse.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Frame.DATASCHEMA` and `compas.geometry.Frame.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Line.DATASCHEMA` and `compas.geometry.Line.JSONSCHEMANAME`. +* Removed properties `compas.geometry.NurbsCurve.DATASCHEMA` and `compas.geometry.NurbsCurve.JSONSCHEMANAME`. +* Removed properties `compas.geometry.NurbsSurface.DATASCHEMA` and `compas.geometry.NurbsSurface.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Plane.DATASCHEMA` and `compas.geometry.Plane.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Point.DATASCHEMA` and `compas.geometry.Point.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Pointcloud.DATASCHEMA` and `compas.geometry.Pointcloud.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Polygon.DATASCHEMA` and `compas.geometry.Polygon.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Polyhedron.DATASCHEMA` and `compas.geometry.Polyhedron.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Polyline.DATASCHEMA` and `compas.geometry.Polyline.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Sphere.DATASCHEMA` and `compas.geometry.Sphere.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Torus.DATASCHEMA` and `compas.geometry.Torus.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Quaternion.DATASCHEMA` and `compas.geometry.Quaternion.JSONSCHEMANAME`. +* Removed properties `compas.geometry.Vector.DATASCHEMA` and `compas.geometry.Vector.JSONSCHEMANAME`. * Removed `compas.datastructures.Graph.key_index`and `compas.datastructures.Graph.index_key`. * Removed `compas.datastructures.Graph.uv_index`and `compas.datastructures.Graph.index_uv`. * Removed `compas.datastructures.Halfedge.key_index` and `compas.datastructures.Halfedge.index_key`. @@ -210,8 +207,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed class attribute `CONTEXT` from `compas.artists.Artist`. * Removed class attribute `AVAILABLE_CONTEXTS` form `compas.artists.Artist`. * Removed `compas.geometry.Primitive`. -* Removed `compas.color.Color.data` property setter, and `compas.color.Color.from_data`. -* Removed `compas.geometry.Vector.data` property setter. +* Removed setter of property `compas.color.Color.data` +* Removed classmethod `compas.color.Color.from_data`. +* Removed setter of property `compas.geometry.Vector.data`. ## [1.17.5] 2023-02-16 From 6ba222b49d766f0ec06900cd11b840dc1310ba0a Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Wed, 9 Aug 2023 23:32:46 +0200 Subject: [PATCH 06/68] updated base data class --- CHANGELOG.md | 10 +++- src/compas/data/data.py | 105 ++++++++-------------------------------- 2 files changed, 28 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f73d47b93..f6f39da603f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added attribute `compas.geometry.Vector.DATASCHEMA`. * Added implementation of property `compas.color.Color.data`. * Added `compas.data.Data.validate_data`. +* Added `compas.data.Data.__jsondump__`. +* Added `compas.data.Data.__jsonload__`. * Added `compas.datastructures.Graph.to_jsondata`. * Added `compas.datastructures.Graph.from_jsondata`. * Added `compas.datastructures.Halfedge.halfedge_loop_vertices`. @@ -168,7 +170,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed "schemas" folder and all contained `.json` files from `compas.data`. * Removed `compas.data.Data.jsondefinititions`. * Removed `compas.data.Data.jsonvalidator`. -* Removed `compas.data.Data.validate_data`. +* Removed `compas.data.Data.validate_json`. +* Removed `compas.data.Data.validate_jsondata`. +* Removed `compas.data.Data.validate_jsonstring`. +* Removed `compas.data.Data.__getstate__`. +* Removed `compas.data.Data.__setstate__`. +* Removed setter of property `compas.data.Data.data`. * Removed properties `compas.data.Data.DATASCHEMA` and `compas.data.Data.JSONSCHEMANAME`. * Removed properties `compas.datastructures.Graph.DATASCHEMA` and `compas.datastructures.Graph.JSONSCHEMANAME`. * Removed properties `compas.datastructures.Halfedge.DATASCHEMA` and `compas.datastructures.Halfedge.JSONSCHEMANAME`. @@ -207,7 +214,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed class attribute `CONTEXT` from `compas.artists.Artist`. * Removed class attribute `AVAILABLE_CONTEXTS` form `compas.artists.Artist`. * Removed `compas.geometry.Primitive`. -* Removed setter of property `compas.color.Color.data` * Removed classmethod `compas.color.Color.from_data`. * Removed setter of property `compas.geometry.Vector.data`. diff --git a/src/compas/data/data.py b/src/compas/data/data.py index 7c6836b4cd6..94a42efb404 100644 --- a/src/compas/data/data.py +++ b/src/compas/data/data.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import division -# import json import hashlib from uuid import uuid4 from uuid import UUID @@ -104,7 +103,7 @@ class Data(object): """ - JSONSCHEMA = {} + DATASCHEMA = {} def __init__(self, name=None): self._guid = None @@ -112,23 +111,7 @@ def __init__(self, name=None): if name: self.name = name - def __getstate__(self): - """Return the object data for state serialization with older pickle protocols.""" - return { - "__dict__": self.__dict__, - "dtype": self.dtype, - "data": self.data, - "guid": str(self.guid), - } - - def __setstate__(self, state): - """Assign a deserialized state to the object data to support older pickle protocols.""" - self.__dict__.update(state["__dict__"]) - self.data = state["data"] - if "guid" in state: - self._guid = UUID(state["guid"]) - - def __json_dump__(self, minimal=True): + def __jsondump__(self, minimal=True): """Return the required information for serialization with the COMPAS JSON serializer.""" state = { "dtype": self.dtype, @@ -140,7 +123,7 @@ def __json_dump__(self, minimal=True): return state @classmethod - def __json_load__(cls, data, guid=None): + def __jsonload__(cls, data, guid=None): """Construct an object of this type from the provided data to support COMPAS JSON serialization.""" obj = cls.from_data(data) if guid: @@ -155,10 +138,6 @@ def dtype(self): def data(self): raise NotImplementedError - @data.setter - def data(self, data): - raise NotImplementedError - def ToString(self): """Converts the instance to a string. @@ -202,9 +181,7 @@ def from_data(cls, data): An instance of this object type if the data contained in the dict has the correct schema. """ - obj = cls() - obj.data = data - return obj + return cls(**data) def to_data(self): """Convert an object to its native data representation. @@ -268,66 +245,24 @@ def sha256(self, as_string=False): return h.hexdigest() return h.digest() - # @classmethod - # def validate_json(cls, filepath): - # """Validate the data contained in the JSON document against the object's JSON data schema. - - # Parameters - # ---------- - # filepath : path string | file-like object | URL string - # The path, file or URL to the file for validation. - - # Returns - # ------- - # Any - - # """ - # from jsonschema import Draft202012Validator - - # validator = Draft202012Validator(cls.JSONSCHEMA) # type: ignore - # jsondata = json.load(filepath) - # validator.validate(jsondata) - # return jsondata - - # @classmethod - # def validate_jsonstring(cls, jsonstring): - # """Validate the data contained in the JSON string against the objects's JSON data schema. - - # Parameters - # ---------- - # jsonstring : str - # The JSON string for validation. - - # Returns - # ------- - # Any - - # """ - # from jsonschema import Draft202012Validator - - # validator = Draft202012Validator(cls.JSONSCHEMA) # type: ignore - # jsondata = json.loads(jsonstring) - # validator.validate(jsondata) - # return jsondata - - # @classmethod - # def validate_jsondata(cls, jsondata): - # """Validate the JSON data against the objects's JSON data schema. + @classmethod + def validate_data(cls, data): + """Validate the data against the object's data schema. - # The JSON data is the result of parsing a JSON string or a JSON document. + The data is the raw data that can be used to construct an object of this type with the classmethod ``from_data``. - # Parameters - # ---------- - # jsondata : Any - # The JSON data for validation. + Parameters + ---------- + data : Any + The data for validation. - # Returns - # ------- - # Any + Returns + ------- + Any - # """ - # from jsonschema import Draft202012Validator + """ + from jsonschema import Draft202012Validator - # validator = Draft202012Validator(cls.JSONSCHEMA) # type: ignore - # validator.validate(jsondata) - # return jsondata + validator = Draft202012Validator(cls.DATASCHEMA) # type: ignore + validator.validate(data) + return data From 35978cc128c19db650c73a51bd45413b6953f19c Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Thu, 10 Aug 2023 00:08:11 +0200 Subject: [PATCH 07/68] remove validate_data from validators --- CHANGELOG.md | 1 + src/compas/data/__init__.py | 2 -- src/compas/data/validators.py | 52 ----------------------------------- 3 files changed, 1 insertion(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f39da603f..449aaf88972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -216,6 +216,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed `compas.geometry.Primitive`. * Removed classmethod `compas.color.Color.from_data`. * Removed setter of property `compas.geometry.Vector.data`. +* Removed `validate_data` from `compas.data.validators`. ## [1.17.5] 2023-02-16 diff --git a/src/compas/data/__init__.py b/src/compas/data/__init__.py index 44e1154e6e2..21fc17cb674 100644 --- a/src/compas/data/__init__.py +++ b/src/compas/data/__init__.py @@ -8,7 +8,6 @@ from .validators import is_float3 from .validators import is_float4x4 from .validators import is_item_iterable -from .validators import validate_data from .encoders import DataEncoder from .encoders import DataDecoder from .data import Data @@ -31,5 +30,4 @@ "json_loads", "json_dump", "json_dumps", - "validate_data", ] diff --git a/src/compas/data/validators.py b/src/compas/data/validators.py index b0c5f8521b1..4f6f17b8dc5 100644 --- a/src/compas/data/validators.py +++ b/src/compas/data/validators.py @@ -7,12 +7,6 @@ except NameError: basestring = str -import os -import json - -from compas.data.encoders import DataEncoder -from compas.data.encoders import DataDecoder - def is_sequence_of_str(items): """Verify that the sequence contains only items of type str. @@ -247,49 +241,3 @@ def is_sequence_of_iterable(items): """ return all(is_item_iterable(item) for item in items) - - -def validate_data(data, cls): - """Validate data against the data and json schemas of an object class. - - Parameters - ---------- - data : dict - The data representation of an object. - cls : Type[:class:`~compas.data.Data`] - The data object class. - - Returns - ------- - dict - The validated data dict. - - Raises - ------ - jsonschema.exceptions.ValidationError - - """ - from jsonschema import RefResolver, Draft7Validator - from jsonschema.exceptions import ValidationError - - here = os.path.dirname(__file__) - - schema_name = "{}.json".format(cls.__name__.lower()) - schema_path = os.path.join(here, "schemas", schema_name) - with open(schema_path, "r") as fp: - schema = json.load(fp) - - definitions_path = os.path.join(here, "schemas", "compas.json") - with open(definitions_path, "r") as fp: - definitions = json.load(fp) - - resolver = RefResolver.from_schema(definitions) - validator = Draft7Validator(schema, resolver=resolver) - - try: - validator.validate(data) - except ValidationError as e: - print("Validation against the JSON schema of this object failed.") - raise e - - return json.loads(json.dumps(data, cls=DataEncoder), cls=DataDecoder) From 4bfe59bfd893a41b8ce68fa49364321558c97a4b Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Thu, 10 Aug 2023 00:25:05 +0200 Subject: [PATCH 08/68] removed validate_json --- CHANGELOG.md | 1 + src/compas/data/json.py | 44 ----------------------------------------- 2 files changed, 1 insertion(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 449aaf88972..bc84a7998d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -217,6 +217,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed classmethod `compas.color.Color.from_data`. * Removed setter of property `compas.geometry.Vector.data`. * Removed `validate_data` from `compas.data.validators`. +* Removed `json_validate` from `compas.data.json`. ## [1.17.5] 2023-02-16 diff --git a/src/compas/data/json.py b/src/compas/data/json.py index cd5579f7bb4..f74f499a33a 100644 --- a/src/compas/data/json.py +++ b/src/compas/data/json.py @@ -170,47 +170,3 @@ def json_loads(s): """ return json.loads(s, cls=DataDecoder) - - -def json_validate(filepath, schema): - """Validates a JSON document with respect to a schema and return the JSON object instance if it is valid. - - Parameters - ---------- - filepath : path string | file-like object | URL string - The filepath of the JSON document. - schema : string - The JSON schema. - - Raises - ------ - jsonschema.exceptions.SchemaError - If the schema itself is invalid. - jsonschema.exceptions.ValidationError - If the document is invalid with respect to the schema. - - Returns - ------- - object - The JSON object contained in the document. - - """ - import jsonschema - import jsonschema.exceptions - - data = json_load(filepath) - - try: - jsonschema.validate(data, schema) - except jsonschema.exceptions.SchemaError as e: - print("The provided schema is invalid:\n\n{}\n\n".format(schema)) - raise e - except jsonschema.exceptions.ValidationError as e: - print( - "The provided JSON document is invalid compared to the provided schema:\n\n{}\n\n{}\n\n".format( - schema, data - ) - ) - raise e - - return data From 8d692bb1f28c095a263aa265499282d89c694c07 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Thu, 10 Aug 2023 09:37:43 +0200 Subject: [PATCH 09/68] use jsondump and jsonload --- src/compas/data/encoders.py | 44 ++++++------------------------------- 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/src/compas/data/encoders.py b/src/compas/data/encoders.py index bd7bcf6845e..5d64b7b0d9b 100644 --- a/src/compas/data/encoders.py +++ b/src/compas/data/encoders.py @@ -4,9 +4,8 @@ import json import platform -import uuid -from compas.data.exceptions import DecoderError +from .exceptions import DecoderError IDictionary = None numpy_support = False @@ -112,33 +111,9 @@ def default(self, o): The serialized object. """ - # if hasattr(o, "to_jsondata"): - # value = o.to_jsondata() - # if hasattr(o, "dtype"): - # dtype = o.dtype - # else: - # dtype = "{}/{}".format( - # ".".join(o.__class__.__module__.split(".")[:-1]), - # o.__class__.__name__, - # ) - - # return {"dtype": dtype, "value": value, "guid": str(o.guid)} - - # if hasattr(o, "to_data"): - # value = o.to_data() - - # if hasattr(o, "dtype"): - # dtype = o.dtype - # else: - # dtype = "{}/{}".format( - # ".".join(o.__class__.__module__.split(".")[:-1]), - # o.__class__.__name__, - # ) - - # return {"dtype": dtype, "value": value, "guid": str(o.guid)} - - if hasattr(o, "__json_dump__"): - return o.__json_dump__(minimal=DataEncoder.minimal) + + if hasattr(o, "__jsondump__"): + return o.__jsondump__(minimal=DataEncoder.minimal) if hasattr(o, "__next__"): return list(o) @@ -239,7 +214,8 @@ def object_hook(self, o): except ValueError: raise DecoderError( "The data type of the object should be in the following format: '{}/{}'".format( - o.__class__.__module__, o.__class__.__name__ + o.__class__.__module__, + o.__class__.__name__, ) ) @@ -255,12 +231,6 @@ def object_hook(self, o): if IDictionary and isinstance(o, IDictionary[str, object]): data = {key: data[key] for key in data.Keys} - # if hasattr(cls, "from_jsondata"): - # obj = cls.from_jsondata(obj_value) - # else: - obj = cls.__json_load__(data, o.get("guid", None)) - - # if "guid" in o: - # obj._guid = uuid.UUID(o["guid"]) + obj = cls.__jsonload__(data, o.get("guid", None)) return obj From 5193683e26bd96d629fcec158212d588b4cca5a2 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Thu, 10 Aug 2023 09:37:57 +0200 Subject: [PATCH 10/68] add schema module --- src/compas/data/schema.py | 294 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 src/compas/data/schema.py diff --git a/src/compas/data/schema.py b/src/compas/data/schema.py new file mode 100644 index 00000000000..3223bda3d17 --- /dev/null +++ b/src/compas/data/schema.py @@ -0,0 +1,294 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import json + + +def dataclass_dataschema(cls): + """Generate a JSON schema for a COMPAS object class. + + Parameters + ---------- + cls : :class:`~compas.data.Data` + The COMPAS object class. + + Returns + ------- + dict + The JSON schema. + + """ + return cls.DATASCHEMA + + +def dataclass_typeschema(cls): + """Generate a JSON schema for the data type of a COMPAS object class. + + Parameters + ---------- + cls : :class:`~compas.data.Data` + The COMPAS object class. + + Returns + ------- + dict + The JSON schema. + + """ + return { + "type": "string", + "const": "{}/{}".format(".".join(cls.__module__.split(".")[:2]), cls.__name__), + } + + +def dataclass_jsonschema(cls, filepath=None, draft=None): + """Generate a JSON schema for a COMPAS object class. + + Parameters + ---------- + cls : :class:`~compas.data.Data` + The COMPAS object class. + filepath : str, optional + The path to the file where the schema should be saved. + draft : str, optional + The JSON schema draft to use. + + Returns + ------- + dict + The JSON schema. + + """ + import compas + + draft = draft or "https://json-schema.org/draft/2020-12/schema" + + schema = { + "$schema": draft, + "$id": "{}.json".format(cls.__name__), + "$compas": "{}".format(compas.__version__), + "type": "object", + "properties": { + "dtype": dataclass_typeschema(cls), + "data": dataclass_dataschema(cls), + "guid": {"type": "string", "format": "uuid"}, + }, + "required": ["dtype", "data"], + } + + if filepath: + with open(filepath, "w") as f: + json.dump(schema, f, indent=4) + + return schema + + +def compas_jsonschema(dirname=None): + """Generate a JSON schema for the COMPAS data model. + + Parameters + ---------- + dirname : str, optional + The path to the directory where the schemas should be saved. + + Returns + ------- + list + A list of JSON schemas. + + """ + schemas = [] + dataclasses = compas_datamodel() + for cls in dataclasses: + filepath = None + if dirname: + filepath = os.path.join(dirname, "{}.json".format(cls.__name__)) + schema = dataclass_jsonschema(cls, filepath=filepath) + schemas.append(schema) + return schemas + + +def compas_datamodel(): + """Find all classes in the COMPAS data model. + + Returns + ------- + list + + """ + from collections import deque + from compas.data import Data + import compas.colors # noqa: F401 + import compas.datastructures # noqa: F401 + import compas.geometry # noqa: F401 + + tovisit = deque([Data]) + dataclasses = [] + + while tovisit: + cls = tovisit.popleft() + dataclasses.append(cls) + for subcls in cls.__subclasses__(): + tovisit.append(subcls) + + return dataclasses + + +# def validate_json(filepath, schema): +# """Validates a JSON document with respect to a schema and return the JSON object instance if it is valid. + +# Parameters +# ---------- +# filepath : path string | file-like object | URL string +# The filepath of the JSON document. +# schema : string +# The JSON schema. + +# Raises +# ------ +# jsonschema.exceptions.SchemaError +# If the schema itself is invalid. +# jsonschema.exceptions.ValidationError +# If the document is invalid with respect to the schema. + +# Returns +# ------- +# object +# The JSON object contained in the document. + +# Examples +# -------- +# >>> import compas +# >>> from compas.geometry import Point +# >>> compas.json_validate("data.json", Point) +# {'dtype': 'compas.geometry.Point', 'data': {'x': 0.0, 'y': 0.0, 'z': 0.0}, 'guid': '00000000-0000-0000-0000-000000000000'} + +# """ +# import jsonschema +# import jsonschema.exceptions + +# data = json_load(filepath) + +# try: +# jsonschema.validate(data, schema) +# except jsonschema.exceptions.SchemaError as e: +# print("The provided schema is invalid:\n\n{}\n\n".format(schema)) +# raise e +# except jsonschema.exceptions.ValidationError as e: +# print( +# "The provided JSON document is invalid compared to the provided schema:\n\n{}\n\n{}\n\n".format( +# schema, data +# ) +# ) +# raise e + +# return data + + +# def validate_jsonstring(jsonstring, schema): +# """Validate the data contained in the JSON string against the JSON data schema. + +# Parameters +# ---------- +# jsonstring : str +# The JSON string for validation. +# schema : string +# The JSON schema. + +# Raises +# ------ +# jsonschema.exceptions.SchemaError +# If the schema itself is invalid. +# jsonschema.exceptions.ValidationError +# If the document is invalid with respect to the schema. + +# Returns +# ------- +# object +# The JSON object contained in the string. + +# """ +# from jsonschema import Draft202012Validator + +# validator = Draft202012Validator(schema) # type: ignore +# jsondata = json.loads(jsonstring) +# validator.validate(jsondata) +# return jsondata + + +# def validate_jsondata(jsondata, schema): +# """Validate the JSON data against the JSON data schema. + +# Parameters +# ---------- +# jsondata : Any +# The JSON data for validation. +# schema : string +# The JSON schema. + +# Raises +# ------ +# jsonschema.exceptions.SchemaError +# If the schema itself is invalid. +# jsonschema.exceptions.ValidationError +# If the document is invalid with respect to the schema. + +# Returns +# ------- +# object +# The JSON object contained in the data. + +# """ +# from jsonschema import Draft202012Validator + +# validator = Draft202012Validator(schema) # type: ignore +# validator.validate(jsondata) +# return jsondata + + +# def validate_data(data, cls): +# """Validate data against the data and json schemas of an object class. + +# Parameters +# ---------- +# data : dict +# The data representation of an object. +# cls : Type[:class:`~compas.data.Data`] +# The data object class. + +# Returns +# ------- +# dict +# The validated data dict. + +# Raises +# ------ +# jsonschema.exceptions.ValidationError + +# """ +# from jsonschema import RefResolver, Draft7Validator +# from jsonschema.exceptions import ValidationError + +# here = os.path.dirname(__file__) + +# schema_name = "{}.json".format(cls.__name__.lower()) +# schema_path = os.path.join(here, "schemas", schema_name) +# with open(schema_path, "r") as fp: +# schema = json.load(fp) + +# definitions_path = os.path.join(here, "schemas", "compas.json") +# with open(definitions_path, "r") as fp: +# definitions = json.load(fp) + +# resolver = RefResolver.from_schema(definitions) +# validator = Draft7Validator(schema, resolver=resolver) + +# try: +# validator.validate(data) +# except ValidationError as e: +# print("Validation against the JSON schema of this object failed.") +# raise e + +# return json.loads(json.dumps(data, cls=DataEncoder), cls=DataDecoder) From c9f87a1f6daf18d4f0f4f83cffa30687a757248c Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Thu, 10 Aug 2023 09:38:15 +0200 Subject: [PATCH 11/68] fix data interface assembly --- .../datastructures/assembly/assembly.py | 57 ++++++++----------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/src/compas/datastructures/assembly/assembly.py b/src/compas/datastructures/assembly/assembly.py index af6d33da85d..4a8d33b82ec 100644 --- a/src/compas/datastructures/assembly/assembly.py +++ b/src/compas/datastructures/assembly/assembly.py @@ -30,6 +30,15 @@ class Assembly(Datastructure): """ + DATASCHEMA = { + "type": "object", + "properties": { + "attributes": {"type": "object"}, + "graph": Graph.DATASCHEMA, + }, + "required": ["graph"], + } + def __init__(self, name=None, **kwargs): super(Assembly, self).__init__() self.attributes = {"name": name or "Assembly"} @@ -37,41 +46,29 @@ def __init__(self, name=None, **kwargs): self.graph = Graph() self._parts = {} + def __str__(self): + tpl = "" + return tpl.format(self.graph.number_of_nodes(), self.graph.number_of_edges()) + # ========================================================================== - # data + # Data # ========================================================================== - @property - def DATASCHEMA(self): - import schema - - return schema.Schema( - { - "attributes": dict, - "graph": Graph, - } - ) - - @property - def JSONSCHEMANAME(self): - return "assembly" - @property def data(self): - data = { + return { "attributes": self.attributes, "graph": self.graph, } - return data - @data.setter - def data(self, data): - self.attributes.update(data["attributes"] or {}) - self.graph = data["graph"] - self._parts = {part.guid: part.key for part in self.parts()} + # @data.setter + # def data(self, data): + # self.attributes.update(data["attributes"] or {}) + # self.graph = data["graph"] + # self._parts = {part.guid: part.key for part in self.parts()} # ========================================================================== - # properties + # Properties # ========================================================================== @property @@ -83,19 +80,11 @@ def name(self, value): self.attributes["name"] = value # ========================================================================== - # customization - # ========================================================================== - - def __str__(self): - tpl = "" - return tpl.format(self.graph.number_of_nodes(), self.graph.number_of_edges()) - - # ========================================================================== - # constructors + # Constructors # ========================================================================== # ========================================================================== - # methods + # Methods # ========================================================================== def add_part(self, part, key=None, **kwargs): From eed498e786e42b37e670d4380fd65e5d01c7cfbe Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Thu, 10 Aug 2023 09:38:23 +0200 Subject: [PATCH 12/68] fix data interface part --- src/compas/datastructures/assembly/part.py | 45 +++++++++------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/src/compas/datastructures/assembly/part.py b/src/compas/datastructures/assembly/part.py index 66b5db2bb8d..cae545f0de3 100644 --- a/src/compas/datastructures/assembly/part.py +++ b/src/compas/datastructures/assembly/part.py @@ -68,9 +68,9 @@ def __init__(self, *args, **kwargs): def data(self): return {"geometry": self._geometry} - @data.setter - def data(self, value): - self._geometry = value["geometry"] + # @data.setter + # def data(self, value): + # self._geometry = value["geometry"] class ParametricFeature(Feature): @@ -151,6 +151,16 @@ class Part(Datastructure): """ + DATASCHEMA = { + "type": "object", + "properties": { + "attributes": {"type": "object"}, + "key": {"type": ["integer", "string"]}, + "frame": Frame.DATASCHEMA, + }, + "required": [], + } + def __init__(self, name=None, frame=None, **kwargs): super(Part, self).__init__() self.attributes = {"name": name or "Part"} @@ -159,36 +169,19 @@ def __init__(self, name=None, frame=None, **kwargs): self.frame = frame or Frame.worldXY() self.features = [] - @property - def DATASCHEMA(self): - import schema - - return schema.Schema( - { - "attributes": dict, - "key": int, - "frame": Frame, - } - ) - - @property - def JSONSCHEMANAME(self): - return "part" - @property def data(self): - data = { + return { "attributes": self.attributes, "key": self.key, "frame": self.frame, } - return data - @data.setter - def data(self, data): - self.attributes.update(data["attributes"] or {}) - self.key = data["key"] - self.frame = data["frame"] + # @data.setter + # def data(self, data): + # self.attributes.update(data["attributes"] or {}) + # self.key = data["key"] + # self.frame = data["frame"] def get_geometry(self, with_features=False): """ From 0160621a0b391e22c99b1158aaacbebb762949d4 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Thu, 10 Aug 2023 09:41:07 +0200 Subject: [PATCH 13/68] log --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc84a7998d4..62a40a5c529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.data.Data.validate_data`. * Added `compas.data.Data.__jsondump__`. * Added `compas.data.Data.__jsonload__`. +* Added `compas.data.schema.dataclass_dataschema`. +* Added `compas.data.schema.dataclass_typeschema`. +* Added `compas.data.schema.dataclass_jsonschema`. +* Added `compas.data.schema.compas_jsonschema`. +* Added `compas.data.schema.compas_datamodel`. * Added `compas.datastructures.Graph.to_jsondata`. * Added `compas.datastructures.Graph.from_jsondata`. * Added `compas.datastructures.Halfedge.halfedge_loop_vertices`. @@ -157,6 +162,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Changed base class of `compas.geometry.Line` to `compas.geometry.Curve.` * Changed base class of `compas.geometry.Polyline` to `compas.geometry.Curve.` * Changed `compas.geometry.oriented_bounding_box_numpy` to minimize volume. +* Fixed data interface `compas.datastructures.Assembly` and `compas.datastructures.Part`. ### Removed From 44e0a0f3afa25f23772ff6353187c1a45d598841 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Thu, 10 Aug 2023 10:00:32 +0200 Subject: [PATCH 14/68] assembly needs a lot of work --- .../datastructures/assembly/assembly.py | 14 +++++----- src/compas/datastructures/assembly/part.py | 27 +++++++++++-------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/compas/datastructures/assembly/assembly.py b/src/compas/datastructures/assembly/assembly.py index 4a8d33b82ec..ea4aa73292c 100644 --- a/src/compas/datastructures/assembly/assembly.py +++ b/src/compas/datastructures/assembly/assembly.py @@ -58,14 +58,16 @@ def __str__(self): def data(self): return { "attributes": self.attributes, - "graph": self.graph, + "graph": self.graph.data, } - # @data.setter - # def data(self, data): - # self.attributes.update(data["attributes"] or {}) - # self.graph = data["graph"] - # self._parts = {part.guid: part.key for part in self.parts()} + @classmethod + def from_data(cls, data): + assembly = cls() + assembly.attributes.update(data["attributes"] or {}) + assembly.graph = Graph.from_data(data["graph"]) + assembly._parts = {part.guid: part.key for part in assembly.parts()} + return assembly # ========================================================================== # Properties diff --git a/src/compas/datastructures/assembly/part.py b/src/compas/datastructures/assembly/part.py index cae545f0de3..946b776f94f 100644 --- a/src/compas/datastructures/assembly/part.py +++ b/src/compas/datastructures/assembly/part.py @@ -68,14 +68,15 @@ def __init__(self, *args, **kwargs): def data(self): return {"geometry": self._geometry} - # @data.setter - # def data(self, value): - # self._geometry = value["geometry"] + @classmethod + def from_data(cls, data): + feature = cls() + feature._geometry = data["geometry"] # this will work but is not consistent with validation + return feature class ParametricFeature(Feature): - """Base class for Features that may be applied to the parametric definition - of a :class:`~compas.datastructures.Part`. + """Base class for Features that may be applied to the parametric definition of a :class:`~compas.datastructures.Part`. Examples -------- @@ -158,7 +159,7 @@ class Part(Datastructure): "key": {"type": ["integer", "string"]}, "frame": Frame.DATASCHEMA, }, - "required": [], + "required": ["key", "frame"], } def __init__(self, name=None, frame=None, **kwargs): @@ -169,6 +170,7 @@ def __init__(self, name=None, frame=None, **kwargs): self.frame = frame or Frame.worldXY() self.features = [] + # features are not included here?! @property def data(self): return { @@ -177,11 +179,13 @@ def data(self): "frame": self.frame, } - # @data.setter - # def data(self, data): - # self.attributes.update(data["attributes"] or {}) - # self.key = data["key"] - # self.frame = data["frame"] + @classmethod + def from_data(cls, data): + part = cls() + part.attributes.update(data["attributes"] or {}) + part.key = data["key"] + part.frame = Frame.from_data(data["frame"]) + return part def get_geometry(self, with_features=False): """ @@ -217,5 +221,6 @@ def add_feature(self, feature, apply=False): Returns ------- None + """ raise NotImplementedError From 8dc5082b8f137318858e77f889bcf81649141071 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Thu, 10 Aug 2023 10:00:44 +0200 Subject: [PATCH 15/68] remove typo --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62a40a5c529..ea73da3374d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `create_id` to `compas_ghpython.utilities`. (moved from `compas_fab`) * Added representation for features in `compas.datastructures.Part`. -* Added `split` and `split_by_length` to `compas.geometry.Polyline`. +* Added `split` and `split_by_length` to `compas.geometry.Polyline`. * Added `compas.rpc.XFunc`. * Added attribute `compas.color.Color.DATASCHEMA`. * Added attribute `compas.data.Data.DATASCHEMA`. @@ -181,7 +181,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed `compas.data.Data.validate_jsonstring`. * Removed `compas.data.Data.__getstate__`. * Removed `compas.data.Data.__setstate__`. -* Removed setter of property `compas.data.Data.data`. +* Removed setter of property `compas.data.Data.data` and similar setters in all data classes. * Removed properties `compas.data.Data.DATASCHEMA` and `compas.data.Data.JSONSCHEMANAME`. * Removed properties `compas.datastructures.Graph.DATASCHEMA` and `compas.datastructures.Graph.JSONSCHEMANAME`. * Removed properties `compas.datastructures.Halfedge.DATASCHEMA` and `compas.datastructures.Halfedge.JSONSCHEMANAME`. From 95ed45fb4e5bf5366ffb63358fb5c9bdef26d7ea Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Thu, 10 Aug 2023 15:30:49 +0200 Subject: [PATCH 16/68] add name property to base data structure class --- src/compas/datastructures/datastructure.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/compas/datastructures/datastructure.py b/src/compas/datastructures/datastructure.py index b92520b5e7a..8f074dc8af0 100644 --- a/src/compas/datastructures/datastructure.py +++ b/src/compas/datastructures/datastructure.py @@ -4,11 +4,18 @@ from compas.data import Data -__all__ = ["Datastructure"] - class Datastructure(Data): """Base class for all data structures.""" - def __init__(self): - super(Datastructure, self).__init__() + def __init__(self, name=None, **kwargs): + super(Datastructure, self).__init__(**kwargs) + self.attributes = {"name": name or self.__class__.__name__} + + @property + def name(self): + return self.attributes.get("name") or self.__class__.__name__ + + @name.setter + def name(self, value): + self.attributes["name"] = value From 26738582edbc29ba500187bbd1275e10b3484f25 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Fri, 11 Aug 2023 10:17:35 +0200 Subject: [PATCH 17/68] type stuff --- src/compas/datastructures/mesh/mesh.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index d13eeadc3e7..0b0990acc5f 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -1422,7 +1422,7 @@ def face_flatness(self, fkey, maxdev=0.02): """ vertices = self.face_vertices(fkey) f = len(vertices) - points = self.vertices_attributes("xyz", keys=vertices) + points = self.vertices_attributes("xyz", keys=vertices) or [] lengths = [distance_point_point(a, b) for a, b in pairwise(points + points[:1])] length = sum(lengths) / f d = distance_line_line((points[0], points[2]), (points[1], points[3])) @@ -1477,8 +1477,8 @@ def face_skewness(self, fkey): angle = angle_points(o, a, b, deg=True) angles.append(angle) return max( - (max(angles) - ideal_angle) / (180 - ideal_angle), - (ideal_angle - min(angles)) / ideal_angle, + (max(angles) - ideal_angle) / (180 - ideal_angle), # type: ignore + (ideal_angle - min(angles)) / ideal_angle, # type: ignore ) def face_curvature(self, fkey): From 9c90898f000968af73971d0b35b2d8d6aa335f22 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Fri, 11 Aug 2023 10:17:47 +0200 Subject: [PATCH 18/68] type stuff --- src/compas/datastructures/volmesh/volmesh.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/compas/datastructures/volmesh/volmesh.py b/src/compas/datastructures/volmesh/volmesh.py index 556ff6ab3b0..172170d0335 100644 --- a/src/compas/datastructures/volmesh/volmesh.py +++ b/src/compas/datastructures/volmesh/volmesh.py @@ -195,9 +195,9 @@ def from_obj(cls, filepath, precision=None): """ obj = OBJ(filepath, precision) - vertices = obj.parser.vertices - faces = obj.parser.faces - groups = obj.parser.groups + vertices = obj.parser.vertices or [] # type: ignore + faces = obj.parser.faces or [] # type: ignore + groups = obj.parser.groups or [] # type: ignore cells = [] for name in groups: group = groups[name] @@ -288,7 +288,7 @@ def to_vertices_and_cells(self): vertex_index = self.vertex_index() vertices = [self.vertex_coordinates(vertex) for vertex in self.vertices()] cells = [] - for cell in self.cell: + for cell in self.cells(): faces = [ [vertex_index[vertex] for vertex in self.halfface_vertices(face)] for face in self.cell_faces(cell) ] From 49a2904f113273e598215a39bc25301bc38cce7f Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Fri, 11 Aug 2023 10:20:16 +0200 Subject: [PATCH 19/68] JSON compatible data in datastructures --- CHANGELOG.md | 3 + src/compas/datastructures/graph/graph.py | 133 ++++--------- .../datastructures/halfedge/halfedge.py | 146 +++++--------- .../datastructures/halfface/halfface.py | 184 +++++++++--------- 4 files changed, 185 insertions(+), 281 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 199741d7a81..2d921ef3363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -166,6 +166,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Changed base class of `compas.geometry.Polyline` to `compas.geometry.Curve.` * Changed `compas.geometry.oriented_bounding_box_numpy` to minimize volume. * Fixed data interface `compas.datastructures.Assembly` and `compas.datastructures.Part`. +* Changed data property of `compas.datastructures.Graph` to contain only JSON compatible data. +* Changed data property of `compas.datastructures.Halfedge` to contain only JSON compatible data. +* Changed data property of `compas.datastructures.Halfface` to contain only JSON compatible data. ### Removed diff --git a/src/compas/datastructures/graph/graph.py b/src/compas/datastructures/graph/graph.py index 7d780acda85..f2fa2ff88b5 100644 --- a/src/compas/datastructures/graph/graph.py +++ b/src/compas/datastructures/graph/graph.py @@ -41,7 +41,7 @@ class Graph(Datastructure): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "attributes": {"type": "object"}, @@ -79,9 +79,8 @@ class Graph(Datastructure): } def __init__(self, name=None, default_node_attributes=None, default_edge_attributes=None): - super(Graph, self).__init__() + super(Graph, self).__init__(name=name) self._max_node = -1 - self.attributes = {"name": name or "Graph"} self.node = {} self.edge = {} self.adjacency = {} @@ -97,7 +96,7 @@ def __str__(self): return tpl.format(self.number_of_nodes(), self.number_of_edges()) # -------------------------------------------------------------------------- - # data + # Data # -------------------------------------------------------------------------- @property @@ -106,48 +105,10 @@ def data(self): "attributes": self.attributes, "dna": self.default_node_attributes, "dea": self.default_edge_attributes, - "node": self.node, - "edge": self.edge, + "node": {}, + "edge": {}, "max_node": self._max_node, } - return data - - @data.setter - def data(self, data): - self.node = {} - self.edge = {} - self.adjacency = {} - self._max_node = -1 - self.attributes.update(data.get("attributes") or {}) - self.default_node_attributes.update(data.get("dna") or {}) - self.default_edge_attributes.update(data.get("dea") or {}) - node = data.get("node") or {} - edge = data.get("edge") or {} - for node, attr in iter(node.items()): - self.add_node(key=node, attr_dict=attr) - for u, nbrs in iter(edge.items()): - for v, attr in iter(nbrs.items()): - self.add_edge(u, v, attr_dict=attr) - self._max_node = data.get("max_node", self._max_node) - - def to_jsondata(self): - """Returns a dictionary of structured data representing the graph that can be serialised to JSON format. - - This is effectively a post-processing step for the :meth:`to_data` method. - - Returns - ------- - dict - The serialisable structured data dictionary. - - See Also - -------- - :meth:`from_jsondata` - - """ - data = self.data - data["node"] = {} - data["edge"] = {} for key in self.node: data["node"][repr(key)] = self.node[key] for u in self.edge: @@ -159,58 +120,35 @@ def to_jsondata(self): return data @classmethod - def from_jsondata(cls, data): - """Construct a graph from structured data representing the graph in JSON format. - - This is effectively a pre-processing step for the :meth:`from_data` method. - - Parameters - ---------- - data : dict - The structured data dictionary. + def from_data(cls, data): + dna = data.get("dna") or {} + dea = data.get("dea") or {} + node = data.get("node") or {} + edge = data.get("edge") or {} - Returns - ------- - :class:`~compas.datastructures.Graph` - The constructed graph. + graph = cls(default_node_attributes=dna, default_edge_attributes=dea) + graph.attributes.update(data.get("attributes") or {}) - See Also - -------- - :meth:`to_jsondata` + for node, attr in iter(node.items()): + node = literal_eval(node) + graph.add_node(key=node, attr_dict=attr) - """ - _node = data["node"] or {} - _edge = data["edge"] or {} - # process the nodes - node = {literal_eval(key): attr for key, attr in iter(_node.items())} - data["node"] = node - # process the edges - edge = {} - for u, nbrs in iter(_edge.items()): - nbrs = nbrs or {} + for u, nbrs in iter(edge.items()): u = literal_eval(u) - edge[u] = {} for v, attr in iter(nbrs.items()): - attr = attr or {} v = literal_eval(v) - edge[u][v] = attr - data["edge"] = edge - return cls.from_data(data) + graph.add_edge(u, v, attr_dict=attr) - # -------------------------------------------------------------------------- - # properties - # -------------------------------------------------------------------------- + graph._max_node = data.get("max_node", graph._max_node) - @property - def name(self): - return self.attributes.get("name") or self.__class__.__name__ + return graph - @name.setter - def name(self, value): - self.attributes["name"] = value + # -------------------------------------------------------------------------- + # Properties + # -------------------------------------------------------------------------- # -------------------------------------------------------------------------- - # constructors + # Constructors # -------------------------------------------------------------------------- @classmethod @@ -296,7 +234,7 @@ def to_networkx(self): return G # -------------------------------------------------------------------------- - # helpers + # Helpers # -------------------------------------------------------------------------- def clear(self): @@ -421,7 +359,7 @@ def index_edge(self): return dict(enumerate(self.edges())) # -------------------------------------------------------------------------- - # builders + # Builders # -------------------------------------------------------------------------- def add_node(self, key=None, attr_dict=None, **kwattr): @@ -523,7 +461,7 @@ def add_edge(self, u, v, attr_dict=None, **kwattr): return u, v # -------------------------------------------------------------------------- - # modifiers + # Modifiers # -------------------------------------------------------------------------- def delete_node(self, key): @@ -594,7 +532,7 @@ def delete_edge(self, edge): del self.edge[v][u] # -------------------------------------------------------------------------- - # info + # Info # -------------------------------------------------------------------------- def summary(self): @@ -647,7 +585,7 @@ def number_of_edges(self): return len(list(self.edges())) # -------------------------------------------------------------------------- - # accessors + # Accessors # -------------------------------------------------------------------------- def nodes(self, data=False): @@ -705,6 +643,7 @@ def nodes_where(self, conditions=None, data=False, **kwargs): for key, attr in self.nodes(True): is_match = True + attr = attr or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -844,7 +783,7 @@ def edges_where(self, conditions=None, data=False, **kwargs): for key in self.edges(): is_match = True - attr = self.edge_attributes(key) + attr = self.edge_attributes(key) or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -969,7 +908,7 @@ def update_default_edge_attributes(self, attr_dict=None, **kwattr): update_dea = update_default_edge_attributes # -------------------------------------------------------------------------- - # node attributes + # Node attributes # -------------------------------------------------------------------------- def node_attribute(self, key, name, value=None): @@ -1075,7 +1014,7 @@ def node_attributes(self, key, names=None, values=None): """ if key not in self.node: raise KeyError(key) - if values is not None: + if names and values is not None: # use it as a setter for name, value in zip(names, values): self.node[key][name] = value @@ -1172,7 +1111,7 @@ def nodes_attributes(self, names=None, values=None, keys=None): return [self.node_attributes(key, names) for key in keys] # -------------------------------------------------------------------------- - # edge attributes + # Edge attributes # -------------------------------------------------------------------------- def edge_attribute(self, key, name, value=None): @@ -1285,7 +1224,7 @@ def edge_attributes(self, key, names=None, values=None): u, v = key if u not in self.edge or v not in self.edge[u]: raise KeyError(key) - if values: + if names and values: # use it as a setter for name, value in zip(names, values): self.edge_attribute(key, name, value) @@ -1379,7 +1318,7 @@ def edges_attributes(self, names=None, values=None, keys=None): return [self.edge_attributes(key, names) for key in keys] # -------------------------------------------------------------------------- - # node topology + # Node topology # -------------------------------------------------------------------------- def has_node(self, key): @@ -1636,7 +1575,7 @@ def connected_edges(self, key): return edges # -------------------------------------------------------------------------- - # edge topology + # Edge topology # -------------------------------------------------------------------------- def has_edge(self, edge, directed=True): diff --git a/src/compas/datastructures/halfedge/halfedge.py b/src/compas/datastructures/halfedge/halfedge.py index 1c7f91adce6..f91de5cecf0 100644 --- a/src/compas/datastructures/halfedge/halfedge.py +++ b/src/compas/datastructures/halfedge/halfedge.py @@ -50,7 +50,7 @@ class HalfEdge(Datastructure): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "attributes": {"type": "object"}, @@ -80,7 +80,7 @@ class HalfEdge(Datastructure): }, "edgedata": { "type": "object", - "patternProperties": {"^\\([0-9]+,[0-9]+\\)$": {"type": "object"}}, + "patternProperties": {"^\\([0-9]+, [0-9]+\\)$": {"type": "object"}}, "additionalProperties": False, }, "max_vertex": {"type": "integer", "minimum": -1}, @@ -107,7 +107,7 @@ def __init__( default_edge_attributes=None, default_face_attributes=None, ): - super(HalfEdge, self).__init__() + super(HalfEdge, self).__init__(name=name) self._max_vertex = -1 self._max_face = -1 self.vertex = {} @@ -115,7 +115,6 @@ def __init__( self.face = {} self.facedata = {} self.edgedata = {} - self.attributes = {"name": name or "HalfEdge"} self.default_vertex_attributes = {} self.default_edge_attributes = {} self.default_face_attributes = {} @@ -131,78 +130,62 @@ def __str__(self): return tpl.format(self.number_of_vertices(), self.number_of_faces(), self.number_of_edges()) # -------------------------------------------------------------------------- - # descriptors + # Data # -------------------------------------------------------------------------- - @property - def name(self): - return self.attributes.get("name") or self.__class__.__name__ - - @name.setter - def name(self, value): - self.attributes["name"] = value - - @property - def adjacency(self): - return self.halfedge - @property def data(self): - """Returns a dictionary of structured data representing the data structure. - - Note that some of the data stored internally is not included in the dictionary representation of the data structure. - This is the case for data that is considered private and/or redundant. - Specifially, the half-edge dictionary is not included in the dictionary representation of the data structure. - This is because the half-edge dictionary can be reconstructed from the face dictionary. - The face dictionary contains the same information as the half-edge dictionary, but in a more compact form. - Therefore, to keep the dictionary representation of the data structure as compact as possible, the half-edge dictionary is not included. - - Returns - ------- - dict - The data dictionary. - - """ return { "attributes": self.attributes, "dva": self.default_vertex_attributes, "dea": self.default_edge_attributes, "dfa": self.default_face_attributes, - "vertex": self.vertex, - "face": self.face, - "facedata": self.facedata, + "vertex": {str(vertex): attr for vertex, attr in self.vertex.items()}, + "face": {str(face): vertices for face, vertices in self.face.items()}, + "facedata": {str(face): attr for face, attr in self.facedata.items()}, "edgedata": self.edgedata, "max_vertex": self._max_vertex, "max_face": self._max_face, } - @data.setter - def data(self, data): - self.vertex = {} - self.halfedge = {} - self.face = {} - self.facedata = {} - self.edgedata = {} - self._max_vertex = -1 - self._max_face = -1 - self.attributes.update(data.get("attributes") or {}) - self.default_vertex_attributes.update(data.get("dva") or {}) - self.default_face_attributes.update(data.get("dfa") or {}) - self.default_edge_attributes.update(data.get("dea") or {}) + @classmethod + def from_data(cls, data): + dva = data.get("dva") or {} + dfa = data.get("dfa") or {} + dea = data.get("dea") or {} + + halfedge = cls(default_vertex_attributes=dva, default_face_attributes=dfa, default_edge_attributes=dea) + halfedge.attributes.update(data.get("attributes") or {}) + vertex = data.get("vertex") or {} face = data.get("face") or {} facedata = data.get("facedata") or {} + edgedata = data.get("edgedata") or {} + for key, attr in iter(vertex.items()): - self.add_vertex(key=key, attr_dict=attr) + halfedge.add_vertex(key=key, attr_dict=attr) + for fkey, vertices in iter(face.items()): attr = facedata.get(fkey) or {} - self.add_face(vertices, fkey=fkey, attr_dict=attr) - self.edgedata.update(data.get("edgedata") or {}) - self._max_vertex = data.get("max_vertex", self._max_vertex) - self._max_face = data.get("max_face", self._max_face) + halfedge.add_face(vertices, fkey=fkey, attr_dict=attr) + + halfedge.edgedata.update(edgedata) + + halfedge._max_vertex = data.get("max_vertex", halfedge._max_vertex) + halfedge._max_face = data.get("max_face", halfedge._max_face) + + return halfedge # -------------------------------------------------------------------------- - # helpers + # Properties + # -------------------------------------------------------------------------- + + @property + def adjacency(self): + return self.halfedge + + # -------------------------------------------------------------------------- + # Helpers # -------------------------------------------------------------------------- def clear(self): @@ -319,7 +302,7 @@ def index_vertex(self): return dict(enumerate(self.vertices())) # -------------------------------------------------------------------------- - # builders + # Builders # -------------------------------------------------------------------------- def add_vertex(self, key=None, attr_dict=None, **kwattr): @@ -439,7 +422,7 @@ def add_face(self, vertices, fkey=None, attr_dict=None, **kwattr): return fkey # -------------------------------------------------------------------------- - # modifiers + # Modifiers # -------------------------------------------------------------------------- def delete_vertex(self, key): @@ -551,7 +534,7 @@ def remove_unused_vertices(self): cull_vertices = remove_unused_vertices # -------------------------------------------------------------------------- - # accessors + # Accessors # -------------------------------------------------------------------------- def vertices(self, data=False): @@ -683,6 +666,7 @@ def vertices_where(self, conditions=None, data=False, **kwargs): for key, attr in self.vertices(True): is_match = True + attr = attr or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -796,7 +780,7 @@ def edges_where(self, conditions=None, data=False, **kwargs): for key in self.edges(): is_match = True - attr = self.edge_attributes(key) + attr = self.edge_attributes(key) or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -893,7 +877,7 @@ def faces_where(self, conditions=None, data=False, **kwargs): for fkey in self.faces(): is_match = True - attr = self.face_attributes(fkey) + attr = self.face_attributes(fkey) or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -958,7 +942,7 @@ def faces_where_predicate(self, predicate, data=False): yield fkey # -------------------------------------------------------------------------- - # attributes + # Attributes # -------------------------------------------------------------------------- def update_default_vertex_attributes(self, attr_dict=None, **kwattr): @@ -1101,7 +1085,7 @@ def vertex_attributes(self, key, names=None, values=None): """ if key not in self.vertex: raise KeyError(key) - if values is not None: + if names and values is not None: # use it as a setter for name, value in zip(names, values): self.vertex[key][name] = value @@ -1343,7 +1327,7 @@ def face_attributes(self, key, names=None, values=None): """ if key not in self.face: raise KeyError(key) - if values is not None: + if names and values is not None: # use it as a setter for name, value in zip(names, values): if key not in self.facedata: @@ -1589,7 +1573,7 @@ def edge_attributes(self, edge, names=None, values=None): u, v = edge if u not in self.halfedge or v not in self.halfedge[u]: raise KeyError(edge) - if values is not None: + if names and values is not None: # use it as a setter for name, value in zip(names, values): self.edge_attribute(edge, name, value) @@ -1687,7 +1671,7 @@ def edges_attributes(self, names=None, values=None, keys=None): return [self.edge_attributes(edge, names) for edge in edges] # -------------------------------------------------------------------------- - # mesh info + # Info # -------------------------------------------------------------------------- def summary(self): @@ -2008,34 +1992,8 @@ def euler(self): F = self.number_of_faces() return V - E + F - def genus(self): - """Calculate the genus. - - Returns - ------- - int - The genus. - - See Also - -------- - :meth:`euler` - - References - ---------- - .. [1] Wolfram MathWorld. *Genus*. - Available at: http://mathworld.wolfram.com/Genus.html. - - """ - X = self.euler() - # each boundary must be taken into account as if it was one face - B = len(self.boundaries()) - if self.is_orientable(): - return (2 - (X + B)) / 2 - else: - return 2 - (X + B) - # -------------------------------------------------------------------------- - # vertex topology + # Vertex topology # -------------------------------------------------------------------------- def has_vertex(self, key): @@ -2251,7 +2209,7 @@ def vertex_faces(self, key, ordered=False, include_none=False): return [fkey for fkey in faces if fkey is not None] # -------------------------------------------------------------------------- - # edge topology + # Edge topology # -------------------------------------------------------------------------- def has_edge(self, key): @@ -2353,7 +2311,7 @@ def is_edge_on_boundary(self, edge): return self.halfedge[v][u] is None or self.halfedge[u][v] is None # -------------------------------------------------------------------------- - # polyedge topology + # Polyedge topology # -------------------------------------------------------------------------- def edge_loop(self, edge): @@ -2511,7 +2469,7 @@ def halfedge_strip(self, edge): return edges # -------------------------------------------------------------------------- - # face topology + # Face topology # -------------------------------------------------------------------------- def has_face(self, fkey): diff --git a/src/compas/datastructures/halfface/halfface.py b/src/compas/datastructures/halfface/halfface.py index dea977b7bce..5ff00893717 100644 --- a/src/compas/datastructures/halfface/halfface.py +++ b/src/compas/datastructures/halfface/halfface.py @@ -48,7 +48,7 @@ class HalfFace(Datastructure): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "attributes": {"type": "object"}, @@ -66,18 +66,34 @@ class HalfFace(Datastructure): "patternProperties": { "^[0-9]+$": { "type": "array", - "minItems": 3, - "items": {"type": "integer", "minimum": 0}, + "minItems": 4, + "items": { + "type": "array", + "minItems": 3, + "items": {"type": "integer", "minimum": 0}, + }, } }, "additionalProperties": False, }, - "edge_data": {"type": "object"}, - "face_data": {"type": "object"}, - "cell_data": {"type": "object"}, - "max_vertex": {"type": "number"}, - "max_face": {"type": "number"}, - "max_cell": {"type": "number"}, + "edge_data": { + "type": "object", + "patternProperties": {"^\\([0-9]+, [0-9]+\\)$": {"type": "object"}}, + "additionalProperties": False, + }, + "face_data": { + "type": "object", + "patternProperties": {"^\\([0-9]+(, [0-9]+){3, }\\)$": {"type": "object"}}, + "additionalProperties": False, + }, + "cell_data": { + "type": "object", + "patternProperties": {"^[0-9]+$": {"type": "object"}}, + "additionalProperties": False, + }, + "max_vertex": {"type": "number", "minimum": -1}, + "max_face": {"type": "number", "minimum": -1}, + "max_cell": {"type": "number", "minimum": -1}, }, "required": [ "attributes", @@ -104,7 +120,7 @@ def __init__( default_face_attributes=None, default_cell_attributes=None, ): - super(HalfFace, self).__init__() + super(HalfFace, self).__init__(name=name) self._max_vertex = -1 self._max_face = -1 self._max_cell = -1 @@ -115,7 +131,6 @@ def __init__( self._edge_data = {} self._face_data = {} self._cell_data = {} - self.attributes = {"name": name or "HalfFace"} self.default_vertex_attributes = {"x": 0.0, "y": 0.0, "z": 0.0} self.default_edge_attributes = {} self.default_face_attributes = {} @@ -139,95 +154,82 @@ def __str__(self): ) # -------------------------------------------------------------------------- - # descriptors + # Data # -------------------------------------------------------------------------- - @property - def name(self): - return self.attributes.get("name") or self.__class__.__name__ - - @name.setter - def name(self, value): - self.attributes["name"] = value - @property def data(self): - """Returns a dictionary of structured data representing the volmesh data object. - - Note that some of the data stored internally in the data structure object is not included in the dictionary representation of the object. - This is the case for data that is considered private and/or redundant. - Specifically, the halfface dictionary and the plane dictionary are not included. - This is because the information in these dictionaries can be reconstructed from the other data. - Therefore, to keep the dictionary representation as compact as possible, these dictionaries are not included. - - To reconstruct the complete object representation from the compact data, the setter of the data property uses the vertex and cell builder methods (:meth:`add_vertex`, :meth:`add_cell`). - - Returns - ------- - dict - The structured data representing the volmesh. - - """ - cell = {} - for c in self._cell: + _cell = {} + for c in self.cells(): faces = [] - for u in sorted(self._cell[c]): - for v in sorted(self._cell[c][u]): - faces.append(self._halfface[self._cell[c][u][v]]) - cell[c] = faces + for face in self.cell_faces(c): + vertices = self.halfface_vertices(face) + faces.append(vertices) + _cell[c] = faces + return { "attributes": self.attributes, "dva": self.default_vertex_attributes, "dea": self.default_edge_attributes, "dfa": self.default_face_attributes, "dca": self.default_cell_attributes, - "vertex": self._vertex, - "cell": cell, + "vertex": {str(vertex): attr for vertex, attr in self._vertex.items()}, + "cell": {str(cell): faces for cell, faces in _cell.items()}, "edge_data": self._edge_data, "face_data": self._face_data, - "cell_data": self._cell_data, + "cell_data": {str(cell): attr for cell, attr in self._cell_data}, "max_vertex": self._max_vertex, "max_face": self._max_face, "max_cell": self._max_cell, } - @data.setter - def data(self, data): - self._vertex = {} - self._halfface = {} - self._cell = {} - self._plane = {} - self._edge_data = {} - self._face_data = {} - self._cell_data = {} - self._max_vertex = -1 - self._max_face = -1 - self._max_cell = -1 + @classmethod + def from_data(cls, data): + dva = data.get("dva") or {} + dea = data.get("dea") or {} + dfa = data.get("dfa") or {} + dca = data.get("dca") or {} + + halfface = cls( + default_vertex_attributes=dva, + default_edge_attributes=dea, + default_face_attributes=dfa, + default_cell_attributes=dca, + ) + + halfface.attributes.update(data.get("attributes") or {}) + vertex = data.get("vertex") or {} cell = data.get("cell") or {} edge_data = data.get("edge_data") or {} face_data = data.get("face_data") or {} cell_data = data.get("cell_data") or {} - self.attributes.update(data.get("attributes") or {}) - self.default_vertex_attributes.update(data.get("dva") or {}) - self.default_edge_attributes.update(data.get("dea") or {}) - self.default_face_attributes.update(data.get("dfa") or {}) - self.default_cell_attributes.update(data.get("dca") or {}) + for key, attr in iter(vertex.items()): - self.add_vertex(key=key, attr_dict=attr) + halfface.add_vertex(key=key, attr_dict=attr) + for ckey, faces in iter(cell.items()): attr = cell_data.get(ckey) or {} - self.add_cell(faces, ckey=ckey, attr_dict=attr) + halfface.add_cell(faces, ckey=ckey, attr_dict=attr) + for edge in edge_data: - self._edge_data[edge] = edge_data[edge] or {} + halfface._edge_data[edge] = edge_data[edge] or {} + for face in face_data: - self._face_data[face] = face_data[face] or {} - self._max_vertex = data.get("max_vertex", self._max_vertex) - self._max_face = data.get("max_face", self._max_face) - self._max_cell = data.get("max_cell", self._max_cell) + halfface._face_data[face] = face_data[face] or {} + + halfface._max_vertex = data.get("max_vertex", halfface._max_vertex) + halfface._max_face = data.get("max_face", halfface._max_face) + halfface._max_cell = data.get("max_cell", halfface._max_cell) + + return halfface + + # -------------------------------------------------------------------------- + # Properties + # -------------------------------------------------------------------------- # -------------------------------------------------------------------------- - # helpers + # Helpers # -------------------------------------------------------------------------- def clear(self): @@ -369,7 +371,7 @@ def index_vertex(self): return dict(enumerate(self.vertices())) # -------------------------------------------------------------------------- - # builders + # Builders # -------------------------------------------------------------------------- def add_vertex(self, key=None, attr_dict=None, **kwattr): @@ -545,7 +547,7 @@ def add_cell(self, faces, ckey=None, attr_dict=None, **kwattr): return ckey # -------------------------------------------------------------------------- - # modifiers + # Modifiers # -------------------------------------------------------------------------- def delete_vertex(self, vertex): @@ -634,7 +636,7 @@ def remove_unused_vertices(self): cull_vertices = remove_unused_vertices # -------------------------------------------------------------------------- - # accessors + # Accessors # -------------------------------------------------------------------------- def vertices(self, data=False): @@ -816,6 +818,8 @@ def vertices_where(self, conditions=None, data=False, **kwargs): for key, attr in self.vertices(True): is_match = True + attr = attr or {} + for name, value in conditions.items(): method = getattr(self, name, None) @@ -927,7 +931,7 @@ def edges_where(self, conditions=None, data=False, **kwargs): for key in self.edges(): is_match = True - attr = self.edge_attributes(key) + attr = self.edge_attributes(key) or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -1022,7 +1026,7 @@ def faces_where(self, conditions=None, data=False, **kwargs): for fkey in self.faces(): is_match = True - attr = self.face_attributes(fkey) + attr = self.face_attributes(fkey) or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -1117,7 +1121,7 @@ def cells_where(self, conditions=None, data=False, **kwargs): for ckey in self.cells(): is_match = True - attr = self.cell_attributes(ckey) + attr = self.cell_attributes(ckey) or {} for name, value in conditions.items(): method = getattr(self, name, None) @@ -1181,7 +1185,7 @@ def cells_where_predicate(self, predicate, data=False): yield ckey # -------------------------------------------------------------------------- - # attributes - vertices + # Vertex attributes # -------------------------------------------------------------------------- def update_default_vertex_attributes(self, attr_dict=None, **kwattr): @@ -1319,7 +1323,7 @@ def vertex_attributes(self, vertex, names=None, values=None): """ if vertex not in self._vertex: raise KeyError(vertex) - if values is not None: + if names and values is not None: # use it as a setter for name, value in zip(names, values): self._vertex[vertex][name] = value @@ -1417,7 +1421,7 @@ def vertices_attributes(self, names=None, values=None, keys=None): return [self.vertex_attributes(vertex, names) for vertex in vertices] # -------------------------------------------------------------------------- - # attributes - edges + # Edge attributes # -------------------------------------------------------------------------- def update_default_edge_attributes(self, attr_dict=None, **kwattr): @@ -1561,7 +1565,7 @@ def edge_attributes(self, edge, names=None, values=None): if u not in self._plane or v not in self._plane[u]: raise KeyError(edge) key = str(tuple(sorted(edge))) - if values: + if names and values: for name, value in zip(names, values): if key not in self._edge_data: self._edge_data[key] = {} @@ -1653,7 +1657,7 @@ def edges_attributes(self, names=None, values=None, edges=None): return [self.edge_attributes(edge, names) for edge in edges] # -------------------------------------------------------------------------- - # face attributes + # Face attributes # -------------------------------------------------------------------------- def update_default_face_attributes(self, attr_dict=None, **kwattr): @@ -1794,7 +1798,7 @@ def face_attributes(self, face, names=None, values=None): if face not in self._halfface: raise KeyError(face) key = str(tuple(sorted(self.halfface_vertices(face)))) - if values: + if names and values: for name, value in zip(names, values): if key not in self._face_data: self._face_data[key] = {} @@ -1887,7 +1891,7 @@ def faces_attributes(self, names=None, values=None, faces=None): return [self.face_attributes(face, names) for face in faces] # -------------------------------------------------------------------------- - # attributes - cell + # Cell attributes # -------------------------------------------------------------------------- def update_default_cell_attributes(self, attr_dict=None, **kwattr): @@ -2026,7 +2030,7 @@ def cell_attributes(self, cell, names=None, values=None): """ if cell not in self._cell: raise KeyError(cell) - if values is not None: + if names and values is not None: for name, value in zip(names, values): if cell not in self._cell_data: self._cell_data[cell] = {} @@ -2120,7 +2124,7 @@ def cells_attributes(self, names=None, values=None, cells=None): return [self.cell_attributes(cell, names) for cell in cells] # -------------------------------------------------------------------------- - # volmesh info + # Info # -------------------------------------------------------------------------- def number_of_vertices(self): @@ -2196,7 +2200,7 @@ def is_valid(self): raise NotImplementedError # -------------------------------------------------------------------------- - # vertex topology + # Vertex topology # -------------------------------------------------------------------------- def has_vertex(self, vertex): @@ -2411,7 +2415,7 @@ def is_vertex_on_boundary(self, vertex): return False # -------------------------------------------------------------------------- - # edge topology + # Edge topology # -------------------------------------------------------------------------- def has_edge(self, edge): @@ -2518,7 +2522,7 @@ def is_edge_on_boundary(self, edge): return None in self._plane[u][v].values() # -------------------------------------------------------------------------- - # halfface topology + # Halfface topology # -------------------------------------------------------------------------- def has_halfface(self, halfface): @@ -2836,7 +2840,7 @@ def is_halfface_on_boundary(self, halfface): return self._plane[w][v][u] is None # -------------------------------------------------------------------------- - # cell topology + # Cell topology # -------------------------------------------------------------------------- def cell_vertices(self, cell): @@ -3154,7 +3158,7 @@ def is_cell_on_boundary(self, cell): return False # -------------------------------------------------------------------------- - # boundary + # Boundary # -------------------------------------------------------------------------- def vertices_on_boundaries(self): From 6a7eb03dd1f4a0d67b2565ef382009229dfb3dd9 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Fri, 11 Aug 2023 10:23:52 +0200 Subject: [PATCH 20/68] changed NotImplmentedError to PluginNotInstalledError --- src/compas/geometry/curves/nurbs.py | 65 +++++++++------------ src/compas/geometry/surfaces/nurbs.py | 84 +++++++++++---------------- 2 files changed, 59 insertions(+), 90 deletions(-) diff --git a/src/compas/geometry/curves/nurbs.py b/src/compas/geometry/curves/nurbs.py index 59db7ce5638..1829a8fa1ee 100644 --- a/src/compas/geometry/curves/nurbs.py +++ b/src/compas/geometry/curves/nurbs.py @@ -5,6 +5,7 @@ from math import sqrt from compas.plugins import pluggable +from compas.plugins import PluginNotInstalledError from compas.geometry import Point from compas.geometry import Frame @@ -20,22 +21,22 @@ def new_nurbscurve(cls, *args, **kwargs): @pluggable(category="factories") def new_nurbscurve_from_parameters(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbscurve_from_points(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbscurve_from_interpolation(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbscurve_from_step(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError class NurbsCurve(Curve): @@ -67,10 +68,10 @@ class NurbsCurve(Curve): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "points": {"type": "array", "minItems": 2, "items": Point.JSONSCHEMA}, + "points": {"type": "array", "minItems": 2, "items": Point.DATASCHEMA}, "weights": {"type": "array", "items": {"type": "number"}}, "knots": {"type": "array", "items": {"type": "number"}}, "multiplicities": {"type": "array", "items": {"type": "integer"}}, @@ -87,25 +88,16 @@ def __new__(cls, *args, **kwargs): def __init__(self, name=None): super(NurbsCurve, self).__init__(name=name) - def __eq__(self, other): - raise NotImplementedError - - def __str__(self): - lines = [ - "NurbsCurve", - "----------", - "Points: {}".format(self.points), - "Weights: {}".format(self.weights), - "Knots: {}".format(self.knots), - "Mults: {}".format(self.multiplicities), - "Degree: {}".format(self.degree), - "Order: {}".format(self.order), - "Domain: {}".format(self.domain), - "Closed: {}".format(self.is_closed), - "Periodic: {}".format(self.is_periodic), - "Rational: {}".format(self.is_rational), - ] - return "\n".join(lines) + def __repr__(self): + return "{0}(points={1!r}, weigths={2}, knots={3}, multiplicities={4}, degree={5}, is_periodic={6})".format( + type(self).__name__, + self.points, + self.weights, + self.knots, + self.multiplicities, + self.degree, + self.is_periodic, + ) # ============================================================================== # Data @@ -113,14 +105,12 @@ def __str__(self): @property def dtype(self): - """str : The type of the object in the form of a '2-level' import and a class name.""" return "compas.geometry/NurbsCurve" @property def data(self): - """dict : Representation of the curve as a dict containing only native Python data.""" return { - "points": self.points, + "points": [point.data for point in self.points], "weights": self.weights, "knots": self.knots, "multiplicities": self.multiplicities, @@ -128,10 +118,6 @@ def data(self): "is_periodic": self.is_periodic, } - @data.setter - def data(self, data): - raise NotImplementedError - @classmethod def from_data(cls, data): """Construct a NURBS curve from its data representation. @@ -147,13 +133,14 @@ def from_data(cls, data): The constructed curve. """ - points = data["points"] - weights = data["weights"] - knots = data["knots"] - multiplicities = data["multiplicities"] - degree = data["degree"] - is_periodic = data["is_periodic"] - return cls.from_parameters(points, weights, knots, multiplicities, degree, is_periodic) + return cls.from_parameters( + data["points"], + data["weights"], + data["knots"], + data["multiplicities"], + data["degree"], + data["is_periodic"], + ) # ============================================================================== # Properties diff --git a/src/compas/geometry/surfaces/nurbs.py b/src/compas/geometry/surfaces/nurbs.py index 9d23b571712..1c52d262816 100644 --- a/src/compas/geometry/surfaces/nurbs.py +++ b/src/compas/geometry/surfaces/nurbs.py @@ -3,6 +3,7 @@ from __future__ import division from compas.plugins import pluggable +from compas.plugins import PluginNotInstalledError from compas.geometry import Point from compas.utilities import linspace from compas.utilities import meshgrid @@ -12,27 +13,27 @@ @pluggable(category="factories") def new_nurbssurface(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbssurface_from_parameters(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbssurface_from_points(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbssurface_from_fill(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError @pluggable(category="factories") def new_nurbssurface_from_step(cls, *args, **kwargs): - raise NotImplementedError + raise PluginNotInstalledError class NurbsSurface(Surface): @@ -64,10 +65,10 @@ class NurbsSurface(Surface): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "points": {"type": "array", "items": {"type": "array", "items": Point.JSONSCHEMA}}, + "points": {"type": "array", "items": {"type": "array", "items": Point.DATASCHEMA}}, "weights": {"type": "array", "items": {"type": "array", "items": {"type": "number"}}}, "u_knots": {"type": "array", "items": {"type": "number"}}, "v_knots": {"type": "array", "items": {"type": "number"}}, @@ -88,24 +89,20 @@ def __new__(cls, *args, **kwargs): def __init__(self, name=None): super(NurbsSurface, self).__init__(name=name) - def __str__(self): - lines = [ - "NurbsSurface", - "------------", - "Points: {}".format(self.points), - "Weights: {}".format(self.weights), - "U Knots: {}".format(self.u_knots), - "V Knots: {}".format(self.v_knots), - "U Mults: {}".format(self.u_mults), - "V Mults: {}".format(self.v_mults), - "U Degree: {}".format(self.u_degree), - "V Degree: {}".format(self.v_degree), - "U Domain: {}".format(self.u_domain), - "V Domain: {}".format(self.v_domain), - "U Periodic: {}".format(self.is_u_periodic), - "V Periodic: {}".format(self.is_v_periodic), - ] - return "\n".join(lines) + def __repr__(self): + return "{0}(points={1!r}, weigths={2}, u_knots={3}, v_knots={4}, u_mults={5}, v_mults={6}, u_degree={7}, v_degree={8}, is_u_periodic={9}, is_v_periodic={10})".format( + type(self).__name__, + self.points, + self.weights, + self.u_knots, + self.v_knots, + self.u_mults, + self.v_mults, + self.u_degree, + self.v_degree, + self.is_u_periodic, + self.is_v_periodic, + ) # ============================================================================== # Data @@ -117,9 +114,8 @@ def dtype(self): @property def data(self): - """dict : Representation of the curve as a dict containing only native Python objects.""" return { - "points": self.points, + "points": [point.data for point in self.points], "weights": self.weights, "u_knots": self.u_knots, "v_knots": self.v_knots, @@ -131,10 +127,6 @@ def data(self): "is_v_periodic": self.is_v_periodic, } - @data.setter - def data(self, data): - raise NotImplementedError - @classmethod def from_data(cls, data): """Construct a BSpline surface from its data representation. @@ -150,27 +142,17 @@ def from_data(cls, data): The constructed surface. """ - points = data["points"] - weights = data["weights"] - u_knots = data["u_knots"] - v_knots = data["v_knots"] - u_mults = data["u_mults"] - v_mults = data["v_mults"] - u_degree = data["u_degree"] - v_degree = data["v_degree"] - is_u_periodic = data["is_u_periodic"] - is_v_periodic = data["is_v_periodic"] return cls.from_parameters( - points, - weights, - u_knots, - v_knots, - u_mults, - v_mults, - u_degree, - v_degree, - is_u_periodic, - is_v_periodic, + data["points"], + data["weights"], + data["u_knots"], + data["v_knots"], + data["u_mults"], + data["v_mults"], + data["u_degree"], + data["v_degree"], + data["is_u_periodic"], + data["is_v_periodic"], ) # ============================================================================== From ea7af772c47211634c0d56bd76e015ba2e1cb9c7 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Fri, 11 Aug 2023 10:24:30 +0200 Subject: [PATCH 21/68] simplify log --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d921ef3363..db9c85ca63c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -227,7 +227,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Removed class attribute `AVAILABLE_CONTEXTS` form `compas.artists.Artist`. * Removed `compas.geometry.Primitive`. * Removed classmethod `compas.color.Color.from_data`. -* Removed setter of property `compas.geometry.Vector.data`. * Removed `validate_data` from `compas.data.validators`. * Removed `json_validate` from `compas.data.json`. From b2c23d2ac1e2a44bcc3537a2e09e5274ee8a8a5d Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Fri, 11 Aug 2023 10:24:38 +0200 Subject: [PATCH 22/68] data interface update --- src/compas/geometry/brep/brep.py | 20 ++-- src/compas/geometry/curves/arc.py | 18 +++- src/compas/geometry/curves/bezier.py | 9 +- src/compas/geometry/curves/circle.py | 16 ++- src/compas/geometry/curves/curve.py | 41 +------- src/compas/geometry/curves/ellipse.py | 25 ++++- src/compas/geometry/curves/hyperbola.py | 25 ++++- src/compas/geometry/curves/line.py | 14 ++- src/compas/geometry/curves/parabola.py | 16 ++- src/compas/geometry/curves/polyline.py | 11 +- src/compas/geometry/frame.py | 51 ++------- src/compas/geometry/geometry.py | 3 + src/compas/geometry/plane.py | 47 +++------ src/compas/geometry/point.py | 39 ++----- src/compas/geometry/pointcloud.py | 16 +-- src/compas/geometry/polygon.py | 27 +---- src/compas/geometry/polyhedron.py | 40 ++----- src/compas/geometry/projection.py | 3 - src/compas/geometry/quaternion.py | 27 +---- src/compas/geometry/reflection.py | 19 ++-- src/compas/geometry/rotation.py | 3 - src/compas/geometry/scale.py | 3 - src/compas/geometry/shapes/box.py | 28 +++-- src/compas/geometry/shapes/capsule.py | 59 ++++------- src/compas/geometry/shapes/cone.py | 59 ++++------- src/compas/geometry/shapes/cylinder.py | 59 ++++------- src/compas/geometry/shapes/shape.py | 4 - src/compas/geometry/shapes/sphere.py | 51 ++++----- src/compas/geometry/shapes/torus.py | 40 ++----- src/compas/geometry/shear.py | 3 - src/compas/geometry/surfaces/conical.py | 37 +++++-- src/compas/geometry/surfaces/cylindrical.py | 33 ++++-- src/compas/geometry/surfaces/planar.py | 37 +++++-- src/compas/geometry/surfaces/spherical.py | 33 ++++-- src/compas/geometry/surfaces/surface.py | 41 ++------ src/compas/geometry/surfaces/toroidal.py | 37 +++++-- src/compas/geometry/transformation.py | 110 ++++++++++---------- src/compas/geometry/translation.py | 3 - 38 files changed, 480 insertions(+), 627 deletions(-) diff --git a/src/compas/geometry/brep/brep.py b/src/compas/geometry/brep/brep.py index 8d64ba4001b..aea8e0c2e6b 100644 --- a/src/compas/geometry/brep/brep.py +++ b/src/compas/geometry/brep/brep.py @@ -203,16 +203,6 @@ def __str__(self): # Data # ============================================================================== - @property - def DATASCHEMA(self): - import schema - - return schema.Schema( - { - "faces": list, - } - ) - @property def data(self): faces = [] @@ -220,9 +210,13 @@ def data(self): faces.append(face.data) return {"faces": faces} - @data.setter - def data(self): - raise NotImplementedError + @classmethod + def from_data(cls, data): + # faces = [] + # for face in data["faces"]: + # faces.append(BrepFace.from_data(face)) + # return cls.from_brepfaces(faces) + pass # ============================================================================== # Properties diff --git a/src/compas/geometry/curves/arc.py b/src/compas/geometry/curves/arc.py index 616ef4889a6..83ecdc678f6 100644 --- a/src/compas/geometry/curves/arc.py +++ b/src/compas/geometry/curves/arc.py @@ -111,14 +111,14 @@ class Arc(Curve): """ - JSONSCHEMA = { + DATASCHEMA = { "value": { "type": "object", "properties": { "radius": {"type": "number", "minimum": 0}, "start_angle": {"type": "number", "minimum": 0, "optional": True}, "end_angle": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["frame", "radius", "start_angle", "end_angle"], } @@ -141,7 +141,8 @@ def __init__(self, radius, start_angle, end_angle, frame=None, **kwargs): self.end_angle = end_angle def __repr__(self): - return "Arc(radius={0!r}, start_angle={1!r}, end_angle={2!r}, frame={3!r})".format( + return "{0}(radius={1}, start_angle={2}, end_angle={3}, frame={4!r})".format( + type(self).__name__, self.radius, self.start_angle, self.end_angle, @@ -166,12 +167,21 @@ def __eq__(self, other): @property def data(self): return { - "frame": self.frame, "radius": self.radius, "start_angle": self.start_angle, "end_angle": self.end_angle, + "frame": self.frame.data, } + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + start_angle=data["start_angle"], + end_angle=data["end_angle"], + frame=Frame.from_data(data["frame"]), + ) + # ============================================================================= # Properties # ============================================================================= diff --git a/src/compas/geometry/curves/bezier.py b/src/compas/geometry/curves/bezier.py index fe259bb5ddd..a2a4bd6a610 100644 --- a/src/compas/geometry/curves/bezier.py +++ b/src/compas/geometry/curves/bezier.py @@ -160,10 +160,10 @@ class Bezier(Curve): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "points": {"type": "array", "minItems": 2, "items": Point.JSONSCHEMA}, + "points": {"type": "array", "minItems": 2, "items": Point.DATASCHEMA}, }, "required": ["points"], } @@ -180,13 +180,16 @@ def __init__(self, points, **kwargs): self._points = [] self.points = points + def __repr__(self): + return "{0}(points={1!r})".format(type(self).__name__, self.points) + # ========================================================================== # Data # ========================================================================== @property def data(self): - return {"points": self.points} + return {"points": [point.data for point in self.points]} # ========================================================================== # Properties diff --git a/src/compas/geometry/curves/circle.py b/src/compas/geometry/curves/circle.py index fdf1139b0df..04109bb5410 100644 --- a/src/compas/geometry/curves/circle.py +++ b/src/compas/geometry/curves/circle.py @@ -85,10 +85,10 @@ class Circle(Conic): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, "radius": {"type": "number", "minimum": 0}, }, "required": ["frame", "radius"], @@ -100,7 +100,11 @@ def __init__(self, radius, frame=None, **kwargs): self.radius = radius def __repr__(self): - return "Circle(radius={0!r}, frame={1!r})".format(self.radius, self.frame) + return "{0}(radius={1!r}, frame={2!r})".format( + type(self).__name__, + self.radius, + self.frame, + ) def __eq__(self, other): try: @@ -116,7 +120,11 @@ def __eq__(self, other): @property def data(self): - return {"frame": self.frame, "radius": self.radius} + return {"radius": self.radius, "frame": self.frame.data} + + @classmethod + def from_data(cls, data): + return cls(radius=data["radius"], frame=Frame.from_data(data["frame"])) # ========================================================================== # Properties diff --git a/src/compas/geometry/curves/curve.py b/src/compas/geometry/curves/curve.py index c216524d570..202bf959a05 100644 --- a/src/compas/geometry/curves/curve.py +++ b/src/compas/geometry/curves/curve.py @@ -77,42 +77,11 @@ def __init__(self, frame=None, name=None): self.frame = frame def __repr__(self): - return "Curve(frame={0!r}, domain={1!r})".format(self.frame, self.domain) - - def __str__(self): - return "".format(self.domain, self.frame) - - def __eq__(self, other): - raise NotImplementedError - - # ============================================================================== - # Data - # ============================================================================== - - @property - def data(self): - raise NotImplementedError - - # @data.setter - # def data(self, data): - # raise NotImplementedError - - @classmethod - def from_data(cls, data): - """Construct a curve from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Curve` - The constructed curve. - - """ - return cls(**data) + return "{0}(frame={1!r}, domain={2})".format( + type(self).__name__, + self.frame, + self.domain, + ) # ============================================================================== # Properties diff --git a/src/compas/geometry/curves/ellipse.py b/src/compas/geometry/curves/ellipse.py index 14d755e3e47..3131fbc4f9e 100644 --- a/src/compas/geometry/curves/ellipse.py +++ b/src/compas/geometry/curves/ellipse.py @@ -106,12 +106,12 @@ class Ellipse(Conic): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "major": {"type": "number", "minimum": 0}, "minor": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["frame", "major", "minor"], } @@ -124,7 +124,12 @@ def __init__(self, major=1.0, minor=1.0, frame=None, **kwargs): self.minor = minor def __repr__(self): - return "Ellipse(major={0!r}, minor={1!r}, frame={2!r})".format(self.major, self.minor, self.frame) + return "{0}(major={1!r}, minor={2}, frame={3!r})".format( + type(self).__name__, + self.major, + self.minor, + self.frame, + ) def __eq__(self, other): try: @@ -141,7 +146,19 @@ def __eq__(self, other): @property def data(self): - return {"frame": self.frame, "major": self.major, "minor": self.minor} + return { + "major": self.major, + "minor": self.minor, + "frame": self.frame.data, + } + + @classmethod + def from_data(cls, data): + return cls( + major=data["major"], + minor=data["minor"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # Properties diff --git a/src/compas/geometry/curves/hyperbola.py b/src/compas/geometry/curves/hyperbola.py index fb84922727f..929afa6ca3b 100644 --- a/src/compas/geometry/curves/hyperbola.py +++ b/src/compas/geometry/curves/hyperbola.py @@ -110,12 +110,12 @@ class Hyperbola(Conic): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "major": {"type": "number", "minimum": 0}, "minor": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["frame", "major", "minor"], } @@ -128,7 +128,12 @@ def __init__(self, major, minor, frame=None, **kwargs): self.minor = minor def __repr__(self): - return "Hyperbola(major={0!r}, minor={1!r}, frame={2!r})".format(self.major, self.minor, self.frame) + return "{0}(major={1}, minor={2}, frame={3!r})".format( + type(self).__name__, + self.major, + self.minor, + self.frame, + ) def __eq__(self, other): try: @@ -142,7 +147,19 @@ def __eq__(self, other): @property def data(self): - return {"frame": self.frame, "major": self.major, "minor": self.minor} + return { + "major": self.major, + "minor": self.minor, + "frame": self.frame.data, + } + + @classmethod + def from_data(cls, data): + return cls( + major=data["major"], + minor=data["minor"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # Properties diff --git a/src/compas/geometry/curves/line.py b/src/compas/geometry/curves/line.py index 42fe6bb68e7..fc1f8f19e8f 100644 --- a/src/compas/geometry/curves/line.py +++ b/src/compas/geometry/curves/line.py @@ -65,11 +65,11 @@ class Line(Curve): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "start": Point.JSONSCHEMA, - "end": Point.JSONSCHEMA, + "start": Point.DATASCHEMA, + "end": Point.DATASCHEMA, }, "required": ["start", "end"], } @@ -91,7 +91,11 @@ def __init__(self, start, end, **kwargs): self.end = end def __repr__(self): - return "Line({0!r}, {1!r})".format(self.start, self.end) + return "{0}({1!r}, {2!r})".format( + type(self).__name__, + self.start, + self.end, + ) def __getitem__(self, key): if key == 0: @@ -126,7 +130,7 @@ def __eq__(self, other): @property def data(self): - return {"start": self.start, "end": self.end} + return {"start": self.start.data, "end": self.end.data} # ========================================================================== # properties diff --git a/src/compas/geometry/curves/parabola.py b/src/compas/geometry/curves/parabola.py index 8a55dcf64d2..60923650c53 100644 --- a/src/compas/geometry/curves/parabola.py +++ b/src/compas/geometry/curves/parabola.py @@ -4,6 +4,7 @@ from compas.geometry import Vector from compas.geometry import Point +from compas.geometry import Frame from .line import Line from .conic import Conic @@ -84,7 +85,11 @@ def __init__(self, focal, frame=None, **kwargs): self.focal = focal def __repr__(self): - return "Parabola(focal={0!r}, frame={1!r})".format(self.focal, self.frame) + return "{0}(focal={1}, frame={2!r})".format( + type(self).__name__, + self.focal, + self.frame, + ) def __eq__(self, other): try: @@ -98,7 +103,14 @@ def __eq__(self, other): @property def data(self): - return {"frame": self.frame, "focal": self.focal} + return {"focal": self.focal, "frame": self.frame.data} + + @classmethod + def from_data(cls, data): + return cls( + focal=data["focal"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # properties diff --git a/src/compas/geometry/curves/polyline.py b/src/compas/geometry/curves/polyline.py index 33de77c4379..e8a158c61fd 100644 --- a/src/compas/geometry/curves/polyline.py +++ b/src/compas/geometry/curves/polyline.py @@ -68,10 +68,10 @@ class Polyline(Curve): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "points": {"type": "array", "minItems": 2, "items": Point.JSONSCHEMA}, + "points": {"type": "array", "minItems": 2, "items": Point.DATASCHEMA}, }, "required": ["points"], } @@ -90,7 +90,10 @@ def __init__(self, points, **kwargs): self.points = points def __repr__(self): - return "Polyline([{0}])".format(", ".join(["{0!r}".format(point) for point in self.points])) + return "{0}({1!r})".format( + type(self).__name__, + self.points, + ) def __getitem__(self, key): return self.points[key] @@ -116,7 +119,7 @@ def __eq__(self, other): @property def data(self): - return {"points": self.points} + return {"points": [point.data for point in self.points]} # ========================================================================== # properties diff --git a/src/compas/geometry/frame.py b/src/compas/geometry/frame.py index 29575b75b5f..91b0d63253f 100644 --- a/src/compas/geometry/frame.py +++ b/src/compas/geometry/frame.py @@ -67,12 +67,12 @@ class Frame(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "point": Point.JSONSCHEMA, - "xaxis": Vector.JSONSCHEMA, - "yaxis": Vector.JSONSCHEMA, + "point": Point.DATASCHEMA, + "xaxis": Vector.DATASCHEMA, + "yaxis": Vector.DATASCHEMA, }, "required": ["point", "xaxis", "yaxis"], } @@ -88,7 +88,12 @@ def __init__(self, point, xaxis, yaxis, **kwargs): self.yaxis = yaxis def __repr__(self): - return "Frame({0!r}, {1!r}, {2!r})".format(self.point, self.xaxis, self.yaxis) + return "{0}(point={1!r}, xaxis={2!r}, yaxis={3!r})".format( + type(self).__name__, + self.point, + self.xaxis, + self.yaxis, + ) def __len__(self): return 3 @@ -133,42 +138,6 @@ def data(self): "yaxis": self.yaxis.data, } - @data.setter - def data(self, data): - self.point = data["point"] - self.xaxis = data["xaxis"] - self.yaxis = data["yaxis"] - - @classmethod - def from_data(cls, data): - """Construct a frame from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Frame` - The constructed frame. - - Examples - -------- - >>> data = {'point': [0.0, 0.0, 0.0], 'xaxis': [1.0, 0.0, 0.0], 'yaxis': [0.0, 1.0, 0.0]} - >>> frame = Frame.from_data(data) - >>> frame.point - Point(0.000, 0.000, 0.000) - >>> frame.xaxis - Vector(1.000, 0.000, 0.000) - >>> frame.yaxis - Vector(0.000, 1.000, 0.000) - >>> frame.zaxis - Vector(0.000, 0.000, 1.000) - - """ - return cls(data["point"], data["xaxis"], data["yaxis"]) - # ========================================================================== # Properties # ========================================================================== diff --git a/src/compas/geometry/geometry.py b/src/compas/geometry/geometry.py index 3ea17ea2811..45b427d1cf7 100644 --- a/src/compas/geometry/geometry.py +++ b/src/compas/geometry/geometry.py @@ -11,6 +11,9 @@ class Geometry(Data): def __init__(self, *args, **kwargs): super(Geometry, self).__init__(*args, **kwargs) + def __eq__(self, other): + raise NotImplementedError + def __ne__(self, other): # this is not obvious to ironpython return not self.__eq__(other) diff --git a/src/compas/geometry/plane.py b/src/compas/geometry/plane.py index 0cac8de8185..6cc1534208a 100644 --- a/src/compas/geometry/plane.py +++ b/src/compas/geometry/plane.py @@ -42,11 +42,11 @@ class Plane(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "point": Point.JSONSCHEMA, - "normal": Vector.JSONSCHEMA, + "point": Point.DATASCHEMA, + "normal": Vector.DATASCHEMA, }, "required": ["point", "normal"], } @@ -59,7 +59,11 @@ def __init__(self, point, normal, **kwargs): self.normal = normal def __repr__(self): - return "Plane({0!r}, {1!r})".format(self.point, self.normal) + return "{0}(point={1!r}, normal={2!r})".format( + type(self).__name__, + self.point, + self.normal, + ) def __len__(self): return 2 @@ -92,37 +96,10 @@ def __eq__(self, other): @property def data(self): - return {"point": self.point, "normal": self.normal} - - @data.setter - def data(self, data): - self.point = data["point"] - self.normal = data["normal"] - - @classmethod - def from_data(cls, data): - """Construct a plane from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Plane` - The constructed plane. - - Examples - -------- - >>> plane = Plane.from_data({'point': [0.0, 0.0, 0.0], 'normal': [0.0, 0.0, 1.0]}) - >>> plane.point - Point(0.000, 0.000, 0.000) - >>> plane.normal - Vector(0.000, 0.000, 1.000) - - """ - return cls(data["point"], data["normal"]) + return { + "point": self.point.data, + "normal": self.normal.data, + } # ========================================================================== # Properties diff --git a/src/compas/geometry/point.py b/src/compas/geometry/point.py index 96316ff1a86..c5ed1676db5 100644 --- a/src/compas/geometry/point.py +++ b/src/compas/geometry/point.py @@ -18,7 +18,7 @@ from compas.geometry import is_point_behind_plane from compas.geometry import transform_points -from compas.geometry import Geometry +from .geometry import Geometry from .vector import Vector @@ -101,7 +101,7 @@ class Point(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "array", "minItems": 3, "maxItems": 3, @@ -118,7 +118,13 @@ def __init__(self, x, y, z=0.0, **kwargs): self.z = z def __repr__(self): - return "Point({0:.{3}f}, {1:.{3}f}, {2:.{3}f})".format(self.x, self.y, self.z, PRECISION[:1]) + return "{0}(x={1:.{4}f}, y={2:.{4}f}, z={3:.{4}f})".format( + type(self).__name__, + self.x, + self.y, + self.z, + PRECISION[:1], + ) def __len__(self): return 3 @@ -210,33 +216,8 @@ def __ipow__(self, n): def data(self): return list(self) - @data.setter - def data(self, data): - self.x = data[0] - self.y = data[1] - self.z = data[2] - @classmethod def from_data(cls, data): - """Construct a point from a data dict. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Point` - The constructed point. - - Examples - -------- - >>> point = Point.from_data([0.0, 0.0, 0.0]) - >>> point - Point(0.000, 0.000, 0.000) - - """ return cls(*data) # ========================================================================== @@ -648,7 +629,7 @@ def in_polyhedron(self, polyhedron): return all(is_point_behind_plane(self, plane) for plane in planes) # ========================================================================== - # transformations + # Transformations # ========================================================================== def transform(self, T): diff --git a/src/compas/geometry/pointcloud.py b/src/compas/geometry/pointcloud.py index 698a3c299c0..2b7ab46744b 100644 --- a/src/compas/geometry/pointcloud.py +++ b/src/compas/geometry/pointcloud.py @@ -33,10 +33,10 @@ class Pointcloud(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "points": {"type": "array", "items": Point.JSONSCHEMA, "minItems": 1}, + "points": {"type": "array", "items": Point.DATASCHEMA, "minItems": 1}, }, "required": ["points"], } @@ -47,7 +47,7 @@ def __init__(self, points, **kwargs): self.points = points def __repr__(self): - return "Pointcloud({0!r})".format(self.points) + return "{0}(points={1!r})".format(type(self).__name__, self.points) def __len__(self): return len(self.points) @@ -78,15 +78,7 @@ def __eq__(self, other): @property def data(self): - return {"points": self.points} - - @data.setter - def data(self, data): - self._points = data["points"] - - @classmethod - def from_data(cls, data): - return cls(data["points"]) + return {"points": [point.data for point in self.points]} # ========================================================================== # Properties diff --git a/src/compas/geometry/polygon.py b/src/compas/geometry/polygon.py index bc3c9bd3f34..e82c8e9fc30 100644 --- a/src/compas/geometry/polygon.py +++ b/src/compas/geometry/polygon.py @@ -87,9 +87,9 @@ class Polygon(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", - "properties": {"points": {"type": "array", "minItems": 2, "items": Point.JSONSCHEMA}}, + "properties": {"points": {"type": "array", "minItems": 2, "items": Point.DATASCHEMA}}, "required": ["points"], } @@ -102,7 +102,7 @@ def __init__(self, points, **kwargs): self.points = points def __repr__(self): - return "Polygon([{0}])".format(", ".join(["{0!r}".format(point) for point in self.points])) + return "{0}(points={1!r})".format(type(self).__name__, self.points) def __len__(self): return len(self.points) @@ -128,11 +128,7 @@ def __eq__(self, other): @property def data(self): - return {"points": self.points} - - @data.setter - def data(self, data): - self.points = data["points"] + return {"points": [point.data for point in self.points]} # ========================================================================== # Properties @@ -178,21 +174,6 @@ def centroid(self): @property def normal(self): - # o = self.centroid - # points = self.points - # a2 = 0 - # normals = [] - # for i in range(-1, len(points) - 1): - # p1 = points[i] - # p2 = points[i + 1] - # u = [p1[_] - o[_] for _ in range(3)] # type: ignore - # v = [p2[_] - o[_] for _ in range(3)] # type: ignore - # w = cross_vectors(u, v) - # a2 += sum(w[_] ** 2 for _ in range(3)) ** 0.5 - # normals.append(w) - # n = [sum(axis) / a2 for axis in zip(*normals)] - # n = Vector(*n) - # return n return self.plane.normal @property diff --git a/src/compas/geometry/polyhedron.py b/src/compas/geometry/polyhedron.py index 5c3fd099efe..adbb72c7e33 100644 --- a/src/compas/geometry/polyhedron.py +++ b/src/compas/geometry/polyhedron.py @@ -169,7 +169,7 @@ class Polyhedron(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "vertices": { @@ -203,7 +203,11 @@ def __init__(self, vertices, faces, **kwargs): self.faces = faces def __repr__(self): - return "".format(len(self.vertices), len(self.faces)) + return "{0}(vertices={1!r}, faces={2!r})".format( + type(self).__name__, + self.vertices, + self.faces, + ) def __len__(self): return 2 @@ -255,43 +259,15 @@ def __or__(self, other): return self.__add__(other) # ========================================================================== - # data + # Data # ========================================================================== @property def data(self): return {"vertices": self.vertices, "faces": self.faces} - @data.setter - def data(self, data): - self.vertices = data["vertices"] - self.faces = data["faces"] - - @classmethod - def from_data(cls, data): - """Construct a polyhedron from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Polyhedron` - The constructed polyhedron. - - Examples - -------- - >>> from compas.geometry import Polyhedron - >>> p = Polyhedron.from_platonicsolid(4) - >>> q = Polyhedron.from_data(p.data) - - """ - return cls(**data) - # ========================================================================== - # properties + # Properties # ========================================================================== @property diff --git a/src/compas/geometry/projection.py b/src/compas/geometry/projection.py index 69644a6c5cb..7a0aef1823f 100644 --- a/src/compas/geometry/projection.py +++ b/src/compas/geometry/projection.py @@ -48,9 +48,6 @@ def __init__(self, matrix=None, check=False): raise ValueError("This is not a proper projection matrix.") super(Projection, self).__init__(matrix=matrix) - def __repr__(self): - return "Projection({0!r}, check=False)".format(self.matrix) - @classmethod def from_plane(cls, plane): """Construct an orthogonal projection transformation to project onto a plane. diff --git a/src/compas/geometry/quaternion.py b/src/compas/geometry/quaternion.py index 7bac8bb464d..67e2a1eda82 100644 --- a/src/compas/geometry/quaternion.py +++ b/src/compas/geometry/quaternion.py @@ -110,7 +110,7 @@ class Quaternion(Geometry): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "w": {"type": "number"}, @@ -138,35 +138,10 @@ def __init__(self, w, x, y, z, **kwargs): @property def data(self): - """dict : Representation of the quaternion as a dict containing only native Python objects.""" return {"w": self.w, "x": self.x, "y": self.y, "z": self.z} - @data.setter - def data(self, data): - self.w = data["w"] - self.x = data["x"] - self.y = data["y"] - self.z = data["z"] - @classmethod def from_data(cls, data): - """Construct a quaternion from a data dict. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Quaternion` - The constructed quaternion. - - Examples - -------- - >>> - - """ return cls(data["w"], data["x"], data["y"], data["z"]) # ========================================================================== diff --git a/src/compas/geometry/reflection.py b/src/compas/geometry/reflection.py index 3b106a8c065..7b32a1cf818 100644 --- a/src/compas/geometry/reflection.py +++ b/src/compas/geometry/reflection.py @@ -11,14 +11,13 @@ Ippoliti for providing code and documentation. """ -# from compas.utilities import flatten -# from compas.geometry import allclose +from compas.utilities import flatten +from compas.geometry import allclose from compas.geometry import dot_vectors from compas.geometry import cross_vectors from compas.geometry import normalize_vector - -# from compas.geometry import decompose_matrix -# from compas.geometry import matrix_from_perspective_entries +from compas.geometry import decompose_matrix +from compas.geometry import matrix_from_perspective_entries from compas.geometry import identity_matrix from compas.geometry import Transformation @@ -46,15 +45,11 @@ class Reflection(Transformation): def __init__(self, matrix=None, check=False): if matrix and check: - # _, _, _, _, perspective = decompose_matrix(matrix) - # if not allclose(flatten(matrix), flatten(matrix_from_perspective_entries(perspective))): - # raise ValueError("This is not a proper reflection matrix.") - pass + _, _, _, _, perspective = decompose_matrix(matrix) + if not allclose(flatten(matrix), flatten(matrix_from_perspective_entries(perspective))): + raise ValueError("This is not a proper reflection matrix.") super(Reflection, self).__init__(matrix=matrix) - def __repr__(self): - return "Reflection({0!r}, check=False)".format(self.matrix) - @classmethod def from_plane(cls, plane): """Construct a reflection transformation that mirrors wrt the given plane. diff --git a/src/compas/geometry/rotation.py b/src/compas/geometry/rotation.py index 61db06ff7e8..41925c1baf8 100644 --- a/src/compas/geometry/rotation.py +++ b/src/compas/geometry/rotation.py @@ -79,9 +79,6 @@ def __init__(self, matrix=None, check=False): raise ValueError("This is not a proper rotation matrix.") super(Rotation, self).__init__(matrix=matrix) - def __repr__(self): - return "Rotation({0!r}, check=False)".format(self.matrix) - @property def quaternion(self): from compas.geometry import Quaternion diff --git a/src/compas/geometry/scale.py b/src/compas/geometry/scale.py index 0bcf4769489..4cac8e7af13 100644 --- a/src/compas/geometry/scale.py +++ b/src/compas/geometry/scale.py @@ -62,9 +62,6 @@ def __init__(self, matrix=None, check=False): raise ValueError("This is not a proper scale matrix.") super(Scale, self).__init__(matrix=matrix) - def __repr__(self): - return "Scale({0!r}, check=False)".format(self.matrix) - @classmethod def from_factors(cls, factors, frame=None): """Construct a scale transformation from scale factors. diff --git a/src/compas/geometry/shapes/box.py b/src/compas/geometry/shapes/box.py index 03dcbe574fa..1a0f675f1a4 100644 --- a/src/compas/geometry/shapes/box.py +++ b/src/compas/geometry/shapes/box.py @@ -96,13 +96,13 @@ class Box(Shape): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "xsize": {"type": "number", "minimum": 0}, "ysize": {"type": "number", "minimum": 0}, "zsize": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "additionalProperties": False, "minProperties": 4, @@ -118,8 +118,12 @@ def __init__(self, xsize=1.0, ysize=None, zsize=None, frame=None, **kwargs): self.zsize = xsize if zsize is None else zsize def __repr__(self): - return "Box(xsize={0!r}, ysize={1!r}, zsize={2!r}, frame={3!r})".format( - self.xsize, self.ysize, self.zsize, self.frame + return "{0}(xsize={1}, ysize={2}, zsize={3}, frame={4!r})".format( + type(self).__name__, + self.xsize, + self.ysize, + self.zsize, + self.frame, ) def __len__(self): @@ -159,18 +163,20 @@ def __iter__(self): @property def data(self): return { - "frame": self.frame, "xsize": self.xsize, "ysize": self.ysize, "zsize": self.zsize, + "frame": self.frame.data, } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.xsize = data["xsize"] - self.ysize = data["ysize"] - self.zsize = data["zsize"] + @classmethod + def from_data(cls, data): + return cls( + xsize=data["xsize"], + ysize=data["ysize"], + zsize=data["zsize"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # Properties diff --git a/src/compas/geometry/shapes/capsule.py b/src/compas/geometry/shapes/capsule.py index 8af46cbe713..539076892c0 100644 --- a/src/compas/geometry/shapes/capsule.py +++ b/src/compas/geometry/shapes/capsule.py @@ -73,17 +73,17 @@ class Capsule(Shape): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "frame": Frame.JSONSCHEMA, "radius": {"type": "number", "minimum": 0}, "height": {"type": "number", "minimum": 0}, + "frame": Frame.DATASCHEMA, }, - "required": ["frame", "radius", "height"], + "required": ["radius", "height", "frame"], } - def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs): + def __init__(self, radius, height, frame=None, **kwargs): super(Capsule, self).__init__(frame=frame, **kwargs) self._radius = None self._height = None @@ -91,33 +91,12 @@ def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs): self.height = height def __repr__(self): - return "Capsule(frame={0!r}, radius={1!r}, height={2!r})".format(self.frame, self.radius, self.height) - - def __len__(self): - return 2 - - def __getitem__(self, key): - if key == 0: - return self.frame - elif key == 1: - return self.radius - elif key == 2: - return self.height - else: - raise KeyError - - def __setitem__(self, key, value): - if key == 0: - self.frame = value - elif key == 1: - self.radius = value - elif key == 2: - self.height = value - else: - raise KeyError - - def __iter__(self): - return iter([self.frame, self.radius, self.height]) + return "{0}(radius={1}, height={2}, frame={3!r})".format( + type(self).__name__, + self.radius, + self.height, + self.frame, + ) # ========================================================================== # Data @@ -125,13 +104,19 @@ def __iter__(self): @property def data(self): - return {"frame": self.frame, "radius": self.radius, "height": self.height} + return { + "radius": self.radius, + "height": self.height, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.radius = data["radius"] - self.height = data["height"] + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + height=data["height"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # Properties diff --git a/src/compas/geometry/shapes/cone.py b/src/compas/geometry/shapes/cone.py index 7e4b12f1a55..40d74a6efc5 100644 --- a/src/compas/geometry/shapes/cone.py +++ b/src/compas/geometry/shapes/cone.py @@ -75,17 +75,17 @@ class Cone(Shape): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "frame": Frame.JSONSCHEMA, "radius": {"type": "number", "minimum": 0}, "height": {"type": "number", "minimum": 0}, + "frame": Frame.DATASCHEMA, }, - "required": ["frame", "radius", "height"], + "required": ["radius", "height", "frame"], } - def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs): + def __init__(self, radius, height, frame=None, **kwargs): super(Cone, self).__init__(frame=frame, **kwargs) self._radius = None self._height = None @@ -93,33 +93,12 @@ def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs): self.height = height def __repr__(self): - return "Cone(frame={0!r}, radius={1!r}, height={2!r})".format(self.frame, self.radius, self.height) - - def __len__(self): - return 2 - - def __getitem__(self, key): - if key == 0: - return self.frame - elif key == 1: - return self.radius - if key == 2: - return self.height - else: - raise KeyError - - def __setitem__(self, key, value): - if key == 0: - self.frame = value - elif key == 1: - self.radius = value - elif key == 2: - self.height = value - else: - raise KeyError - - def __iter__(self): - return iter([self.frame, self.radius, self.height]) + return "{0}(radius={1}, height={2}, frame={3!r})".format( + type(self).__name__, + self.radius, + self.height, + self.frame, + ) # ========================================================================== # data @@ -127,13 +106,19 @@ def __iter__(self): @property def data(self): - return {"frame": self.frame, "radius": self.radius, "height": self.height} + return { + "radius": self.radius, + "height": self.height, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.radius = data["radius"] - self.height = data["height"] + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + height=data["height"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # properties diff --git a/src/compas/geometry/shapes/cylinder.py b/src/compas/geometry/shapes/cylinder.py index 834942e12ed..3c2e36f7fa8 100644 --- a/src/compas/geometry/shapes/cylinder.py +++ b/src/compas/geometry/shapes/cylinder.py @@ -72,17 +72,17 @@ class Cylinder(Shape): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "frame": Frame.JSONSCHEMA, "radius": {"type": "number", "minimum": 0}, "height": {"type": "number", "minimum": 0}, + "frame": Frame.DATASCHEMA, }, - "required": ["frame", "radius", "height"], + "required": ["radius", "height", "frame"], } - def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs): + def __init__(self, radius, height, frame=None, **kwargs): super(Cylinder, self).__init__(frame=frame, **kwargs) self._radius = None self._height = None @@ -90,33 +90,12 @@ def __init__(self, frame=None, radius=0.3, height=1.0, **kwargs): self.height = height def __repr__(self): - return "Cylinder(frame={0!r}, radius={1!r}, height={2!r})".format(self.frame, self.radius, self.height) - - def __len__(self): - return 2 - - def __getitem__(self, key): - if key == 0: - return self.frame - elif key == 1: - return self.radius - elif key == 2: - return self.height - else: - raise KeyError - - def __setitem__(self, key, value): - if key == 0: - self.frame = value - elif key == 1: - self.radius = value - elif key == 2: - self.height = value - else: - raise KeyError - - def __iter__(self): - return iter([self.frame, self.radius, self.height]) + return "{0}(radius={1}, height={2}, frame={3!r})".format( + type(self).__name__, + self.radius, + self.height, + self.frame, + ) # ========================================================================== # Data @@ -124,13 +103,19 @@ def __iter__(self): @property def data(self): - return {"frame": self.frame, "radius": self.radius, "height": self.height} + return { + "radius": self.radius, + "height": self.height, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.radius = data["radius"] - self.height = data["height"] + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + height=data["height"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # Properties diff --git a/src/compas/geometry/shapes/shape.py b/src/compas/geometry/shapes/shape.py index c10d94c5a16..140189291b5 100644 --- a/src/compas/geometry/shapes/shape.py +++ b/src/compas/geometry/shapes/shape.py @@ -90,10 +90,6 @@ def point(self): def point(self, point): self.frame.point = point - @property - def normal(self): - return self.frame.zaxis - @property def plane(self): return Plane(self.frame.point, self.frame.zaxis) diff --git a/src/compas/geometry/shapes/sphere.py b/src/compas/geometry/shapes/sphere.py index ea1b39b6d9c..e273c7105d9 100644 --- a/src/compas/geometry/shapes/sphere.py +++ b/src/compas/geometry/shapes/sphere.py @@ -60,16 +60,16 @@ class Sphere(Shape): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "frame": Frame.JSONSCHEMA, "radius": {"type": "number", "minimum": 0}, + "frame": Frame.DATASCHEMA, }, - "required": ["frame", "radius"], + "required": ["radius", "frame"], } - def __init__(self, frame=None, radius=None, point=None, **kwargs): + def __init__(self, radius, point=None, frame=None, **kwargs): super(Sphere, self).__init__(frame=frame, **kwargs) self._radius = 1.0 self.radius = radius @@ -77,29 +77,11 @@ def __init__(self, frame=None, radius=None, point=None, **kwargs): self.frame.point = point def __repr__(self): - return "Sphere(frame={0!r}, radius={1!r})".format(self.frame, self.radius) - - def __len__(self): - return 2 - - def __getitem__(self, key): - if key == 0: - return self.frame - elif key == 1: - return self.radius - else: - raise KeyError - - def __setitem__(self, key, value): - if key == 0: - self.frame = value - elif key == 1: - self.radius = value - else: - raise KeyError - - def __iter__(self): - return iter([self.frame, self.radius]) + return "{0}(radius={1}, frame={2!r})".format( + type(self).__name__, + self.radius, + self.frame, + ) # ========================================================================== # Data @@ -107,12 +89,17 @@ def __iter__(self): @property def data(self): - return {"frame": self.frame, "radius": self.radius} + return { + "radius": self.radius, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.radius = data["radius"] + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # Properties diff --git a/src/compas/geometry/shapes/torus.py b/src/compas/geometry/shapes/torus.py index 64f52ddcd42..bd869272063 100644 --- a/src/compas/geometry/shapes/torus.py +++ b/src/compas/geometry/shapes/torus.py @@ -63,17 +63,17 @@ class Torus(Shape): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { - "frame": Frame.JSONSCHEMA, "radius_axis": {"type": "number", "minimum": 0}, "radius_pipe": {"type": "number", "minimum": 0}, + "frame": Frame.DATASCHEMA, }, - "required": ["frame", "radius_axis", "radius_pipe"], + "required": ["radius_axis", "radius_pipe", "frame"], } - def __init__(self, frame=None, radius_axis=1.0, radius_pipe=0.3, **kwargs): + def __init__(self, radius_axis, radius_pipe, frame=None, **kwargs): super(Torus, self).__init__(frame=frame, **kwargs) self._radius_axis = None self._radius_pipe = None @@ -87,38 +87,18 @@ def __init__(self, frame=None, radius_axis=1.0, radius_pipe=0.3, **kwargs): @property def data(self): return { - "frame": self.frame, "radius_axis": self.radius_axis, "radius_pipe": self.radius_pipe, + "frame": self.frame.data, } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.radius_axis = data["radius_axis"] - self.radius_pipe = data["radius_pipe"] - @classmethod def from_data(cls, data): - """Construct a torus from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Torus` - The constructed torus. - - Examples - -------- - >>> data = {"frame": Frame.worldXY(), "radius_axis": 1.0, "radius_pipe": 0.3} - >>> torus = Torus.from_data(data) - - """ - return cls(**data) + return cls( + radius_axis=data["radius_axis"], + radius_pipe=data["radius_pipe"], + frame=Frame.from_data(data["frame"]), + ) # ========================================================================== # properties diff --git a/src/compas/geometry/shear.py b/src/compas/geometry/shear.py index 0a5a7557e61..63c2e6d25fa 100644 --- a/src/compas/geometry/shear.py +++ b/src/compas/geometry/shear.py @@ -51,9 +51,6 @@ def __init__(self, matrix=None, check=False): raise ValueError("This is not a proper shear matrix.") super(Shear, self).__init__(matrix=matrix) - def __repr__(self): - return "Shear({0!r}, check=False)".format(self.matrix) - @classmethod def from_angle_direction_plane(cls, angle, direction, plane): """Construct a shear transformation from an angle, direction and plane. diff --git a/src/compas/geometry/surfaces/conical.py b/src/compas/geometry/surfaces/conical.py index 54937a6d2b7..2def358d447 100644 --- a/src/compas/geometry/surfaces/conical.py +++ b/src/compas/geometry/surfaces/conical.py @@ -29,12 +29,12 @@ class ConicalSurface(Surface): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "radius": {"type": "number", "minimum": 0}, "height": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["radius", "height", "frame"], } @@ -54,7 +54,12 @@ def __init__(self, radius, height, frame=None, **kwargs): self.height = height def __repr__(self): - return "ConicalSurface(radius={0!r}, height={1!r}, frame={2!r})".format(self.radius, self.height, self.frame) + return "{0}(radius={1}, height={2}, frame={3!r})".format( + type(self).__name__, + self.radius, + self.height, + self.frame, + ) def __eq__(self, other): try: @@ -65,15 +70,29 @@ def __eq__(self, other): return False return self.radius == other_radius and self.height == other_height and self.frame == other_frame + # ============================================================================= + # Data + # ============================================================================= + @property def data(self): - return {"radius": self.radius, "height": self.height, "frame": self.frame} + return { + "radius": self.radius, + "height": self.height, + "frame": self.frame.data, + } + + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + height=data["height"], + frame=Frame.from_data(data["frame"]), + ) - @data.setter - def data(self, data): - self.radius = data["radius"] - self.height = data["height"] - self.frame = data["frame"] + # ============================================================================= + # Properties + # ============================================================================= @property def center(self): diff --git a/src/compas/geometry/surfaces/cylindrical.py b/src/compas/geometry/surfaces/cylindrical.py index 5f721aadcc1..c6bdb4f60b7 100644 --- a/src/compas/geometry/surfaces/cylindrical.py +++ b/src/compas/geometry/surfaces/cylindrical.py @@ -31,11 +31,11 @@ class CylindricalSurface(Surface): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "radius": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["radius", "frame"], } @@ -53,7 +53,11 @@ def __init__(self, radius, frame=None, **kwargs): self.radius = radius def __repr__(self): - return "CylindricalSurface(radius={0!r}, frame={1!r})".format(self.radius, self.frame) + return "{0}(radius={1}, frame={2!r})".format( + type(self).__name__, + self.radius, + self.frame, + ) def __eq__(self, other): try: @@ -63,14 +67,27 @@ def __eq__(self, other): return False return self.radius == other_radius and self.frame == other_frame + # ============================================================================= + # Data + # ============================================================================= + @property def data(self): - return {"radius": self.radius, "frame": self.frame} + return { + "radius": self.radius, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.radius = data["radius"] - self.frame = data["frame"] + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + frame=Frame.from_data(data["frame"]), + ) + + # ============================================================================= + # Properties + # ============================================================================= @property def center(self): diff --git a/src/compas/geometry/surfaces/planar.py b/src/compas/geometry/surfaces/planar.py index da357bdcae5..5ce511ab4d9 100644 --- a/src/compas/geometry/surfaces/planar.py +++ b/src/compas/geometry/surfaces/planar.py @@ -27,12 +27,12 @@ class PlanarSurface(Surface): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "xsize": {"type": "number", "minimum": 0}, "ysize": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["xsize", "ysize", "frame"], } @@ -52,7 +52,12 @@ def __init__(self, xsize=1.0, ysize=1.0, frame=None): self.ysize = ysize def __repr__(self): - return "PlanarSurface(xsize={0!r}, ysize={1!r}, frame={2!r})".format(self.xsize, self.ysize, self.frame) + return "{0}(xsize={1}, ysize={2}, frame={3!r})".format( + type(self).__name__, + self.xsize, + self.ysize, + self.frame, + ) def __eq__(self, other): try: @@ -63,15 +68,29 @@ def __eq__(self, other): return False return self.xsize == other_xsize and self.ysize == other_ysize and self.frame == other_frame + # ============================================================================= + # Data + # ============================================================================= + @property def data(self): - return {"xsize": self.xsize, "ysize": self.ysize, "frame": self.frame} + return { + "xsize": self.xsize, + "ysize": self.ysize, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.frame = data["frame"] - self.xsize = data["xsize"] - self.ysize = data["ysize"] + @classmethod + def from_data(cls, data): + return cls( + xsize=data["xsize"], + ysize=data["ysize"], + frame=Frame.from_data(data["frame"]), + ) + + # ============================================================================= + # Properties + # ============================================================================= @property def xsize(self): diff --git a/src/compas/geometry/surfaces/spherical.py b/src/compas/geometry/surfaces/spherical.py index 0187a0c32c4..1d072536593 100644 --- a/src/compas/geometry/surfaces/spherical.py +++ b/src/compas/geometry/surfaces/spherical.py @@ -32,11 +32,11 @@ class SphericalSurface(Surface): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "radius": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["radius", "frame"], } @@ -54,7 +54,11 @@ def __init__(self, radius, frame=None, **kwargs): self.radius = radius def __repr__(self): - return "SphericalSurface(radius={0!r}, frame={1!r})".format(self.radius, self.frame) + return "{0}(radius={1}, frame={2!r})".format( + type(self).__name__, + self.radius, + self.frame, + ) def __eq__(self, other): try: @@ -64,14 +68,27 @@ def __eq__(self, other): return False return self.radius == other_radius and self.frame == other_frame + # ============================================================================= + # Data + # ============================================================================= + @property def data(self): - return {"radius": self.radius, "frame": self.frame} + return { + "radius": self.radius, + "frame": self.frame.data, + } - @data.setter - def data(self, data): - self.radius = data["radius"] - self.frame = data["frame"] + @classmethod + def from_data(cls, data): + return cls( + radius=data["radius"], + frame=Frame.from_data(data["frame"]), + ) + + # ============================================================================= + # Properties + # ============================================================================= @property def center(self): diff --git a/src/compas/geometry/surfaces/surface.py b/src/compas/geometry/surfaces/surface.py index 6f6929ef697..35027d81c74 100644 --- a/src/compas/geometry/surfaces/surface.py +++ b/src/compas/geometry/surfaces/surface.py @@ -63,40 +63,13 @@ def __init__(self, frame=None, name=None): if frame: self.frame = frame - def __eq__(self, other): - raise NotImplementedError - - def __str__(self): - return "".format(self.u_domain, self.v_domain) - - # ============================================================================== - # Data - # ============================================================================== - - @property - def data(self): - raise NotImplementedError - - @data.setter - def data(self, data): - raise NotImplementedError - - @classmethod - def from_data(cls, data): - """Construct a surface from its data representation. - - Parameters - ---------- - data : dict - The data dictionary. - - Returns - ------- - :class:`~compas.geometry.Surface` - The constructed surface. - - """ - return cls(**data) + def __repr__(self): + return "{0}(frame={1!r}, u_domain={2}, v_domain={3})".format( + type(self).__name__, + self.frame, + self.u_domain, + self.v_domain, + ) # ============================================================================== # Properties diff --git a/src/compas/geometry/surfaces/toroidal.py b/src/compas/geometry/surfaces/toroidal.py index 13c5d7ee47c..e7187496a3c 100644 --- a/src/compas/geometry/surfaces/toroidal.py +++ b/src/compas/geometry/surfaces/toroidal.py @@ -30,12 +30,12 @@ class ToroidalSurface(Surface): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "radius_axis": {"type": "number", "minimum": 0}, "radius_pipe": {"type": "number", "minimum": 0}, - "frame": Frame.JSONSCHEMA, + "frame": Frame.DATASCHEMA, }, "required": ["radius", "frame"], } @@ -55,8 +55,11 @@ def __init__(self, radius_axis, radius_pipe, frame=None, **kwargs): self.radius_pipe = radius_pipe def __repr__(self): - return "ToroidalSurface(radius_axis={0!r}, radius_pipe={1!r}, frame={2!r})".format( - self.radius_axis, self.radius_pipe, self.frame + return "{0}(radius_axis={1}, radius_pipe={2}, frame={3!r})".format( + type(self).__name__, + self.radius_axis, + self.radius_pipe, + self.frame, ) def __eq__(self, other): @@ -72,15 +75,29 @@ def __eq__(self, other): and self.frame == other_frame ) + # ============================================================================= + # Data + # ============================================================================= + @property def data(self): - return {"radius_axis": self.radius_axis, "radius_pipe": self.radius_pipe, "frame": self.frame} + return { + "radius_axis": self.radius_axis, + "radius_pipe": self.radius_pipe, + "frame": self.frame.data, + } + + @classmethod + def from_data(cls, data): + return cls( + radius_axis=data["radius_axis"], + radius_pipe=data["radius_pipe"], + frame=Frame.from_data(data["frame"]), + ) - @data.setter - def data(self, data): - self.radius_axis = data["radius_axis"] - self.radius_pipe = data["radius_pipe"] - self.frame = data["frame"] + # ============================================================================= + # Properties + # ============================================================================= @property def center(self): diff --git a/src/compas/geometry/transformation.py b/src/compas/geometry/transformation.py index 2d214acd4be..ad8b5924efb 100644 --- a/src/compas/geometry/transformation.py +++ b/src/compas/geometry/transformation.py @@ -77,7 +77,7 @@ class Transformation(Data): """ - JSONSCHEMA = { + DATASCHEMA = { "type": "object", "properties": { "matrix": { @@ -101,17 +101,63 @@ def __init__(self, matrix=None): matrix = identity_matrix(4) self.matrix = matrix + def __mul__(self, other): + return self.concatenated(other) + + def __imul__(self, other): + return self.concatenated(other) + + def __getitem__(self, key): + i, j = key + return self.matrix[i][j] + + def __setitem__(self, key, value): + i, j = key + self.matrix[i][j] = value + + def __iter__(self): + return iter(self.matrix) + + def __eq__(self, other, tol=1e-05): + try: + A = self.matrix + B = other.matrix + for i in range(4): + for j in range(4): + if math.fabs(A[i][j] - B[i][j]) > tol: + return False + return True + except BaseException: + return False + + def __ne__(self, other): + # this is not obvious to ironpython + return not self.__eq__(other) + + def __repr__(self): + return "{0}({1!r}, check=False)".format(self.__class__.__name__, self.matrix) + + def __str__(self): + s = "[[%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[0]]) + s += " [%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[1]]) + s += " [%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[2]]) + s += " [%s]]\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[3]]) + return s + + def __len__(self): + return len(self.matrix) + # ========================================================================== - # descriptors + # Data # ========================================================================== @property def data(self): return {"matrix": self.matrix} - @data.setter - def data(self, data): - self.matrix = data["matrix"] + # ========================================================================== + # Properties + # ========================================================================== @property def scale(self): @@ -161,57 +207,7 @@ def determinant(self): return matrix_determinant(self.matrix) # ========================================================================== - # customisation - # ========================================================================== - - def __mul__(self, other): - return self.concatenated(other) - - def __imul__(self, other): - return self.concatenated(other) - - def __getitem__(self, key): - i, j = key - return self.matrix[i][j] - - def __setitem__(self, key, value): - i, j = key - self.matrix[i][j] = value - - def __iter__(self): - return iter(self.matrix) - - def __eq__(self, other, tol=1e-05): - try: - A = self.matrix - B = other.matrix - for i in range(4): - for j in range(4): - if math.fabs(A[i][j] - B[i][j]) > tol: - return False - return True - except BaseException: - return False - - def __ne__(self, other): - # this is not obvious to ironpython - return not self.__eq__(other) - - def __repr__(self): - return "Transformation({0!r})".format(self.matrix) - - def __str__(self): - s = "[[%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[0]]) - s += " [%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[1]]) - s += " [%s],\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[2]]) - s += " [%s]]\n" % ",".join([("%.4f" % n).rjust(10) for n in self.matrix[3]]) - return s - - def __len__(self): - return len(self.matrix) - - # ========================================================================== - # constructors + # Constructors # ========================================================================== @classmethod @@ -392,7 +388,7 @@ def from_change_of_basis(cls, frame_from, frame_to): return cls(multiply_matrices(matrix_inverse(T2.matrix), T1.matrix)) # ========================================================================== - # methods + # Methods # ========================================================================== def copy(self): diff --git a/src/compas/geometry/translation.py b/src/compas/geometry/translation.py index 017ba1c5630..cb52359950a 100644 --- a/src/compas/geometry/translation.py +++ b/src/compas/geometry/translation.py @@ -82,9 +82,6 @@ def translation_vector(self): z = self.matrix[2][3] return Vector(x, y, z) - def __repr__(self): - return "Translation({0!r}, check=False)".format(self.matrix) - @classmethod def from_vector(cls, vector): """Create a translation transformation from a translation vector. From be85f58e6730c61e0a3a8b18483e8943adad3279 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Sat, 12 Aug 2023 15:45:38 +0200 Subject: [PATCH 23/68] fix tests --- src/compas/data/data.py | 42 +++++++++- src/compas/data/encoders.py | 5 +- src/compas/data/json.py | 4 +- .../datastructures/assembly/assembly.py | 2 +- src/compas/datastructures/assembly/part.py | 2 +- .../datastructures/halfface/halfface.py | 15 +++- tests/compas/compas_api.json | 3 +- tests/compas/compas_api_ipy.json | 3 +- tests/compas/data/test_jsonschema.py | 84 +++++++++---------- tests/compas/geometry/test_point.py | 4 +- 10 files changed, 102 insertions(+), 62 deletions(-) diff --git a/src/compas/data/data.py b/src/compas/data/data.py index 94a42efb404..c8c63063025 100644 --- a/src/compas/data/data.py +++ b/src/compas/data/data.py @@ -111,8 +111,19 @@ def __init__(self, name=None): if name: self.name = name - def __jsondump__(self, minimal=True): - """Return the required information for serialization with the COMPAS JSON serializer.""" + def __jsondump__(self, minimal=False): + """Return the required information for serialization with the COMPAS JSON serializer. + + Parameters + ---------- + minimal : bool, optional + If True, exclude the GUID from the dump dict. + + Returns + ------- + dict + + """ state = { "dtype": self.dtype, "data": self.data, @@ -124,12 +135,35 @@ def __jsondump__(self, minimal=True): @classmethod def __jsonload__(cls, data, guid=None): - """Construct an object of this type from the provided data to support COMPAS JSON serialization.""" + """Construct an object of this type from the provided data to support COMPAS JSON serialization. + + Parameters + ---------- + data : dict + The raw Python data representing the object. + guid : str, optional + The GUID of the object. + + Returns + ------- + object + + """ obj = cls.from_data(data) - if guid: + if guid is not None: obj._guid = UUID(guid) return obj + def __getstate__(self): + state = self.__jsondump__() + state["__dict__"] = self.__dict__ + return state + + def __setstate__(self, state): + self.__dict__.update(state["__dict__"]) + if "guid" in state: + self._guid = UUID(state["guid"]) + @property def dtype(self): return "{}/{}".format(".".join(self.__class__.__module__.split(".")[:2]), self.__class__.__name__) diff --git a/src/compas/data/encoders.py b/src/compas/data/encoders.py index 5d64b7b0d9b..e36f4ffb112 100644 --- a/src/compas/data/encoders.py +++ b/src/compas/data/encoders.py @@ -95,7 +95,7 @@ class DataEncoder(json.JSONEncoder): """ - minimal = True + minimal = False def default(self, o): """Return an object in serialized form. @@ -226,11 +226,12 @@ def object_hook(self, o): raise DecoderError("The data type can't be found in the specified module: {}.".format(o["dtype"])) data = o["data"] + guid = o.get("guid") # Kick-off from_data from a rebuilt Python dictionary instead of the C# data type if IDictionary and isinstance(o, IDictionary[str, object]): data = {key: data[key] for key in data.Keys} - obj = cls.__jsonload__(data, o.get("guid", None)) + obj = cls.__jsonload__(data, guid) return obj diff --git a/src/compas/data/json.py b/src/compas/data/json.py index f74f499a33a..f2715f124c0 100644 --- a/src/compas/data/json.py +++ b/src/compas/data/json.py @@ -8,7 +8,7 @@ from compas.data import DataDecoder -def json_dump(data, fp, pretty=False, compact=False, minimal=True): +def json_dump(data, fp, pretty=False, compact=False, minimal=False): """Write a collection of COMPAS object data to a JSON file. Parameters @@ -59,7 +59,7 @@ def json_dump(data, fp, pretty=False, compact=False, minimal=True): return json.dump(data, f, cls=DataEncoder, **kwargs) -def json_dumps(data, pretty=False, compact=False, minimal=True): +def json_dumps(data, pretty=False, compact=False, minimal=False): """Write a collection of COMPAS objects to a JSON string. Parameters diff --git a/src/compas/datastructures/assembly/assembly.py b/src/compas/datastructures/assembly/assembly.py index ea4aa73292c..4a042a1f63a 100644 --- a/src/compas/datastructures/assembly/assembly.py +++ b/src/compas/datastructures/assembly/assembly.py @@ -66,7 +66,7 @@ def from_data(cls, data): assembly = cls() assembly.attributes.update(data["attributes"] or {}) assembly.graph = Graph.from_data(data["graph"]) - assembly._parts = {part.guid: part.key for part in assembly.parts()} + assembly._parts = {part.guid: part.key for part in assembly.parts()} # type: ignore return assembly # ========================================================================== diff --git a/src/compas/datastructures/assembly/part.py b/src/compas/datastructures/assembly/part.py index 946b776f94f..417fbc1684b 100644 --- a/src/compas/datastructures/assembly/part.py +++ b/src/compas/datastructures/assembly/part.py @@ -176,7 +176,7 @@ def data(self): return { "attributes": self.attributes, "key": self.key, - "frame": self.frame, + "frame": self.frame.data, } @classmethod diff --git a/src/compas/datastructures/halfface/halfface.py b/src/compas/datastructures/halfface/halfface.py index 5ff00893717..15a6f52839e 100644 --- a/src/compas/datastructures/halfface/halfface.py +++ b/src/compas/datastructures/halfface/halfface.py @@ -160,11 +160,18 @@ def __str__(self): @property def data(self): _cell = {} - for c in self.cells(): + # this sometimes changes the cycle order of faces + # for c in self.cells(): + # faces = [] + # for face in self.cell_faces(c): + # vertices = self.halfface_vertices(face) + # faces.append(vertices) + # _cell[c] = faces + for c in self._cell: faces = [] - for face in self.cell_faces(c): - vertices = self.halfface_vertices(face) - faces.append(vertices) + for u in sorted(self._cell[c]): + for v in sorted(self._cell[c][u]): + faces.append(self._halfface[self._cell[c][u][v]]) _cell[c] = faces return { diff --git a/tests/compas/compas_api.json b/tests/compas/compas_api.json index f7d73179f0e..b45f2f2bdbd 100644 --- a/tests/compas/compas_api.json +++ b/tests/compas/compas_api.json @@ -44,8 +44,7 @@ "json_dump", "json_dumps", "json_load", - "json_loads", - "validate_data" + "json_loads" ], "compas.datastructures": [ "Assembly", diff --git a/tests/compas/compas_api_ipy.json b/tests/compas/compas_api_ipy.json index a1c0833e554..6a9e331710e 100644 --- a/tests/compas/compas_api_ipy.json +++ b/tests/compas/compas_api_ipy.json @@ -44,8 +44,7 @@ "json_dump", "json_dumps", "json_load", - "json_loads", - "validate_data" + "json_loads" ], "compas.datastructures": [ "Assembly", diff --git a/tests/compas/data/test_jsonschema.py b/tests/compas/data/test_jsonschema.py index 4c123933d0e..5f1fce18d64 100644 --- a/tests/compas/data/test_jsonschema.py +++ b/tests/compas/data/test_jsonschema.py @@ -35,7 +35,7 @@ ], ) def test_schema_point_valid(point): - Point.validate_jsondata(point) + Point.validate_data(point) @pytest.mark.parametrize( "point", @@ -47,7 +47,7 @@ def test_schema_point_valid(point): ) def test_schema_point_invalid(point): with pytest.raises(jsonschema.exceptions.ValidationError): - Point.validate_jsondata(point) + Point.validate_data(point) @pytest.mark.parametrize( "vector", @@ -58,7 +58,7 @@ def test_schema_point_invalid(point): ], ) def test_schema_vector_valid(vector): - Vector.validate_jsondata(vector) + Vector.validate_data(vector) @pytest.mark.parametrize( "vector", @@ -70,7 +70,7 @@ def test_schema_vector_valid(vector): ) def test_schema_vector_invalid(vector): with pytest.raises(jsonschema.exceptions.ValidationError): - Vector.validate_jsondata(vector) + Vector.validate_data(vector) @pytest.mark.parametrize( "line", @@ -80,7 +80,7 @@ def test_schema_vector_invalid(vector): ], ) def test_schema_line_valid(line): - Line.validate_jsondata(line) + Line.validate_data(line) @pytest.mark.parametrize( "line", @@ -91,7 +91,7 @@ def test_schema_line_valid(line): ) def test_schema_line_invalid(line): with pytest.raises(jsonschema.exceptions.ValidationError): - Line.validate_jsondata(line) + Line.validate_data(line) @pytest.mark.parametrize( "plane", @@ -100,7 +100,7 @@ def test_schema_line_invalid(line): ], ) def test_schema_plane_valid(plane): - Plane.validate_jsondata(plane) + Plane.validate_data(plane) @pytest.mark.parametrize( "plane", @@ -111,7 +111,7 @@ def test_schema_plane_valid(plane): ) def test_schema_plane_invalid(plane): with pytest.raises(jsonschema.exceptions.ValidationError): - Plane.validate_jsondata(plane) + Plane.validate_data(plane) @pytest.mark.parametrize( "circle", @@ -121,7 +121,7 @@ def test_schema_plane_invalid(plane): ], ) def test_schema_circle_valid(circle): - Circle.validate_jsondata(circle) + Circle.validate_data(circle) @pytest.mark.parametrize( "circle", @@ -139,7 +139,7 @@ def test_schema_circle_valid(circle): ) def test_schema_circle_invalid(circle): with pytest.raises(jsonschema.exceptions.ValidationError): - Circle.validate_jsondata(circle) + Circle.validate_data(circle) @pytest.mark.parametrize( "ellipse", @@ -167,7 +167,7 @@ def test_schema_circle_invalid(circle): ], ) def test_schema_ellipse_valid(ellipse): - Ellipse.validate_jsondata(ellipse) + Ellipse.validate_data(ellipse) @pytest.mark.parametrize( "ellipse", @@ -211,7 +211,7 @@ def test_schema_ellipse_valid(ellipse): ) def test_schema_ellipse_invalid(ellipse): with pytest.raises(jsonschema.exceptions.ValidationError): - Ellipse.validate_jsondata(ellipse) + Ellipse.validate_data(ellipse) @pytest.mark.parametrize( "frame", @@ -233,7 +233,7 @@ def test_schema_ellipse_invalid(ellipse): ], ) def test_schema_frame_valid(frame): - Frame.validate_jsondata(frame) + Frame.validate_data(frame) @pytest.mark.parametrize( "frame", @@ -244,7 +244,7 @@ def test_schema_frame_valid(frame): ) def test_schema_frame_invalid(frame): with pytest.raises(jsonschema.exceptions.ValidationError): - Frame.validate_jsondata(frame) + Frame.validate_data(frame) @pytest.mark.parametrize( "quaternion", @@ -255,7 +255,7 @@ def test_schema_frame_invalid(frame): ], ) def test_schema_quaternion_valid(quaternion): - Quaternion.validate_jsondata(quaternion) + Quaternion.validate_data(quaternion) @pytest.mark.parametrize( "quaternion", @@ -268,7 +268,7 @@ def test_schema_quaternion_valid(quaternion): ) def test_schema_quaternion_invalid(quaternion): with pytest.raises(jsonschema.exceptions.ValidationError): - Quaternion.validate_jsondata(quaternion) + Quaternion.validate_data(quaternion) @pytest.mark.parametrize( "polygon", @@ -278,7 +278,7 @@ def test_schema_quaternion_invalid(quaternion): ], ) def test_schema_polygon_valid(polygon): - Polygon.validate_jsondata(polygon) + Polygon.validate_data(polygon) @pytest.mark.parametrize( "polygon", @@ -290,7 +290,7 @@ def test_schema_polygon_valid(polygon): ) def test_schema_polygon_invalid(polygon): with pytest.raises(jsonschema.exceptions.ValidationError): - Polygon.validate_jsondata(polygon) + Polygon.validate_data(polygon) @pytest.mark.parametrize( "polyline", @@ -300,7 +300,7 @@ def test_schema_polygon_invalid(polygon): ], ) def test_schema_polyline_valid(polyline): - Polyline.validate_jsondata(polyline) + Polyline.validate_data(polyline) @pytest.mark.parametrize( "polyline", @@ -312,7 +312,7 @@ def test_schema_polyline_valid(polyline): ) def test_schema_polyline_invalid(polyline): with pytest.raises(jsonschema.exceptions.ValidationError): - Polyline.validate_jsondata(polyline) + Polyline.validate_data(polyline) @pytest.mark.parametrize( "box", @@ -326,7 +326,7 @@ def test_schema_polyline_invalid(polyline): ], ) def test_schema_box_valid(box): - Box.validate_jsondata(box) + Box.validate_data(box) @pytest.mark.parametrize( "box", @@ -359,7 +359,7 @@ def test_schema_box_valid(box): ) def test_schema_box_invalid(box): with pytest.raises(jsonschema.exceptions.ValidationError): - Box.validate_jsondata(box) + Box.validate_data(box) @pytest.mark.parametrize( "capsule", @@ -371,7 +371,7 @@ def test_schema_box_invalid(box): ], ) def test_schema_capsule_valid(capsule): - Capsule.validate_jsondata(capsule) + Capsule.validate_data(capsule) @pytest.mark.parametrize( "capsule", @@ -386,7 +386,7 @@ def test_schema_capsule_valid(capsule): ) def test_schema_capsule_invalid(capsule): with pytest.raises(jsonschema.exceptions.ValidationError): - Capsule.validate_jsondata(capsule) + Capsule.validate_data(capsule) @pytest.mark.parametrize( "cone", @@ -398,7 +398,7 @@ def test_schema_capsule_invalid(capsule): ], ) def test_schema_cone_valid(cone): - Cone.validate_jsondata(cone) + Cone.validate_data(cone) @pytest.mark.parametrize( "cone", @@ -413,7 +413,7 @@ def test_schema_cone_valid(cone): ) def test_schema_cone_invalid(cone): with pytest.raises(jsonschema.exceptions.ValidationError): - Cone.validate_jsondata(cone) + Cone.validate_data(cone) @pytest.mark.parametrize( "cylinder", @@ -425,7 +425,7 @@ def test_schema_cone_invalid(cone): ], ) def test_schema_cylinder_valid(cylinder): - Cylinder.validate_jsondata(cylinder) + Cylinder.validate_data(cylinder) @pytest.mark.parametrize( "cylinder", @@ -440,7 +440,7 @@ def test_schema_cylinder_valid(cylinder): ) def test_schema_cylinder_invalid(cylinder): with pytest.raises(jsonschema.exceptions.ValidationError): - Cylinder.validate_jsondata(cylinder) + Cylinder.validate_data(cylinder) @pytest.mark.parametrize( "polyhedron", @@ -452,7 +452,7 @@ def test_schema_cylinder_invalid(cylinder): ], ) def test_schema_polyhedron_valid(polyhedron): - Polyhedron.validate_jsondata(polyhedron) + Polyhedron.validate_data(polyhedron) @pytest.mark.parametrize( "polyhedron", @@ -472,7 +472,7 @@ def test_schema_polyhedron_valid(polyhedron): ) def test_schema_polyhedron_invalid(polyhedron): with pytest.raises(jsonschema.exceptions.ValidationError): - Polyhedron.validate_jsondata(polyhedron) + Polyhedron.validate_data(polyhedron) @pytest.mark.parametrize( "sphere", @@ -482,7 +482,7 @@ def test_schema_polyhedron_invalid(polyhedron): ], ) def test_schema_sphere_valid(sphere): - Sphere.validate_jsondata(sphere) + Sphere.validate_data(sphere) @pytest.mark.parametrize( "sphere", @@ -494,7 +494,7 @@ def test_schema_sphere_valid(sphere): ) def test_schema_sphere_invalid(sphere): with pytest.raises(jsonschema.exceptions.ValidationError): - Sphere.validate_jsondata(sphere) + Sphere.validate_data(sphere) @pytest.mark.parametrize( "torus", @@ -522,7 +522,7 @@ def test_schema_sphere_invalid(sphere): ], ) def test_schema_torus_valid(torus): - Torus.validate_jsondata(torus) + Torus.validate_data(torus) @pytest.mark.parametrize( "torus", @@ -566,7 +566,7 @@ def test_schema_torus_valid(torus): ) def test_schema_torus_invalid(torus): with pytest.raises(jsonschema.exceptions.ValidationError): - Torus.validate_jsondata(torus) + Torus.validate_data(torus) @pytest.mark.parametrize( "pointcloud", @@ -577,7 +577,7 @@ def test_schema_torus_invalid(torus): ], ) def test_schema_pointcloud_valid(pointcloud): - Pointcloud.validate_jsondata(pointcloud) + Pointcloud.validate_data(pointcloud) @pytest.mark.parametrize( "pointcloud", @@ -590,7 +590,7 @@ def test_schema_pointcloud_valid(pointcloud): ) def test_schema_pointcloud_invalid(pointcloud): with pytest.raises(jsonschema.exceptions.ValidationError): - Pointcloud.validate_jsondata(pointcloud) + Pointcloud.validate_data(pointcloud) @pytest.mark.parametrize( "graph", @@ -625,7 +625,7 @@ def test_schema_pointcloud_invalid(pointcloud): ], ) def test_schema_graph_valid(graph): - Graph.validate_jsondata(graph) + Graph.validate_data(graph) @pytest.mark.parametrize( "graph", @@ -699,7 +699,7 @@ def test_schema_graph_valid(graph): ) def test_schema_graph_invalid(graph): with pytest.raises(jsonschema.exceptions.ValidationError): - Graph.validate_jsondata(graph) + Graph.validate_data(graph) @pytest.mark.parametrize( "halfedge", @@ -784,14 +784,14 @@ def test_schema_graph_invalid(graph): "vertex": {"0": {}, "1": {}, "2": {}}, "face": {"0": [0, 1, 2]}, "facedata": {"0": {}}, - "edgedata": {"(0,1)": {}}, + "edgedata": {"(0, 1)": {}}, "max_vertex": -1, "max_face": -1, }, ], ) def test_schema_halfedge_valid(halfedge): - HalfEdge.validate_jsondata(halfedge) + HalfEdge.validate_data(halfedge) @pytest.mark.parametrize( "halfedge", @@ -848,7 +848,7 @@ def test_schema_halfedge_valid(halfedge): ) def test_schema_halfedge_invalid(halfedge): with pytest.raises(jsonschema.exceptions.ValidationError): - HalfEdge.validate_jsondata(halfedge) + HalfEdge.validate_data(halfedge) @pytest.mark.parametrize( "halfedge", @@ -881,4 +881,4 @@ def test_schema_halfedge_invalid(halfedge): ) def test_schema_halfedge_failing(halfedge): with pytest.raises(TypeError): - HalfEdge.validate_jsondata(halfedge) + HalfEdge.validate_data(halfedge) diff --git a/tests/compas/geometry/test_point.py b/tests/compas/geometry/test_point.py index a487d855089..4445bdcc209 100644 --- a/tests/compas/geometry/test_point.py +++ b/tests/compas/geometry/test_point.py @@ -2,11 +2,11 @@ def test_point(): - p = Point(1, 0, "0") + p = Point(1, 0, "0") # type: ignore assert p.x == 1.0 and p.y == 0.0 and p.z == 0.0 assert p[0] == 1.0 and p[1] == 0.0 and p[2] == 0.0 assert p == [1.0, 0.0, 0.0] - assert repr(p) == "Point(1.000, 0.000, 0.000)" + assert repr(p) == "Point(x=1.000, y=0.000, z=0.000)" def test_point_operators(): From f19cc8134dc356ad78ab4283bc78abe650e7333b Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 10:38:20 +0200 Subject: [PATCH 24/68] rename to compas_dataclasses --- src/compas/data/__init__.py | 7 ++++++- src/compas/data/schema.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/compas/data/__init__.py b/src/compas/data/__init__.py index 21fc17cb674..a3a884ac2c6 100644 --- a/src/compas/data/__init__.py +++ b/src/compas/data/__init__.py @@ -11,8 +11,9 @@ from .encoders import DataEncoder from .encoders import DataDecoder from .data import Data - from .json import json_load, json_loads, json_dump, json_dumps +from .schema import dataclass_dataschema, dataclass_typeschema, dataclass_jsonschema +from .schema import compas_dataclasses __all__ = [ "Data", @@ -30,4 +31,8 @@ "json_loads", "json_dump", "json_dumps", + "dataclass_dataschema", + "dataclass_typeschema", + "dataclass_jsonschema", + "compas_dataclasses", ] diff --git a/src/compas/data/schema.py b/src/compas/data/schema.py index 3223bda3d17..808c553a8f0 100644 --- a/src/compas/data/schema.py +++ b/src/compas/data/schema.py @@ -100,7 +100,7 @@ def compas_jsonschema(dirname=None): """ schemas = [] - dataclasses = compas_datamodel() + dataclasses = compas_dataclasses() for cls in dataclasses: filepath = None if dirname: @@ -110,7 +110,7 @@ def compas_jsonschema(dirname=None): return schemas -def compas_datamodel(): +def compas_dataclasses(): """Find all classes in the COMPAS data model. Returns From 776cec1a48be057663503bcd8b1154db3365d45f Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 10:38:29 +0200 Subject: [PATCH 25/68] stub for tests --- tests/compas/data/test_schema.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/compas/data/test_schema.py diff --git a/tests/compas/data/test_schema.py b/tests/compas/data/test_schema.py new file mode 100644 index 00000000000..e69de29bb2d From 64bfa45132fce4857b697e17c758e3b15dcdf5ee Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 10:38:46 +0200 Subject: [PATCH 26/68] formatting --- src/compas/data/data.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/compas/data/data.py b/src/compas/data/data.py index c8c63063025..03bc409aa93 100644 --- a/src/compas/data/data.py +++ b/src/compas/data/data.py @@ -181,7 +181,9 @@ def ToString(self): printing self.GetType().FullName or similar. Overriding the `ToString` method of .NET object class fixes that and makes Rhino/Grasshopper display proper string representations when the objects are printed or - connected to a panel or other type of string output.""" + connected to a panel or other type of string output. + + """ return str(self) @property From 4547500dec1570f9e2a38dd78a4ed98b29938e89 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 10:38:58 +0200 Subject: [PATCH 27/68] remove precision from repr --- CHANGELOG.md | 3 ++- src/compas/geometry/point.py | 5 +---- src/compas/geometry/vector.py | 5 +---- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db9c85ca63c..3252c61d9b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `compas.data.schema.dataclass_typeschema`. * Added `compas.data.schema.dataclass_jsonschema`. * Added `compas.data.schema.compas_jsonschema`. -* Added `compas.data.schema.compas_datamodel`. +* Added `compas.data.schema.compas_dataclasses`. * Added `compas.datastructures.Graph.to_jsondata`. * Added `compas.datastructures.Graph.from_jsondata`. * Added `compas.datastructures.Halfedge.halfedge_loop_vertices`. @@ -169,6 +169,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Changed data property of `compas.datastructures.Graph` to contain only JSON compatible data. * Changed data property of `compas.datastructures.Halfedge` to contain only JSON compatible data. * Changed data property of `compas.datastructures.Halfface` to contain only JSON compatible data. +* Changed `__repr__` of `compas.geometry.Point` and `compas.geometry.Vector` to not use limited precision (`compas.PRECISION`) to ensure proper object reconstruction through `eval(repr(point))`. ### Removed diff --git a/src/compas/geometry/point.py b/src/compas/geometry/point.py index c5ed1676db5..324c18d0afc 100644 --- a/src/compas/geometry/point.py +++ b/src/compas/geometry/point.py @@ -2,8 +2,6 @@ from __future__ import absolute_import from __future__ import division -from compas import PRECISION - from compas.geometry import centroid_points from compas.geometry import normal_polygon from compas.geometry import distance_point_point @@ -118,12 +116,11 @@ def __init__(self, x, y, z=0.0, **kwargs): self.z = z def __repr__(self): - return "{0}(x={1:.{4}f}, y={2:.{4}f}, z={3:.{4}f})".format( + return "{0}({1}, {2}, z={3})".format( type(self).__name__, self.x, self.y, self.z, - PRECISION[:1], ) def __len__(self): diff --git a/src/compas/geometry/vector.py b/src/compas/geometry/vector.py index 534e2cad169..0e43d8beb79 100644 --- a/src/compas/geometry/vector.py +++ b/src/compas/geometry/vector.py @@ -2,8 +2,6 @@ from __future__ import absolute_import from __future__ import division -from compas import PRECISION - from compas.geometry import length_vector from compas.geometry import cross_vectors from compas.geometry import subtract_vectors @@ -82,12 +80,11 @@ def __init__(self, x, y, z=0.0, **kwargs): self.z = z def __repr__(self): - return "{0}(x={1:.{4}f}, y={2:.{4}f}, z={3:.{4}f})".format( + return "{0}(x={1}, y={2}, z={3})".format( type(self).__name__, self.x, self.y, self.z, - PRECISION[:1], ) def __len__(self): From 79306961ce8914e2e827abea692afc8603417f26 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 10:39:07 +0200 Subject: [PATCH 28/68] add data tests --- tests/compas/geometry/test_point.py | 58 +++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/tests/compas/geometry/test_point.py b/tests/compas/geometry/test_point.py index 4445bdcc209..3dd1717e85f 100644 --- a/tests/compas/geometry/test_point.py +++ b/tests/compas/geometry/test_point.py @@ -1,16 +1,50 @@ +import pytest +from random import random from compas.geometry import Point -def test_point(): - p = Point(1, 0, "0") # type: ignore - assert p.x == 1.0 and p.y == 0.0 and p.z == 0.0 - assert p[0] == 1.0 and p[1] == 0.0 and p[2] == 0.0 - assert p == [1.0, 0.0, 0.0] - assert repr(p) == "Point(x=1.000, y=0.000, z=0.000)" +@pytest.mark.parametrize( + "x,y,z", + [ + (1, 2, 3), + (1.0, 2.0, 3.0), + ("1.0", "2", 3.0), + (random(), random(), random()), + ], +) +def test_point(x, y, z): + p = Point(x, y, z) + x, y, z = float(x), float(y), float(z) + assert p.x == x and p.y == y and p.z == z + assert p[0] == x and p[1] == y and p[2] == z + assert eval(repr(p)) == p + + +@pytest.mark.parametrize( + "x,y", + [ + (1, 2), + (1.0, 2.0), + ("1.0", "2"), + (random(), random()), + ], +) +def test_point2(x, y): + p = Point(x, y) + x, y, z = float(x), float(y), 0.0 + assert p.x == x and p.y == y and p.z == z + assert p[0] == x and p[1] == y and p[2] == z + assert eval(repr(p)) == p def test_point_operators(): - pass + a = Point(random(), random(), random()) + b = Point(random(), random(), random()) + assert a + b == [a.x + b.x, a.y + b.y, a.z + b.z] + assert a - b == [a.x - b.x, a.y - b.y, a.z - b.z] + assert a * 2 == [a.x * 2, a.y * 2, a.z * 2] + assert a / 2 == [a.x / 2, a.y / 2, a.z / 2] + assert a**3 == [a.x**3, a.y**3, a.z**3] def test_point_equality(): @@ -27,6 +61,16 @@ def test_point_inplace_operators(): pass +def test_point_data(): + point = Point(random(), random(), random()) + other = Point.from_data(point.data) + assert point == other + assert point.data == other.data + assert point.guid != other.guid + assert Point.validate_data(point.data) + assert Point.validate_data(other.data) + + def test_point_distance_to_point(): pass From 3087e0b6e7a1e074bbf3b613d98fde1c59e8137e Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 12:11:54 +0200 Subject: [PATCH 29/68] ipy repr is fucked --- tests/compas/geometry/test_point.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/compas/geometry/test_point.py b/tests/compas/geometry/test_point.py index 3dd1717e85f..bf6e63435d2 100644 --- a/tests/compas/geometry/test_point.py +++ b/tests/compas/geometry/test_point.py @@ -1,4 +1,6 @@ +from __future__ import division import pytest +import compas from random import random from compas.geometry import Point @@ -17,7 +19,9 @@ def test_point(x, y, z): x, y, z = float(x), float(y), float(z) assert p.x == x and p.y == y and p.z == z assert p[0] == x and p[1] == y and p[2] == z - assert eval(repr(p)) == p + + if not compas.IPY: + assert eval(repr(p)) == p @pytest.mark.parametrize( @@ -34,7 +38,9 @@ def test_point2(x, y): x, y, z = float(x), float(y), 0.0 assert p.x == x and p.y == y and p.z == z assert p[0] == x and p[1] == y and p[2] == z - assert eval(repr(p)) == p + + if not compas.IPY: + assert eval(repr(p)) == p def test_point_operators(): @@ -67,8 +73,10 @@ def test_point_data(): assert point == other assert point.data == other.data assert point.guid != other.guid - assert Point.validate_data(point.data) - assert Point.validate_data(other.data) + + if not compas.IPY: + assert Point.validate_data(point.data) + assert Point.validate_data(other.data) def test_point_distance_to_point(): From d75a823361d4e7db678c2fda32d34febc81870be Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 12:32:36 +0200 Subject: [PATCH 30/68] data tests --- tests/compas/geometry/test_point.py | 4 +- tests/compas/geometry/test_vector.py | 80 ++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/tests/compas/geometry/test_point.py b/tests/compas/geometry/test_point.py index bf6e63435d2..4d21e61c21e 100644 --- a/tests/compas/geometry/test_point.py +++ b/tests/compas/geometry/test_point.py @@ -1,5 +1,6 @@ from __future__ import division import pytest +import json import compas from random import random from compas.geometry import Point @@ -69,7 +70,8 @@ def test_point_inplace_operators(): def test_point_data(): point = Point(random(), random(), random()) - other = Point.from_data(point.data) + other = Point.from_data(json.loads(json.dumps(point.data))) + assert point == other assert point.data == other.data assert point.guid != other.guid diff --git a/tests/compas/geometry/test_vector.py b/tests/compas/geometry/test_vector.py index e617a7500db..e86bf2a8afe 100644 --- a/tests/compas/geometry/test_vector.py +++ b/tests/compas/geometry/test_vector.py @@ -1,6 +1,86 @@ +from __future__ import division +import pytest +import json +import compas +from random import random from compas.geometry import Vector +@pytest.mark.parametrize( + "x,y,z", + [ + (1, 2, 3), + (1.0, 2.0, 3.0), + ("1.0", "2", 3.0), + (random(), random(), random()), + ], +) +def test_vector(x, y, z): + v = Vector(x, y, z) + x, y, z = float(x), float(y), float(z) + assert v.x == x and v.y == y and v.z == z + assert v[0] == x and v[1] == y and v[2] == z + + if not compas.IPY: + assert eval(repr(v)) == v + + +@pytest.mark.parametrize( + "x,y", + [ + (1, 2), + (1.0, 2.0), + ("1.0", "2"), + (random(), random()), + ], +) +def test_vector2(x, y): + v = Vector(x, y) + x, y, z = float(x), float(y), 0.0 + assert v.x == x and v.y == y and v.z == z + assert v[0] == x and v[1] == y and v[2] == z + + if not compas.IPY: + assert eval(repr(v)) == v + + +def test_vector_operators(): + a = Vector(random(), random(), random()) + b = Vector(random(), random(), random()) + assert a + b == [a.x + b.x, a.y + b.y, a.z + b.z] + assert a - b == [a.x - b.x, a.y - b.y, a.z - b.z] + assert a * 2 == [a.x * 2, a.y * 2, a.z * 2] + assert a / 2 == [a.x / 2, a.y / 2, a.z / 2] + assert a**3 == [a.x**3, a.y**3, a.z**3] + + +def test_vector_equality(): + p1 = Vector(1, 1, 1) + p2 = Vector(1, 1, 1) + p3 = Vector(0, 0, 0) + assert p1 == p2 + assert not (p1 != p2) + assert p1 != p3 + assert not (p1 == p3) + + +def test_vector_inplace_operators(): + pass + + +def test_vector_data(): + vector = Vector(random(), random(), random()) + other = Vector.from_data(json.loads(json.dumps(vector.data))) + + assert vector == other + assert vector.data == other.data + assert vector.guid != other.guid + + if not compas.IPY: + assert Vector.validate_data(vector.data) + assert Vector.validate_data(other.data) + + def test_cross_vectors(): vec_list1 = [[1, 2, 3], [7, 8, 9]] vec_list2 = [[2, 3, 4], [5, 6, 7]] From b6571ef54452fafb701a765ab3a52faec45ad4eb Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 12:59:45 +0200 Subject: [PATCH 31/68] plane data tests --- tests/compas/geometry/test_plane.py | 51 +++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/tests/compas/geometry/test_plane.py b/tests/compas/geometry/test_plane.py index ec84e9bbf08..b0488df2e5d 100644 --- a/tests/compas/geometry/test_plane.py +++ b/tests/compas/geometry/test_plane.py @@ -1,7 +1,54 @@ +import pytest +import json +import compas +from random import random +from compas.geometry import close +from compas.geometry import allclose +from compas.geometry import Point +from compas.geometry import Vector from compas.geometry import Plane -def test_from_point_and_two_vectors(): +@pytest.mark.parametrize( + "point,vector", + [ + ([1, 2, 3], [0, 0, 1]), + (Point(1.0, 2.0, 3.0), [0.0, 0.0, 1.0]), + (Point(1.0, 2.0, 3.0), Vector(0.0, 0.0, 1.0)), + ([1.0, 2.0, 3.0], Vector(0.0, 0.0, 1.0)), + ([random(), random(), random()], [random(), random(), random()]), + ], +) +def test_plane(point, vector): + plane = Plane(point, vector) + assert plane.point == Point(*point) + assert plane.normal == Vector(*vector).unitized() + assert isinstance(plane.point, Point) + assert isinstance(plane.normal, Vector) + assert close(plane.normal.length, 1.0, tol=1e-12) + + if not compas.IPY: + other = eval(repr(plane)) + assert allclose(other.point, plane.point, tol=1e-12) + assert allclose(other.normal, plane.normal, tol=1e-12) + + +def test_plane_data(): + point = Point(random(), random(), random()) + vector = Vector(random(), random(), random()) + plane = Plane(point, vector) + other = Plane.from_data(json.loads(json.dumps(plane.data))) + + assert plane == other + assert plane.data == other.data + assert plane.guid != other.guid + + if not compas.IPY: + assert Plane.validate_data(plane.data) + assert Plane.validate_data(other.data) + + +def test_plane_from_point_and_two_vectors(): pt = [1, 2, 3] vec1 = [1, 0, 0] vec2 = [0, 1, 0] @@ -10,7 +57,7 @@ def test_from_point_and_two_vectors(): assert result == [[1, 2, 3], [0, 0, 1]] -def test_from_three_points(): +def test_plane_from_three_points(): pt1 = [0, 0, 0] pt2 = [1, 0, 0] pt3 = [0, 1, 0] From 00bd027f8aa1934a5b55b33cc6d9b5da0963d8da Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 13:39:30 +0200 Subject: [PATCH 32/68] test closeness not equality --- tests/compas/geometry/test_plane.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/tests/compas/geometry/test_plane.py b/tests/compas/geometry/test_plane.py index b0488df2e5d..fa86f57cdc2 100644 --- a/tests/compas/geometry/test_plane.py +++ b/tests/compas/geometry/test_plane.py @@ -27,10 +27,9 @@ def test_plane(point, vector): assert isinstance(plane.normal, Vector) assert close(plane.normal.length, 1.0, tol=1e-12) - if not compas.IPY: - other = eval(repr(plane)) - assert allclose(other.point, plane.point, tol=1e-12) - assert allclose(other.normal, plane.normal, tol=1e-12) + other = eval(repr(plane)) + assert allclose(other.point, plane.point, tol=1e-12) + assert allclose(other.normal, plane.normal, tol=1e-12) def test_plane_data(): @@ -39,7 +38,8 @@ def test_plane_data(): plane = Plane(point, vector) other = Plane.from_data(json.loads(json.dumps(plane.data))) - assert plane == other + assert allclose(other.point, plane.point, tol=1e-12) + assert allclose(other.normal, plane.normal, tol=1e-12) assert plane.data == other.data assert plane.guid != other.guid @@ -48,6 +48,20 @@ def test_plane_data(): assert Plane.validate_data(other.data) +def test_plane_predefined(): + plane = Plane.worldXY() + assert plane.point == Point(0, 0, 0) + assert plane.normal == Vector(0, 0, 1) + + plane = Plane.worldYZ() + assert plane.point == Point(0, 0, 0) + assert plane.normal == Vector(1, 0, 0) + + plane = Plane.worldZX() + assert plane.point == Point(0, 0, 0) + assert plane.normal == Vector(0, 1, 0) + + def test_plane_from_point_and_two_vectors(): pt = [1, 2, 3] vec1 = [1, 0, 0] From 1a04f3bc2dbdb2f8aa472a4e774be46af40cc5f5 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 13:39:41 +0200 Subject: [PATCH 33/68] add other planes --- CHANGELOG.md | 1 + src/compas/geometry/plane.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3252c61d9b5..673fc14b360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `vertices_and_faces_to_rhino` to `compas_rhino.conversions`. * Added `polyhedron_to_rhino` to `compas_rhino.conversions`. * Added `from_mesh` plugin to `compas_rhino.geometry.RhinoBrep`. +* Added `compas.geometry.Plane.worldYZ` and `compas.geometry.Plane.worldZX`. ### Changed diff --git a/src/compas/geometry/plane.py b/src/compas/geometry/plane.py index 6cc1534208a..98ca10d6563 100644 --- a/src/compas/geometry/plane.py +++ b/src/compas/geometry/plane.py @@ -239,6 +239,30 @@ def worldXY(cls): """ return cls([0, 0, 0], [0, 0, 1]) + @classmethod + def worldYZ(cls): + """Construct the world YZ plane. + + Returns + ------- + :class:`~compas.geometry.Plane` + The world YZ plane. + + """ + return cls([0, 0, 0], [1, 0, 0]) + + @classmethod + def worldZX(cls): + """Construct the world ZX plane. + + Returns + ------- + :class:`~compas.geometry.Plane` + The world ZX plane. + + """ + return cls([0, 0, 0], [0, 1, 0]) + @classmethod def from_frame(cls, frame): """Construct a plane from a frame. From 65437d4f21cc83404002c7cfa4fdf019084b8329 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 13:48:42 +0200 Subject: [PATCH 34/68] normalisation can result in minor differences between vector components --- tests/compas/geometry/test_plane.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/compas/geometry/test_plane.py b/tests/compas/geometry/test_plane.py index fa86f57cdc2..622b85a7568 100644 --- a/tests/compas/geometry/test_plane.py +++ b/tests/compas/geometry/test_plane.py @@ -40,7 +40,6 @@ def test_plane_data(): assert allclose(other.point, plane.point, tol=1e-12) assert allclose(other.normal, plane.normal, tol=1e-12) - assert plane.data == other.data assert plane.guid != other.guid if not compas.IPY: From 33ac168c1f7b22bc174b20a4bffacf2d4bf84aaa Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 15:49:26 +0200 Subject: [PATCH 35/68] frame tests --- tests/compas/geometry/test_frame.py | 72 ++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/tests/compas/geometry/test_frame.py b/tests/compas/geometry/test_frame.py index 77d96adf2df..f9782e4690d 100644 --- a/tests/compas/geometry/test_frame.py +++ b/tests/compas/geometry/test_frame.py @@ -1,10 +1,70 @@ +from __future__ import division +import pytest +import json +import compas +from random import random +from compas.geometry import allclose +from compas.geometry import close +from compas.geometry import Point +from compas.geometry import Vector from compas.geometry import Frame -def test_axes_are_orthonormal(): - pt = [1, 2, 3] - vec1 = [1, 0, 0] - vec2 = [0, 0.9, 0] +@pytest.mark.parametrize( + "point,xaxis,yaxis", + [ + ([0, 0, 0], [1, 0, 0], [0, 1, 0]), + ([0, 0, 0], [1, 0, 0], [1, 1, 0]), + ([0, 0, 0], [1, 0, 0], [0, 1, 1]), + ([0, 0, 0], [1, 0, 0], [1, 1, 1]), + ([random(), random(), random()], [random(), random(), random()], [random(), random(), random()]), + ], +) +def test_frame(point, xaxis, yaxis): + frame = Frame(point, xaxis, yaxis) + assert frame.point == Point(*point) + assert frame.xaxis == Vector(*xaxis).unitized() + assert close(frame.zaxis.dot(xaxis), 0, tol=1e-12) + assert close(frame.zaxis.dot(yaxis), 0, tol=1e-12) + assert close(frame.xaxis.length, 1, tol=1e-12) + assert close(frame.yaxis.length, 1, tol=1e-12) + assert close(frame.zaxis.length, 1, tol=1e-12) - frame = Frame(pt, vec1, vec2) - assert frame == [[1, 2, 3], [1, 0, 0], [0, 1, 0]] + other = eval(repr(frame)) + assert allclose(frame.point, other.point, tol=1e-12) + assert allclose(frame.xaxis, other.xaxis, tol=1e-12) + assert allclose(frame.yaxis, other.yaxis, tol=1e-12) + + +def test_frame_data(): + point = [random(), random(), random()] + xaxis = [random(), random(), random()] + yaxis = [random(), random(), random()] + frame = Frame(point, xaxis, yaxis) + other = Frame.from_data(json.loads(json.dumps(frame.data))) + + assert allclose(frame.point, other.point, tol=1e-12) + assert allclose(frame.xaxis, other.xaxis, tol=1e-12) + assert allclose(frame.yaxis, other.yaxis, tol=1e-12) + assert frame.guid != other.guid + + if not compas.IPY: + assert Frame.validate_data(frame.data) + assert Frame.validate_data(other.data) + + +def test_frame_predefined(): + frame = Frame.worldXY() + assert frame.point == Point(0, 0, 0) + assert frame.xaxis == Vector(1, 0, 0) + assert frame.yaxis == Vector(0, 1, 0) + + frame = Frame.worldYZ() + assert frame.point == Point(0, 0, 0) + assert frame.xaxis == Vector(0, 1, 0) + assert frame.yaxis == Vector(0, 0, 1) + + frame = Frame.worldZX() + assert frame.point == Point(0, 0, 0) + assert frame.xaxis == Vector(0, 0, 1) + assert frame.yaxis == Vector(1, 0, 0) From a303d7522ffffe22f8fabe93bb4f5b7004362775 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 17:18:37 +0200 Subject: [PATCH 36/68] cleanup --- src/compas/geometry/polygon.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/compas/geometry/polygon.py b/src/compas/geometry/polygon.py index e82c8e9fc30..25232088398 100644 --- a/src/compas/geometry/polygon.py +++ b/src/compas/geometry/polygon.py @@ -5,24 +5,17 @@ import math from compas.utilities import pairwise - from compas.geometry import allclose from compas.geometry import area_polygon - -# from compas.geometry import cross_vectors from compas.geometry import centroid_polygon from compas.geometry import is_coplanar from compas.geometry import is_polygon_convex from compas.geometry import transform_points from compas.geometry import earclip_polygon from compas.geometry import bounding_box - -# from compas.geometry import bestfit_plane from compas.geometry import Geometry from compas.geometry import Transformation from compas.geometry import Point - -# from compas.geometry import Vector from compas.geometry import Plane from compas.geometry import Frame from compas.geometry import Line From 77facd3daf2769c175a44ec8170e4c1053941009 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 17:18:49 +0200 Subject: [PATCH 37/68] schema and dataschema tests --- ...{test_jsonschema.py => test_dataschema.py} | 0 tests/compas/data/test_schema.py | 37 +++++++++++++++++++ 2 files changed, 37 insertions(+) rename tests/compas/data/{test_jsonschema.py => test_dataschema.py} (100%) diff --git a/tests/compas/data/test_jsonschema.py b/tests/compas/data/test_dataschema.py similarity index 100% rename from tests/compas/data/test_jsonschema.py rename to tests/compas/data/test_dataschema.py diff --git a/tests/compas/data/test_schema.py b/tests/compas/data/test_schema.py index e69de29bb2d..11462132c69 100644 --- a/tests/compas/data/test_schema.py +++ b/tests/compas/data/test_schema.py @@ -0,0 +1,37 @@ +import compas +from compas.data import Data +from compas.data import compas_dataclasses +from compas.data import dataclass_dataschema +from compas.data import dataclass_typeschema +from compas.data import dataclass_jsonschema + + +def test_schema_dataclasses(): + for cls in compas_dataclasses(): + assert issubclass(cls, Data) + + +def test_schema_dataclasses_typeschema(): + for cls in compas_dataclasses(): + dtype = dataclass_typeschema(cls) + modname, clsname = dtype["const"].split("/") # type: ignore + assert cls.__name__ == clsname + # module = __import__(modname, fromlist=[clsname]) + # assert hasattr(module, clsname) + # assert getattr(module, clsname) == cls + + +def test_schema_dataclasses_dataschema(): + for cls in compas_dataclasses(): + assert dataclass_dataschema(cls) == cls.DATASCHEMA + + +def test_schema_dataclasses_jsonschema(): + for cls in compas_dataclasses(): + schema = dataclass_jsonschema(cls) + assert schema["$schema"] == "https://json-schema.org/draft/2020-12/schema" + assert schema["$id"] == "{}.json".format(cls.__name__) + assert schema["$compas"] == "{}".format(compas.__version__) + assert schema["type"] == "object" + assert schema["properties"]["dtype"] == dataclass_typeschema(cls) + assert schema["properties"]["data"] == dataclass_dataschema(cls) From 5efdd8eb0f16ada3c6aa78afd64d02f4717f5648 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 17:19:06 +0200 Subject: [PATCH 38/68] polygon and pointcloud tests --- tests/compas/geometry/test_pointcloud.py | 44 ++++++++++++++++++-- tests/compas/geometry/test_polygon.py | 52 ++++++++++++++++++------ 2 files changed, 79 insertions(+), 17 deletions(-) diff --git a/tests/compas/geometry/test_pointcloud.py b/tests/compas/geometry/test_pointcloud.py index 63de3128374..7e884361fd9 100644 --- a/tests/compas/geometry/test_pointcloud.py +++ b/tests/compas/geometry/test_pointcloud.py @@ -1,16 +1,52 @@ -import random +import pytest +import json +import compas +from random import random, shuffle +from compas.geometry import Point # noqa: F401 from compas.geometry import Pointcloud -def test_equality(): +@pytest.mark.parametrize( + "points", + [ + [[0, 0, 0], [1, 0, 0]], + [[0, 0, 0], [1, 0, 0], [1, 1, 0]], + [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], + [[0, 0, x] for x in range(5)], + [[random(), random(), random()] for i in range(10)], + ], +) +def test_pointcloud(points): + pointcloud = Pointcloud(points) + assert pointcloud.points == points + + if not compas.IPY: + assert pointcloud == eval(repr(pointcloud)) + + +def test_pointcloud_data(): + points = [[random(), random(), random()] for i in range(10)] + pointcloud = Pointcloud(points) + other = Pointcloud.from_data(json.loads(json.dumps(pointcloud.to_data()))) + + assert pointcloud == other + assert pointcloud.points == other.points + assert pointcloud.data == other.data + + if not compas.IPY: + assert Pointcloud.validate_data(pointcloud.data) + assert Pointcloud.validate_data(other.data) + + +def test_pointcloud__eq__(): a = Pointcloud.from_bounds(10, 10, 10, 10) points = a.points[:] - random.shuffle(points) + shuffle(points) b = Pointcloud(points) assert a == b -def test_inequality(): +def test_pointcloud__neq__(): a = Pointcloud.from_bounds(10, 10, 10, 10) b = Pointcloud.from_bounds(10, 10, 10, 11) assert a != b diff --git a/tests/compas/geometry/test_polygon.py b/tests/compas/geometry/test_polygon.py index f35c0b6a159..81cc5bfdaf6 100644 --- a/tests/compas/geometry/test_polygon.py +++ b/tests/compas/geometry/test_polygon.py @@ -1,18 +1,36 @@ import pytest - +import json +import compas +from random import random from compas.geometry import Point from compas.geometry import Polygon from compas.utilities import pairwise -def test_polygon(): - points = [[0, 0, x] for x in range(5)] +@pytest.mark.parametrize( + "points", + [ + [[0, 0, 0], [1, 0, 0]], + [[0, 0, 0], [1, 0, 0], [1, 1, 0]], + [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], + [[0, 0, x] for x in range(5)], + [[random(), random(), random()] for i in range(10)], + ], +) +def test_polygon(points): polygon = Polygon(points) assert polygon.points == points assert polygon.lines == [(a, b) for a, b in pairwise(points + points[:1])] + assert polygon.points[-1] != polygon.points[0] + assert polygon.lines[0][0] == polygon.points[0] + assert polygon.lines[-1][1] == polygon.points[0] + assert polygon.lines[-1][0] == polygon.points[-1] + + if not compas.IPY: + assert polygon == eval(repr(polygon)) -def test_ctor_does_not_modify_input_params(): +def test_polygon_constructor_does_not_modify_input_params(): pts = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 0]] polygon = Polygon(pts) @@ -20,7 +38,21 @@ def test_ctor_does_not_modify_input_params(): assert len(polygon.points) == 4, "The last point (matching the first) should have been removed" -def test_equality(): +def test_polygon_data(): + points = [[random(), random(), random()] for i in range(10)] + polygon = Polygon(points) + other = Polygon.from_data(json.loads(json.dumps(polygon.to_data()))) + + assert polygon == other + assert polygon.points == other.points + assert polygon.data == other.data + + if not compas.IPY: + assert Polygon.validate_data(polygon.data) + assert Polygon.validate_data(other.data) + + +def test_polygon__eq__(): points1 = [[0, 0, x] for x in range(5)] polygon1 = Polygon(points1) points2 = [[0, 0, x] for x in range(6)] @@ -38,13 +70,7 @@ def test_equality(): assert polygon1 == polygon3 -def test___repr__(): - points = [[0, 0, x] for x in range(5)] - polygon = Polygon(points) - assert polygon == eval(repr(polygon)) - - -def test___getitem__(): +def test_polygon__getitem__(): points = [[0, 0, x] for x in range(5)] polygon = Polygon(points) for x in range(5): @@ -53,7 +79,7 @@ def test___getitem__(): polygon[6] = [0, 0, 6] -def test___setitem__(): +def test_polygon__setitem__(): points = [[0, 0, x] for x in range(5)] polygon = Polygon(points) point = [1, 1, 4] From c6991f200484bd8f5b527272bd231cfd1bc37427 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 20:19:09 +0200 Subject: [PATCH 39/68] curve data tests --- tests/compas/data/test_json_dotnet.py | 2 +- ...dicates_2.py => test_core_predicates_2.py} | 0 tests/compas/geometry/test_curves_arc.py | 51 +++++++++++++++++-- tests/compas/geometry/test_curves_bezier.py | 26 +++++++++- tests/compas/geometry/test_curves_circle.py | 34 ++++++++++--- tests/compas/geometry/test_curves_ellipse.py | 35 ++++++++++--- .../compas/geometry/test_curves_hyperbola.py | 27 +++++++++- tests/compas/geometry/test_curves_line.py | 32 ++++++++++-- tests/compas/geometry/test_curves_parabola.py | 25 ++++++++- tests/compas/geometry/test_curves_polyline.py | 22 +++++++- 10 files changed, 223 insertions(+), 31 deletions(-) rename tests/compas/geometry/{predicates/test_predicates_2.py => test_core_predicates_2.py} (100%) diff --git a/tests/compas/data/test_json_dotnet.py b/tests/compas/data/test_json_dotnet.py index 3bd7bc2b10a..3f2132bddf1 100644 --- a/tests/compas/data/test_json_dotnet.py +++ b/tests/compas/data/test_json_dotnet.py @@ -1,7 +1,7 @@ import compas try: - import System + import System # type: ignore def test_decimal(): before = System.Decimal(100.0) diff --git a/tests/compas/geometry/predicates/test_predicates_2.py b/tests/compas/geometry/test_core_predicates_2.py similarity index 100% rename from tests/compas/geometry/predicates/test_predicates_2.py rename to tests/compas/geometry/test_core_predicates_2.py diff --git a/tests/compas/geometry/test_curves_arc.py b/tests/compas/geometry/test_curves_arc.py index 673a62237db..6ffc3c09d64 100644 --- a/tests/compas/geometry/test_curves_arc.py +++ b/tests/compas/geometry/test_curves_arc.py @@ -1,7 +1,11 @@ import math +import json import pytest +import compas from compas.geometry import Arc +from compas.geometry import Point # noqa: F401 +from compas.geometry import Vector # noqa: F401 from compas.geometry import Frame from compas.geometry import Circle from compas.geometry import close, allclose @@ -12,7 +16,7 @@ def frame(): return Frame([1.23, 0.44, -4.02], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]) -def test_create_arc(): +def test_arc_create(): arc = Arc(radius=1.0, start_angle=0.0, end_angle=math.pi) assert close(arc.radius, 1.0) @@ -28,8 +32,16 @@ def test_create_arc(): assert allclose(arc.point_at(0.5, world=True), arc.point_at(0.5, world=False), tol=1e-12) assert allclose(arc.point_at(1.0, world=True), arc.point_at(1.0, world=False), tol=1e-12) + other = eval(repr(arc)) + assert arc.radius == other.radius + assert arc.start_angle == other.start_angle + assert arc.end_angle == other.end_angle + assert arc.frame.point == other.frame.point + assert allclose(arc.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(arc.frame.yaxis, other.frame.yaxis, tol=1e-12) -def test_create_arc_frame(frame): + +def test_arc_create_with_frame(frame): arc = Arc(radius=0.2, start_angle=0.0, end_angle=1.14, frame=frame) assert close(arc.radius, 0.2) @@ -38,6 +50,14 @@ def test_create_arc_frame(frame): assert close(arc.end_angle, 1.14) assert not arc.is_circle + other = eval(repr(arc)) + assert arc.radius == other.radius + assert arc.start_angle == other.start_angle + assert arc.end_angle == other.end_angle + assert arc.frame.point == other.frame.point + assert allclose(arc.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(arc.frame.yaxis, other.frame.yaxis, tol=1e-12) + assert not allclose( arc.point_at(0.0, world=True), arc.point_at(0.0, world=False), @@ -71,17 +91,38 @@ def test_create_arc_frame(frame): ) -def test_create_arc_invalid(): +def test_arc_create_invalid(): with pytest.raises(ValueError): Arc(radius=1.0, start_angle=0.2314, end_angle=7.14) +# ============================================================================= +# Data +# ============================================================================= + + +def test_arc_data(): + arc = Arc(radius=1.0, start_angle=0.0, end_angle=math.pi) + other = Arc.from_data(json.loads(json.dumps(arc.data))) + + assert arc.radius == other.radius + assert arc.start_angle == other.start_angle + assert arc.end_angle == other.end_angle + assert arc.frame.point == other.frame.point + assert allclose(arc.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(arc.frame.yaxis, other.frame.yaxis, tol=1e-12) + + if not compas.IPY: + assert Arc.validate_data(arc.data) + assert Arc.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= -def test_create_from_circle(frame): +def test_arc_create_from_circle(frame): circle = Circle(radius=34.222, frame=frame) arc = Arc.from_circle(circle, 0.1, 0.443) @@ -97,7 +138,7 @@ def test_create_from_circle(frame): assert allclose(arc.frame, circle.frame) -def test_create_from_full_circle(frame): +def test_arc_create_from_full_circle(frame): circle = Circle(radius=34.222, frame=frame) arc = Arc.from_circle(circle, 0.0, 2.0 * math.pi) diff --git a/tests/compas/geometry/test_curves_bezier.py b/tests/compas/geometry/test_curves_bezier.py index a7cc8b5ac45..87d9a06a540 100644 --- a/tests/compas/geometry/test_curves_bezier.py +++ b/tests/compas/geometry/test_curves_bezier.py @@ -1,10 +1,13 @@ import pytest +import json +import compas + from compas.geometry import allclose from compas.geometry import Frame from compas.geometry import Bezier -def test_create_bezier(): +def test_bezier_create(): curve = Bezier([[-1, 0, 0], [0, 1, 0], [+1, 0, 0]]) assert allclose(curve.points[0], [-1, 0, 0], tol=1e-12) @@ -16,11 +19,30 @@ def test_create_bezier(): assert allclose(curve.point_at(1.0), [+1, 0, 0], tol=1e-12) -def test_create_bezier_frame(): +def test_bezier_create_with_frame(): with pytest.raises(Exception): Bezier([[-1, 0, 0], [0, 1, 0], [+1, 0, 0]], frame=Frame.worldXY()) +# ============================================================================= +# Data +# ============================================================================= + + +def test_bezier_data(): + curve = Bezier([[-1, 0, 0], [0, 1, 0], [+1, 0, 0]]) + other = Bezier.from_data(json.loads(json.dumps(curve.data))) + + assert curve.points == other.points + assert curve.frame.point == other.frame.point + assert allclose(curve.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(curve.frame.yaxis, other.frame.yaxis, tol=1e-12) + + if not compas.IPY: + assert Bezier.validate_data(curve.data) + assert Bezier.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= diff --git a/tests/compas/geometry/test_curves_circle.py b/tests/compas/geometry/test_curves_circle.py index 256eecbc63f..495d4d667d3 100644 --- a/tests/compas/geometry/test_curves_circle.py +++ b/tests/compas/geometry/test_curves_circle.py @@ -1,3 +1,6 @@ +import json +import compas + from compas.geometry import close from compas.geometry import allclose from compas.geometry import Circle @@ -5,7 +8,7 @@ from compas.geometry import Plane -def test_create_circle(): +def test_circle_create(): circle = Circle(radius=1.0) assert close(circle.radius, 1.0, tol=1e-12) @@ -25,7 +28,7 @@ def test_create_circle(): assert allclose(circle.point_at(1.0), [1.0, 0.0, 0.0], tol=1e-12) -def test_create_circle_frame(): +def test_circle_create_with_frame(): circle = Circle(radius=1.0, frame=Frame.worldZX()) assert close(circle.radius, 1.0, tol=1e-12) @@ -77,12 +80,31 @@ def test_create_circle_frame(): ) +# ============================================================================= +# Data +# ============================================================================= + + +def test_circle_data(): + circle = Circle(radius=1.0) + other = Circle.from_data(json.loads(json.dumps(circle.data))) + + assert circle.radius == other.radius + assert circle.frame.point == other.frame.point + assert allclose(circle.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(circle.frame.yaxis, other.frame.yaxis, tol=1e-12) + + if not compas.IPY: + assert Circle.validate_data(circle.data) + assert Circle.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= -def test_create_circle_from_point_and_radius(): +def test_circle_create_from_point_and_radius(): circle = Circle.from_point_and_radius([1.0, 2.0, 3.0], 1.0) assert close(circle.radius, 1.0, tol=1e-12) @@ -99,7 +121,7 @@ def test_create_circle_from_point_and_radius(): assert allclose(circle.frame.zaxis, Frame.worldXY().zaxis, tol=1e-12) -def test_create_circle_from_plane_and_radius(): +def test_circle_create_from_plane_and_radius(): plane = Plane([1.0, 2.0, 3.0], [0.0, 0.0, 1.0]) frame = Frame.from_plane(plane) circle = Circle.from_plane_and_radius(plane, 1.0) @@ -118,11 +140,11 @@ def test_create_circle_from_plane_and_radius(): assert allclose(circle.frame.zaxis, frame.zaxis, tol=1e-12) -def test_create_circle_from_three_points(): +def test_circle_create_from_three_points(): pass -def test_create_circle_from_points(): +def test_circle_create_from_points(): pass diff --git a/tests/compas/geometry/test_curves_ellipse.py b/tests/compas/geometry/test_curves_ellipse.py index cd53fa7e7df..00b752ab8aa 100644 --- a/tests/compas/geometry/test_curves_ellipse.py +++ b/tests/compas/geometry/test_curves_ellipse.py @@ -1,4 +1,7 @@ import pytest +import json +import compas + from compas.geometry import close from compas.geometry import allclose from compas.geometry import Frame @@ -6,13 +9,12 @@ from compas.geometry import Plane -def test_create_ellipse(): +def test_ellipse_create(): ellipse = Ellipse(major=1.0, minor=0.5) assert close(ellipse.major, 1.0, tol=1e-12) assert close(ellipse.minor, 0.5, tol=1e-12) assert close(ellipse.area, 1.5707963267948966, tol=1e-12) - # assert close(ellipse.circumference, 4.442882938158366, tol=1e-12) assert close(ellipse.semifocal, 0.8660254037844386, tol=1e-12) assert close(ellipse.eccentricity, 0.8660254037844386, tol=1e-12) assert close(ellipse.focal, 1.7320508075688772, tol=1e-12) @@ -35,13 +37,12 @@ def test_create_ellipse(): assert allclose(ellipse.point_at(1.0), ellipse.point_at(1.0, world=False), tol=1e-12) -def test_create_ellipse_frame(): +def test_ellipse_create_with_frame(): ellipse = Ellipse(major=1.0, minor=0.5, frame=Frame.worldZX()) assert close(ellipse.major, 1.0, tol=1e-12) assert close(ellipse.minor, 0.5, tol=1e-12) assert close(ellipse.area, 1.5707963267948966, tol=1e-12) - # assert close(ellipse.circumference, 4.442882938158366, tol=1e-12) assert close(ellipse.semifocal, 0.8660254037844386, tol=1e-12) assert close(ellipse.eccentricity, 0.8660254037844386, tol=1e-12) assert close(ellipse.focal, 1.7320508075688772, tol=1e-12) @@ -90,18 +91,37 @@ def test_create_ellipse_frame(): ) +# ============================================================================= +# Data +# ============================================================================= + + +def test_ellipse_data(): + ellipse = Ellipse(major=1.0, minor=0.5) + other = Ellipse.from_data(json.loads(json.dumps(ellipse.data))) + + assert ellipse.major == other.major + assert ellipse.minor == other.minor + assert ellipse.frame.point == other.frame.point + assert allclose(ellipse.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(ellipse.frame.yaxis, other.frame.yaxis, tol=1e-12) + + if not compas.IPY: + assert Ellipse.validate_data(ellipse.data) + assert Ellipse.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= -def test_create_ellipse_from_point_major_minor(): +def test_ellipse_create_from_point_major_minor(): ellipse = Ellipse.from_point_major_minor([1.0, 2.0, 3.0], 1.0, 0.5) assert close(ellipse.major, 1.0, tol=1e-12) assert close(ellipse.minor, 0.5, tol=1e-12) assert close(ellipse.area, 1.5707963267948966, tol=1e-12) - # assert close(ellipse.circumference, 4.442882938158366, tol=1e-12) assert close(ellipse.semifocal, 0.8660254037844386, tol=1e-12) assert close(ellipse.eccentricity, 0.8660254037844386, tol=1e-12) assert close(ellipse.focal, 1.7320508075688772, tol=1e-12) @@ -115,7 +135,7 @@ def test_create_ellipse_from_point_major_minor(): assert allclose(ellipse.frame.zaxis, Frame.worldXY().zaxis, tol=1e-12) -def test_create_ellipse_from_plane_major_minor(): +def test_ellipse_create_from_plane_major_minor(): plane = Plane([1.0, 2.0, 3.0], [0.0, 0.0, 1.0]) frame = Frame.from_plane(plane) ellipse = Ellipse.from_plane_major_minor(plane, 1.0, 0.5) @@ -123,7 +143,6 @@ def test_create_ellipse_from_plane_major_minor(): assert close(ellipse.major, 1.0, tol=1e-12) assert close(ellipse.minor, 0.5, tol=1e-12) assert close(ellipse.area, 1.5707963267948966, tol=1e-12) - # assert close(ellipse.circumference, 4.442882938158366, tol=1e-12) assert close(ellipse.semifocal, 0.8660254037844386, tol=1e-12) assert close(ellipse.eccentricity, 0.8660254037844386, tol=1e-12) assert close(ellipse.focal, 1.7320508075688772, tol=1e-12) diff --git a/tests/compas/geometry/test_curves_hyperbola.py b/tests/compas/geometry/test_curves_hyperbola.py index 8207a3684a0..72165f46c53 100644 --- a/tests/compas/geometry/test_curves_hyperbola.py +++ b/tests/compas/geometry/test_curves_hyperbola.py @@ -1,11 +1,14 @@ import pytest +import json +import compas + from compas.geometry import close from compas.geometry import allclose from compas.geometry import Frame from compas.geometry import Hyperbola -def test_create_hyperbola(): +def test_hyperbola_create(): hyperbola = Hyperbola(major=1.0, minor=0.5) assert close(hyperbola.major, 1.0, tol=1e-12) @@ -26,7 +29,7 @@ def test_create_hyperbola(): assert allclose(hyperbola.point_at(1.0), hyperbola.point_at(1.0, world=False), tol=1e-12) -def test_create_hyperbola_frame(): +def test_hyperbola_create_with_frame(): hyperbola = Hyperbola(major=1.0, minor=0.5, frame=Frame.worldZX()) assert close(hyperbola.major, 1.0, tol=1e-12) @@ -67,6 +70,26 @@ def test_create_hyperbola_frame(): ) +# ============================================================================= +# Data +# ============================================================================= + + +def test_hyperbola_data(): + hyperbola = Hyperbola(major=1.0, minor=0.5) + other = Hyperbola.from_data(json.loads(json.dumps(hyperbola.data))) + + assert hyperbola.major == other.major + assert hyperbola.minor == other.minor + assert hyperbola.frame.point == other.frame.point + assert allclose(hyperbola.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(hyperbola.frame.yaxis, other.frame.yaxis, tol=1e-12) + + if not compas.IPY: + assert Hyperbola.validate_data(hyperbola.data) + assert Hyperbola.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= diff --git a/tests/compas/geometry/test_curves_line.py b/tests/compas/geometry/test_curves_line.py index 980826d072c..4a26b6324d1 100644 --- a/tests/compas/geometry/test_curves_line.py +++ b/tests/compas/geometry/test_curves_line.py @@ -1,4 +1,7 @@ import pytest +import json +import compas + from compas.geometry import add_vectors from compas.geometry import scale_vector from compas.geometry import normalize_vector @@ -27,11 +30,34 @@ ([0, 0, 0], Point(1, 2, 3)), ], ) -def test_create_line(p1, p2): +def test_line_create(p1, p2): line = Line(p1, p2) assert line.start == p1 assert line.end == p2 + assert line.frame == Frame.worldXY() + + +def test_line_create_with_frame(): + with pytest.raises(AttributeError): + Line([0, 0, 0], [1, 0, 0], frame=Frame.worldXY()) + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_line_data(): + line = Line([0, 0, 0], [1, 0, 0]) + other = Line.from_data(json.loads(json.dumps(line.data))) + + assert line.start == other.start + assert line.end == other.end + + if not compas.IPY: + assert Line.validate_data(line.data) + assert Line.validate_data(other.data) # ============================================================================= @@ -50,7 +76,7 @@ def test_create_line(p1, p2): (Point(0, 0, 0), Vector(1, 2, 3)), ], ) -def test_create_line_from_point_and_vector(point, vector): +def test_line_create_from_point_and_vector(point, vector): line = Line.from_point_and_vector(point, vector) assert line.start == point @@ -68,7 +94,7 @@ def test_create_line_from_point_and_vector(point, vector): (Point(0, 0, 0), Vector(1, 2, 3), 3.0), ], ) -def test_create_line_from_point_direction_length(point, direction, length): +def test_line_create_from_point_direction_length(point, direction, length): line = Line.from_point_direction_length(point, direction, length) assert line.start == point diff --git a/tests/compas/geometry/test_curves_parabola.py b/tests/compas/geometry/test_curves_parabola.py index c41407f632a..40e7bf6eeb8 100644 --- a/tests/compas/geometry/test_curves_parabola.py +++ b/tests/compas/geometry/test_curves_parabola.py @@ -1,11 +1,13 @@ import pytest +import json +import compas from compas.geometry import allclose from compas.geometry import Frame from compas.geometry import Parabola -def test_create_parabola(): +def test_parabola_create(): parabola = Parabola(focal=1) assert parabola.focal == 1 @@ -16,7 +18,7 @@ def test_create_parabola(): assert allclose(parabola.point_at(1.0), parabola.point_at(1.0, world=False), tol=1e-12) -def test_create_parabola_frame(): +def test_parabola_create_with_frame(): frame = Frame.worldZX() parabola = Parabola(focal=1, frame=frame) @@ -44,6 +46,25 @@ def test_create_parabola_frame(): ) +# ============================================================================= +# Data +# ============================================================================= + + +def test_parabola_data(): + parabola = Parabola(focal=1) + other = Parabola.from_data(json.loads(json.dumps(parabola.data))) + + assert parabola.focal == other.focal + assert parabola.frame.point == other.frame.point + assert allclose(parabola.frame.xaxis, other.frame.xaxis, tol=1e-12) + assert allclose(parabola.frame.yaxis, other.frame.yaxis, tol=1e-12) + + if not compas.IPY: + assert Parabola.validate_data(parabola.data) + assert Parabola.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= diff --git a/tests/compas/geometry/test_curves_polyline.py b/tests/compas/geometry/test_curves_polyline.py index 7872e61007d..8747759e31e 100644 --- a/tests/compas/geometry/test_curves_polyline.py +++ b/tests/compas/geometry/test_curves_polyline.py @@ -1,5 +1,7 @@ import pytest import math +import json +import compas from compas.geometry import Frame from compas.geometry import Polyline @@ -14,17 +16,33 @@ [[0, 0, 0], [1, 0, 0], [2, 0, 0]], ], ) -def test_create_polyline(points): +def test_polyline_create(points): curve = Polyline(points) assert curve.frame == Frame.worldXY() -def test_create_polyline_frame(): +def test_polyline_create_with_frame(): with pytest.raises(AttributeError): Polyline([], frame=Frame.worldXY()) +# ============================================================================= +# Data +# ============================================================================= + + +def test_polyline_data(): + curve = Polyline([[0, 0, 0], [1, 0, 0]]) + other = Polyline.from_data(json.loads(json.dumps(curve.data))) + + assert curve.points == other.points + + if not compas.IPY: + assert Polyline.validate_data(curve.data) + assert Polyline.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= From 954ef9ec0d318eb38fae14392ce1f3421e1ddf77 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 20:43:46 +0200 Subject: [PATCH 40/68] basic color tests --- tests/compas/colors/test_color.py | 69 +++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 tests/compas/colors/test_color.py diff --git a/tests/compas/colors/test_color.py b/tests/compas/colors/test_color.py new file mode 100644 index 00000000000..d654df50f27 --- /dev/null +++ b/tests/compas/colors/test_color.py @@ -0,0 +1,69 @@ +import pytest +import json +import compas +from random import random + +from compas.colors import Color + + +@pytest.mark.parametrize( + "color", + [ + (0, 0, 0), + (1, 1, 1), + (0.5, 0.5, 0.5), + (0.5, 0.5, 0.5, 0.5), + (0.5, 0.5, 0.5, 1.0), + (0.5, 0.5, 0.5, 0.0), + (random(), random(), random()), + ], +) +def test_color(color): + c = Color(*color) + assert c.r == color[0] + assert c.g == color[1] + assert c.b == color[2] + assert c.a == color[3] if len(color) == 4 else 1.0 + + assert eval(repr(c)) == c + + +def test_color_data(): + color = Color(random(), random(), random(), random()) + other = Color.from_data(json.loads(json.dumps(color.data))) + + assert color.r == other.r + assert color.g == other.g + assert color.b == other.b + assert color.a == other.a + + assert color == other + + if not compas.IPY: + assert Color.validate_data(color.data) + assert Color.validate_data(other.data) + + +def test_color_predefined(): + assert Color.red() == Color(1.0, 0.0, 0.0) + assert Color.green() == Color(0.0, 1.0, 0.0) + assert Color.blue() == Color(0.0, 0.0, 1.0) + assert Color.cyan() == Color(0.0, 1.0, 1.0) + assert Color.magenta() == Color(1.0, 0.0, 1.0) + assert Color.yellow() == Color(1.0, 1.0, 0.0) + assert Color.white() == Color(1.0, 1.0, 1.0) + assert Color.black() == Color(0.0, 0.0, 0.0) + assert Color.grey() == Color(0.5, 0.5, 0.5) + assert Color.orange() == Color(1.0, 0.5, 0.0) + assert Color.lime() == Color(0.5, 1.0, 0.0) + assert Color.mint() == Color(0.0, 1.0, 0.5) + assert Color.azure() == Color(0.0, 0.5, 1.0) + assert Color.violet() == Color(0.5, 0.0, 1.0) + assert Color.pink() == Color(1.0, 0.0, 0.5) + assert Color.brown() == Color(0.5, 0.25, 0.0) + assert Color.purple() == Color(0.5, 0.0, 0.5) + assert Color.teal() == Color(0.0, 0.5, 0.5) + assert Color.olive() == Color(0.5, 0.5, 0.0) + assert Color.navy() == Color(0.0, 0.0, 0.5) + assert Color.maroon() == Color(0.5, 0.0, 0.0) + assert Color.silver() == Color(0.75, 0.75, 0.75) From be62def494b05f90523e69a0d6504a51fc72238b Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 22:23:44 +0200 Subject: [PATCH 41/68] adjacency is not a required attribute --- src/compas/datastructures/graph/graph.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/compas/datastructures/graph/graph.py b/src/compas/datastructures/graph/graph.py index a4c7488c099..a9e0d908500 100644 --- a/src/compas/datastructures/graph/graph.py +++ b/src/compas/datastructures/graph/graph.py @@ -58,13 +58,6 @@ class Graph(Datastructure): "additionalProperties": {"type": "object"}, }, }, - "adjacency": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {"type": "null"}, - }, - }, "max_node": {"type": "integer", "minimum": -1}, }, "required": [ @@ -73,7 +66,6 @@ class Graph(Datastructure): "dea", "node", "edge", - "adjacency", "max_node", ], } From b009b6812c6b9d177af3ab404d6381f27a1bd666 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 22:47:24 +0200 Subject: [PATCH 42/68] closeness not equality --- tests/compas/geometry/test_curves_arc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/compas/geometry/test_curves_arc.py b/tests/compas/geometry/test_curves_arc.py index 6ffc3c09d64..4d997bb6f7d 100644 --- a/tests/compas/geometry/test_curves_arc.py +++ b/tests/compas/geometry/test_curves_arc.py @@ -34,8 +34,8 @@ def test_arc_create(): other = eval(repr(arc)) assert arc.radius == other.radius - assert arc.start_angle == other.start_angle - assert arc.end_angle == other.end_angle + assert close(arc.start_angle, other.start_angle, tol=1e-12) + assert close(arc.end_angle, other.end_angle, tol=1e-12) assert arc.frame.point == other.frame.point assert allclose(arc.frame.xaxis, other.frame.xaxis, tol=1e-12) assert allclose(arc.frame.yaxis, other.frame.yaxis, tol=1e-12) @@ -52,8 +52,8 @@ def test_arc_create_with_frame(frame): other = eval(repr(arc)) assert arc.radius == other.radius - assert arc.start_angle == other.start_angle - assert arc.end_angle == other.end_angle + assert close(arc.start_angle, other.start_angle, tol=1e-12) + assert close(arc.end_angle, other.end_angle, tol=1e-12) assert arc.frame.point == other.frame.point assert allclose(arc.frame.xaxis, other.frame.xaxis, tol=1e-12) assert allclose(arc.frame.yaxis, other.frame.yaxis, tol=1e-12) @@ -106,8 +106,8 @@ def test_arc_data(): other = Arc.from_data(json.loads(json.dumps(arc.data))) assert arc.radius == other.radius - assert arc.start_angle == other.start_angle - assert arc.end_angle == other.end_angle + assert close(arc.start_angle, other.start_angle, tol=1e-12) + assert close(arc.end_angle, other.end_angle, tol=1e-12) assert arc.frame.point == other.frame.point assert allclose(arc.frame.xaxis, other.frame.xaxis, tol=1e-12) assert allclose(arc.frame.yaxis, other.frame.yaxis, tol=1e-12) From fcadae1932cc1b3e6bee09018269f51b41a6031c Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 22:47:39 +0200 Subject: [PATCH 43/68] adjacency is not required --- tests/compas/data/test_dataschema.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/compas/data/test_dataschema.py b/tests/compas/data/test_dataschema.py index 5f1fce18d64..0a5d1f56a95 100644 --- a/tests/compas/data/test_dataschema.py +++ b/tests/compas/data/test_dataschema.py @@ -601,7 +601,6 @@ def test_schema_pointcloud_invalid(pointcloud): "dea": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": -1, }, { @@ -610,7 +609,6 @@ def test_schema_pointcloud_invalid(pointcloud): "dea": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": 0, }, { @@ -619,7 +617,6 @@ def test_schema_pointcloud_invalid(pointcloud): "dea": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": 1000, }, ], @@ -636,7 +633,6 @@ def test_schema_graph_valid(graph): "dea": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": -2, }, { @@ -644,7 +640,6 @@ def test_schema_graph_valid(graph): "dea": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": -1, }, { @@ -652,7 +647,6 @@ def test_schema_graph_valid(graph): "dea": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": -1, }, { @@ -660,7 +654,6 @@ def test_schema_graph_valid(graph): "dna": {}, "node": {}, "edge": {}, - "adjacency": {}, "max_node": -1, }, { @@ -668,7 +661,6 @@ def test_schema_graph_valid(graph): "dna": {}, "dea": {}, "edge": {}, - "adjacency": {}, "max_node": -1, }, { @@ -676,7 +668,6 @@ def test_schema_graph_valid(graph): "dna": {}, "dea": {}, "node": {}, - "adjacency": {}, "max_node": -1, }, { @@ -685,15 +676,6 @@ def test_schema_graph_valid(graph): "dea": {}, "node": {}, "edge": {}, - "max_node": -1, - }, - { - "attributes": {}, - "dna": {}, - "dea": {}, - "node": {}, - "edge": {}, - "adjacency": {}, }, ], ) From d70781fc045545821ec5787325608167eb746dc1 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 22:47:48 +0200 Subject: [PATCH 44/68] type stuff --- src/compas/colors/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compas/colors/color.py b/src/compas/colors/color.py index a5738536270..02f2942ba18 100644 --- a/src/compas/colors/color.py +++ b/src/compas/colors/color.py @@ -3,7 +3,7 @@ from __future__ import print_function try: - basestring + basestring # type: ignore except NameError: basestring = str From 1bae6e5398d0e3a21022c4922d2ad99cd7fb61fa Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Mon, 14 Aug 2023 22:48:09 +0200 Subject: [PATCH 45/68] base datastructure data tests --- tests/compas/datastructures/test_graph.py | 109 ++++++++++++------- tests/compas/datastructures/test_halfedge.py | 61 +++++++++-- tests/compas/datastructures/test_halfface.py | 20 ++-- 3 files changed, 129 insertions(+), 61 deletions(-) diff --git a/tests/compas/datastructures/test_graph.py b/tests/compas/datastructures/test_graph.py index 5ad2ccb31a9..fe8fcbba79a 100644 --- a/tests/compas/datastructures/test_graph.py +++ b/tests/compas/datastructures/test_graph.py @@ -1,6 +1,7 @@ import pytest - +import json import compas + from compas.datastructures import Graph @@ -19,38 +20,81 @@ def graph(): # ============================================================================== -# Tests - Schema & jsonschema +# Basics +# ============================================================================== + # ============================================================================== +# Data +# ============================================================================== + + +def test_graph_data(graph): + other = Graph.from_data(json.loads(json.dumps(graph.data))) + assert graph.data == other.data + assert graph.default_node_attributes == other.default_node_attributes + assert graph.default_edge_attributes == other.default_edge_attributes + assert graph.number_of_nodes() == other.number_of_nodes() + assert graph.number_of_edges() == other.number_of_edges() -# def test_edgedata_directionality(graph): -# graph.update_default_edge_attributes({'index': 0}) -# for index, (u, v) in enumerate(graph.edges()): -# graph.edge_attribute((u, v), 'index', index) -# assert all(graph.edge_attribute((u, v), 'index') != graph.edge_attribute((v, u), 'index') for u, v in graph.edges()) + if not compas.IPY: + assert Graph.validate_data(graph.data) + assert Graph.validate_data(other.data) -def test_edgedata_io(graph): - graph.update_default_edge_attributes({"index": 0}) - for index, edge in enumerate(graph.edges()): - graph.edge_attribute(edge, "index", index) - other = Graph.from_data(graph.data) - assert all(other.edge_attribute(edge, "index") == index for index, edge in enumerate(other.edges())) +# ============================================================================== +# Constructors +# ============================================================================== + +# ============================================================================== +# Properties +# ============================================================================== + +# ============================================================================== +# Accessors +# ============================================================================== +# ============================================================================== +# Builders +# ============================================================================== # ============================================================================== -# Tests - Samples +# Modifiers # ============================================================================== -def test_node_sample(graph): +def test_graph_invalid_edge_delete(): + graph = Graph() + node = graph.add_node() + edge = graph.add_edge(node, node) + graph.delete_edge(edge) + assert graph.has_edge(edge) is False + + +def test_graph_opposite_direction_edge_delete(): + graph = Graph() + node_a = graph.add_node() + node_b = graph.add_node() + edge_a = graph.add_edge(node_a, node_b) + edge_b = graph.add_edge(node_b, node_a) + graph.delete_edge(edge_a) + assert graph.has_edge(edge_a) is False + assert graph.has_edge(edge_b) is True + + +# ============================================================================== +# Samples +# ============================================================================== + + +def test_graph_node_sample(graph): for node in graph.node_sample(): assert graph.has_node(node) for node in graph.node_sample(size=graph.number_of_nodes()): assert graph.has_node(node) -def test_edge_sample(graph): +def test_graph_edge_sample(graph): for edge in graph.edge_sample(): assert graph.has_edge(edge) for edge in graph.edge_sample(size=graph.number_of_edges()): @@ -58,11 +102,11 @@ def test_edge_sample(graph): # ============================================================================== -# Tests - Attributes +# Attributes # ============================================================================== -def test_default_node_attributes(): +def test_graph_default_node_attributes(): graph = Graph(name="test", default_node_attributes={"a": 1, "b": 2}) for node in graph.nodes(): assert graph.node_attribute(node, name="a") == 1 @@ -71,7 +115,7 @@ def test_default_node_attributes(): assert graph.node_attribute(node, name="a") == 3 -def test_default_edge_attributes(): +def test_graph_default_edge_attributes(): graph = Graph(name="test", default_edge_attributes={"a": 1, "b": 2}) for edge in graph.edges(): assert graph.edge_attribute(edge, name="a") == 1 @@ -81,11 +125,11 @@ def test_default_edge_attributes(): # ============================================================================== -# Tests - Conversion +# Conversion # ============================================================================== -def test_graph_networkx_conversion(): +def test_graph_to_networkx(): if compas.IPY: return @@ -101,8 +145,8 @@ def test_graph_networkx_conversion(): nxg = g.to_networkx() - assert nxg.graph["name"] == "DiGraph", "Graph attributes must be preserved" - assert nxg.graph["val"] == (0, 0, 0), "Graph attributes must be preserved" + assert nxg.graph["name"] == "DiGraph", "Graph attributes must be preserved" # type: ignore + assert nxg.graph["val"] == (0, 0, 0), "Graph attributes must be preserved" # type: ignore assert set(nxg.nodes()) == set(g.nodes()), "Node sets must match" assert nxg.nodes[1]["weight"] == 1.2, "Node attributes must be preserved" assert nxg.nodes[1]["height"] == "test", "Node attributes must be preserved" @@ -118,22 +162,3 @@ def test_graph_networkx_conversion(): assert g2.edge_attribute((0, 1), "attr_value") == 10 assert g2.attributes["name"] == "DiGraph", "Graph attributes must be preserved" assert g2.attributes["val"] == (0, 0, 0), "Graph attributes must be preserved" - - -def test_invalid_edge_delete(): - graph = Graph() - node = graph.add_node() - edge = graph.add_edge(node, node) - graph.delete_edge(edge) - assert graph.has_edge(edge) is False - - -def test_opposite_direction_edge_delete(): - graph = Graph() - node_a = graph.add_node() - node_b = graph.add_node() - edge_a = graph.add_edge(node_a, node_b) - edge_b = graph.add_edge(node_b, node_a) - graph.delete_edge(edge_a) - assert graph.has_edge(edge_a) is False - assert graph.has_edge(edge_b) is True diff --git a/tests/compas/datastructures/test_halfedge.py b/tests/compas/datastructures/test_halfedge.py index 1534477850f..7dcebc57d02 100644 --- a/tests/compas/datastructures/test_halfedge.py +++ b/tests/compas/datastructures/test_halfedge.py @@ -1,5 +1,7 @@ import pytest import random +import json +import compas from compas.geometry import Sphere from compas.geometry import Box @@ -61,7 +63,7 @@ def grid(): # ============================================================================== -# Tests - Schema & jsonschema +# ??? # ============================================================================== @@ -81,7 +83,52 @@ def test_edgedata_io(mesh): # ============================================================================== -# Tests - Samples +# Basics +# ============================================================================== + +# ============================================================================== +# Data +# ============================================================================== + + +def test_halfedge_data(mesh): + other = HalfEdge.from_data(json.loads(json.dumps(mesh.data))) + + assert mesh.data == other.data + assert mesh.default_vertex_attributes == other.default_vertex_attributes + assert mesh.default_edge_attributes == other.default_edge_attributes + assert mesh.default_face_attributes == other.default_face_attributes + assert mesh.number_of_vertices() == other.number_of_vertices() + assert mesh.number_of_edges() == other.number_of_edges() + assert mesh.number_of_faces() == other.number_of_faces() + + if not compas.IPY: + assert HalfEdge.validate_data(mesh.data) + assert HalfEdge.validate_data(other.data) + + +# ============================================================================== +# Constructors +# ============================================================================== + +# ============================================================================== +# Properties +# ============================================================================== + +# ============================================================================== +# Accessors +# ============================================================================== + +# ============================================================================== +# Builders +# ============================================================================== + +# ============================================================================== +# Modifiers +# ============================================================================== + +# ============================================================================== +# Samples # ============================================================================== @@ -107,7 +154,7 @@ def test_face_sample(mesh): # ============================================================================== -# Tests - Vertex Attributes +# Vertex Attributes # ============================================================================== @@ -158,7 +205,7 @@ def test_del_vertex_attribute_in_view(mesh, vertex_key): # ============================================================================== -# Tests - Face Attributes +# Face Attributes # ============================================================================== @@ -208,7 +255,7 @@ def test_del_face_attribute_in_view(mesh, face_key): # ============================================================================== -# Tests - Edge Attributes +# Edge Attributes # ============================================================================== @@ -258,7 +305,7 @@ def test_del_edge_attribute_in_view(mesh, edge_key): # ============================================================================== -# Tests - Halfedges Before/After +# Halfedges Before/After # ============================================================================== @@ -285,7 +332,7 @@ def test_halfedge_before_on_boundary(grid): # ============================================================================== -# Tests - Loops & Strip +# Loops & Strip # ============================================================================== diff --git a/tests/compas/datastructures/test_halfface.py b/tests/compas/datastructures/test_halfface.py index 97cedbd8669..66da9b0ea1c 100644 --- a/tests/compas/datastructures/test_halfface.py +++ b/tests/compas/datastructures/test_halfface.py @@ -6,11 +6,7 @@ # ============================================================================== # ============================================================================== -# Tests - Schema & jsonschema -# ============================================================================== - -# ============================================================================== -# Tests - Vertex Attributes +# Vertex Attributes # ============================================================================== @@ -24,7 +20,7 @@ def test_default_vertex_attributes(): # ============================================================================== -# Tests - Face Attributes +# Face Attributes # ============================================================================== @@ -38,7 +34,7 @@ def test_default_face_attributes(): # ============================================================================== -# Tests - Edge Attributes +# Edge Attributes # ============================================================================== @@ -52,7 +48,7 @@ def test_default_edge_attributes(): # ============================================================================== -# Tests - Cell Attributes +# Cell Attributes # ============================================================================== @@ -66,7 +62,7 @@ def test_default_cell_attributes(): # ============================================================================== -# Tests - Vertex Queries +# Vertex Queries # ============================================================================== @@ -88,7 +84,7 @@ def test_vertices_where_predicate(): # ============================================================================== -# Tests - Edge Queries +# Edge Queries # ============================================================================== @@ -111,7 +107,7 @@ def test_edges_where_predicate(): # ============================================================================== -# Tests - Face Queries +# Face Queries # ============================================================================== @@ -136,7 +132,7 @@ def test_faces_where_predicate(): # ============================================================================== -# Tests - Cell Queries +# Cell Queries # ============================================================================== From 9a9a3830a1666daa3daa5b73bb261f72489c363c Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 08:28:11 +0200 Subject: [PATCH 46/68] fix color repr ipy? --- src/compas/colors/color.py | 112 ++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 58 deletions(-) diff --git a/src/compas/colors/color.py b/src/compas/colors/color.py index 02f2942ba18..9a8b59558e7 100644 --- a/src/compas/colors/color.py +++ b/src/compas/colors/color.py @@ -126,8 +126,56 @@ def __init__(self, red, green, blue, alpha=1.0, **kwargs): self.b = blue self.a = alpha + def __repr__(self): + return "{0}({1}, {2}, {3}, alpha={4})".format(type(self).__name__, self.r, self.g, self.b, self.a) + + def __getitem__(self, key): + if key == 0: + return self.r + if key == 1: + return self.g + if key == 2: + return self.b + raise KeyError + + def __len__(self): + return 3 + + def __iter__(self): + return iter(self.rgb) + + def __eq__(self, other): + return all(a == b for a, b in zip(self, other)) + + # -------------------------------------------------------------------------- + # Descriptor + # -------------------------------------------------------------------------- + + def __set_name__(self, owner, name): + self.public_name = name + self.private_name = "_" + name + + def __get__(self, obj, otype=None): + return getattr(obj, self.private_name, None) or self + + def __set__(self, obj, value): + if not obj: + return + + if not value: + return + + if Color.is_rgb255(value): + value = Color.from_rgb255(value[0], value[1], value[2]) + elif Color.is_hex(value): + value = Color.from_hex(value) + else: + value = Color(value[0], value[1], value[2]) + + setattr(obj, self.private_name, value) + # -------------------------------------------------------------------------- - # data + # Data # -------------------------------------------------------------------------- @property @@ -135,7 +183,7 @@ def data(self): return {"red": self.r, "green": self.g, "blue": self.b, "alpha": self.a} # -------------------------------------------------------------------------- - # properties + # Properties # -------------------------------------------------------------------------- @property @@ -272,59 +320,7 @@ def saturation(self): return (maxval - minval) / maxval # -------------------------------------------------------------------------- - # descriptor - # -------------------------------------------------------------------------- - - def __set_name__(self, owner, name): - self.public_name = name - self.private_name = "_" + name - - def __get__(self, obj, otype=None): - return getattr(obj, self.private_name, None) or self - - def __set__(self, obj, value): - if not obj: - return - - if not value: - return - - if Color.is_rgb255(value): - value = Color.from_rgb255(value[0], value[1], value[2]) - elif Color.is_hex(value): - value = Color.from_hex(value) - else: - value = Color(value[0], value[1], value[2]) - - setattr(obj, self.private_name, value) - - # -------------------------------------------------------------------------- - # customization - # -------------------------------------------------------------------------- - - def __repr__(self): - return "Color({}, {}, {}, {})".format(self.r, self.g, self.b, self.a) - - def __getitem__(self, key): - if key == 0: - return self.r - if key == 1: - return self.g - if key == 2: - return self.b - raise KeyError - - def __len__(self): - return 3 - - def __iter__(self): - return iter(self.rgb) - - def __eq__(self, other): - return all(a == b for a, b in zip(self, other)) - - # -------------------------------------------------------------------------- - # constructors + # Constructors # -------------------------------------------------------------------------- @classmethod @@ -529,7 +525,7 @@ def from_name(cls, name): return cls.from_rgb255(*rgb255) # -------------------------------------------------------------------------- - # presets + # Presets # -------------------------------------------------------------------------- @classmethod @@ -698,7 +694,7 @@ def pink(cls): return cls(1.0, 0.0, 0.5) # -------------------------------------------------------------------------- - # other presets + # Other presets # -------------------------------------------------------------------------- @classmethod @@ -786,7 +782,7 @@ def silver(cls): # salmon # -------------------------------------------------------------------------- - # methods + # Methods # -------------------------------------------------------------------------- @staticmethod From d8574d4d3340baea5cf440d9d76d60ecf7347242 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 08:30:12 +0200 Subject: [PATCH 47/68] use allclose with random numbers --- tests/compas/colors/test_color.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/compas/colors/test_color.py b/tests/compas/colors/test_color.py index d654df50f27..d771c61d613 100644 --- a/tests/compas/colors/test_color.py +++ b/tests/compas/colors/test_color.py @@ -4,6 +4,7 @@ from random import random from compas.colors import Color +from compas.geometry import allclose @pytest.mark.parametrize( @@ -25,7 +26,7 @@ def test_color(color): assert c.b == color[2] assert c.a == color[3] if len(color) == 4 else 1.0 - assert eval(repr(c)) == c + assert allclose(eval(repr(c)), c, tol=1e-12) def test_color_data(): From dd371e1b1e0c61f929e83393384725896d27c41e Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 08:35:16 +0200 Subject: [PATCH 48/68] halfface data test --- tests/compas/datastructures/test_halfface.py | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/compas/datastructures/test_halfface.py b/tests/compas/datastructures/test_halfface.py index 66da9b0ea1c..2c6676a69f5 100644 --- a/tests/compas/datastructures/test_halfface.py +++ b/tests/compas/datastructures/test_halfface.py @@ -1,10 +1,72 @@ +import pytest +import json +import compas + from compas.datastructures import HalfFace +from compas.datastructures import VolMesh # ============================================================================== # Fixtures # ============================================================================== + +@pytest.fixture +def halfface(): + return VolMesh.from_meshgrid(1, 1, 1, 2, 2, 2) + + +# ============================================================================== +# Basics +# ============================================================================== + +# ============================================================================== +# Data +# ============================================================================== + + +def test_halfface_data(halfface): + other = HalfFace.from_data(json.loads(json.dumps(halfface.data))) + + assert halfface.data == other.data + assert halfface.default_vertex_attributes == other.default_vertex_attributes + assert halfface.default_edge_attributes == other.default_edge_attributes + assert halfface.default_face_attributes == other.default_face_attributes + assert halfface.default_cell_attributes == other.default_cell_attributes + assert halfface.number_of_vertices() == other.number_of_vertices() + assert halfface.number_of_edges() == other.number_of_edges() + assert halfface.number_of_faces() == other.number_of_faces() + assert halfface.number_of_cells() == other.number_of_cells() + + if not compas.IPY: + assert HalfFace.validate_data(halfface.data) + assert HalfFace.validate_data(other.data) + + +# ============================================================================== +# Constructors +# ============================================================================== + +# ============================================================================== +# Properties +# ============================================================================== + +# ============================================================================== +# Accessors +# ============================================================================== + +# ============================================================================== +# Builders +# ============================================================================== + +# ============================================================================== +# Modifiers +# ============================================================================== + +# ============================================================================== +# Samples +# ============================================================================== + # ============================================================================== # Vertex Attributes # ============================================================================== From 7e7e82f8be73f4e269519538301ea654c83ea8e8 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 09:10:24 +0200 Subject: [PATCH 49/68] conical surface data test --- tests/compas/geometry/test_surfaces_cone.py | 58 +++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/compas/geometry/test_surfaces_cone.py b/tests/compas/geometry/test_surfaces_cone.py index 3427383961b..e12b4b86c00 100644 --- a/tests/compas/geometry/test_surfaces_cone.py +++ b/tests/compas/geometry/test_surfaces_cone.py @@ -1,7 +1,65 @@ +import pytest +import json +import compas +from random import random + +from compas.geometry import Point # noqa: F401 +from compas.geometry import Vector # noqa: F401 +from compas.geometry import Frame +from compas.geometry import ConicalSurface +from compas.geometry import close + + # ============================================================================= # Constructors # ============================================================================= + +@pytest.mark.parametrize( + "radius,height", + [ + (0, 0), + (1, 0), + (0, 1), + (1, 1), + (random(), random()), + ], +) +def test_cone(radius, height): + cone = ConicalSurface(radius=radius, height=height) + + assert cone.radius == radius + assert cone.height == height + assert cone.frame == Frame.worldXY() + + other = eval(repr(cone)) + + assert close(cone.radius, other.radius, tol=1e-12) + assert close(cone.height, other.height, tol=1e-12) + assert cone.frame == other.frame + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_cone_data(): + radius = random() + height = random() + cone = ConicalSurface(radius=radius, height=height) + other = ConicalSurface.from_data(json.loads(json.dumps(cone.data))) + + assert cone.data == other.data + assert cone.radius == radius + assert cone.height == height + assert cone.frame == Frame.worldXY() + + if not compas.IPY: + assert ConicalSurface.validate_data(cone.data) + assert ConicalSurface.validate_data(other.data) + + # ============================================================================= # Properties and Geometry # ============================================================================= From 942fdd87ca9f781d3d85fe279634964c13e49093 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 09:10:34 +0200 Subject: [PATCH 50/68] cylindrical surface data test --- .../compas/geometry/test_surfaces_cylinder.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/compas/geometry/test_surfaces_cylinder.py b/tests/compas/geometry/test_surfaces_cylinder.py index 3427383961b..f63d157ec7a 100644 --- a/tests/compas/geometry/test_surfaces_cylinder.py +++ b/tests/compas/geometry/test_surfaces_cylinder.py @@ -1,7 +1,52 @@ +import pytest +import json +import compas +from random import random + +from compas.geometry import Point # noqa: F401 +from compas.geometry import Vector # noqa: F401 +from compas.geometry import Frame +from compas.geometry import CylindricalSurface +from compas.geometry import close + + # ============================================================================= # Constructors # ============================================================================= + +@pytest.mark.parametrize("radius", [0, 1, random()]) +def test_cylinder(radius): + cylinder = CylindricalSurface(radius) + + assert cylinder.radius == radius + assert cylinder.frame == Frame.worldXY() + + other = eval(repr(cylinder)) + + assert close(cylinder.radius, other.radius, tol=1e-12) + assert cylinder.frame == other.frame + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_cylinder_data(): + radius = random() + cylinder = CylindricalSurface(radius=radius) + other = CylindricalSurface.from_data(json.loads(json.dumps(cylinder.data))) + + assert cylinder.data == other.data + assert cylinder.radius == radius + assert cylinder.frame == Frame.worldXY() + + if not compas.IPY: + assert CylindricalSurface.validate_data(cylinder.data) + assert CylindricalSurface.validate_data(other.data) + + # ============================================================================= # Properties and Geometry # ============================================================================= From 5f2615a7de24d3a6101476338e3215212690b915 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 09:16:19 +0200 Subject: [PATCH 51/68] consistency with other surfaces --- src/compas/geometry/surfaces/planar.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/compas/geometry/surfaces/planar.py b/src/compas/geometry/surfaces/planar.py index 5ce511ab4d9..be14cc22e3f 100644 --- a/src/compas/geometry/surfaces/planar.py +++ b/src/compas/geometry/surfaces/planar.py @@ -94,26 +94,26 @@ def from_data(cls, data): @property def xsize(self): - if not self._xsize: + if self._xsize is None: raise ValueError("The size of the surface in the local X-direction is not set.") return self._xsize @xsize.setter def xsize(self, xsize): - if xsize <= 0: - raise ValueError("The size of the surface in the local X-direction should be larger than zero.") + if xsize < 0: + raise ValueError("The size of the surface in the local X-direction should be at least zero.") self._xsize = float(xsize) @property def ysize(self): - if not self._ysize: + if self._ysize is None: raise ValueError("The size of the surface in the local Y-direction is not set.") return self._ysize @ysize.setter def ysize(self, ysize): - if ysize <= 0: - raise ValueError("The size of the surface in the local Y-direction should be larger than zero.") + if ysize < 0: + raise ValueError("The size of the surface in the local Y-direction should be at least zero.") self._ysize = float(ysize) # ============================================================================= From 0a3cfa890bbed8e2296152397e49d9be427ffc14 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 09:20:17 +0200 Subject: [PATCH 52/68] planar surface data tests --- tests/compas/geometry/test_surfaces_plane.py | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/compas/geometry/test_surfaces_plane.py b/tests/compas/geometry/test_surfaces_plane.py index 3427383961b..d2b48db244a 100644 --- a/tests/compas/geometry/test_surfaces_plane.py +++ b/tests/compas/geometry/test_surfaces_plane.py @@ -1,7 +1,65 @@ +import pytest +import json +import compas +from random import random + +from compas.geometry import Point # noqa: F401 +from compas.geometry import Vector # noqa: F401 +from compas.geometry import Frame +from compas.geometry import PlanarSurface +from compas.geometry import close + + # ============================================================================= # Constructors # ============================================================================= + +@pytest.mark.parametrize( + "xsize,ysize", + [ + (0, 0), + (1, 0), + (0, 1), + (1, 1), + (random(), random()), + ], +) +def test_plane(xsize, ysize): + plane = PlanarSurface(xsize=xsize, ysize=ysize) + + assert plane.xsize == xsize + assert plane.ysize == ysize + assert plane.frame == Frame.worldXY() + + other = eval(repr(plane)) + + assert close(plane.xsize, other.xsize, tol=1e-12) + assert close(plane.ysize, other.ysize, tol=1e-12) + assert plane.frame == other.frame + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_plane_data(): + xsize = random() + ysize = random() + plane = PlanarSurface(xsize=xsize, ysize=ysize) + other = PlanarSurface.from_data(json.loads(json.dumps(plane.data))) + + assert plane.data == other.data + assert plane.xsize == xsize + assert plane.ysize == ysize + assert plane.frame == Frame.worldXY() + + if not compas.IPY: + assert PlanarSurface.validate_data(plane.data) + assert PlanarSurface.validate_data(other.data) + + # ============================================================================= # Properties and Geometry # ============================================================================= From fda3a0eef2af9e43de3500a546425a94feb86d97 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 09:27:38 +0200 Subject: [PATCH 53/68] consistency --- tests/compas/geometry/test_surfaces_cone.py | 9 ++++----- tests/compas/geometry/test_surfaces_cylinder.py | 9 ++++----- tests/compas/geometry/test_surfaces_plane.py | 9 ++++----- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/compas/geometry/test_surfaces_cone.py b/tests/compas/geometry/test_surfaces_cone.py index e12b4b86c00..c98ab6fa863 100644 --- a/tests/compas/geometry/test_surfaces_cone.py +++ b/tests/compas/geometry/test_surfaces_cone.py @@ -10,11 +10,6 @@ from compas.geometry import close -# ============================================================================= -# Constructors -# ============================================================================= - - @pytest.mark.parametrize( "radius,height", [ @@ -60,6 +55,10 @@ def test_cone_data(): assert ConicalSurface.validate_data(other.data) +# ============================================================================= +# Constructors +# ============================================================================= + # ============================================================================= # Properties and Geometry # ============================================================================= diff --git a/tests/compas/geometry/test_surfaces_cylinder.py b/tests/compas/geometry/test_surfaces_cylinder.py index f63d157ec7a..e9a89e2d241 100644 --- a/tests/compas/geometry/test_surfaces_cylinder.py +++ b/tests/compas/geometry/test_surfaces_cylinder.py @@ -10,11 +10,6 @@ from compas.geometry import close -# ============================================================================= -# Constructors -# ============================================================================= - - @pytest.mark.parametrize("radius", [0, 1, random()]) def test_cylinder(radius): cylinder = CylindricalSurface(radius) @@ -47,6 +42,10 @@ def test_cylinder_data(): assert CylindricalSurface.validate_data(other.data) +# ============================================================================= +# Constructors +# ============================================================================= + # ============================================================================= # Properties and Geometry # ============================================================================= diff --git a/tests/compas/geometry/test_surfaces_plane.py b/tests/compas/geometry/test_surfaces_plane.py index d2b48db244a..70f6dbccb48 100644 --- a/tests/compas/geometry/test_surfaces_plane.py +++ b/tests/compas/geometry/test_surfaces_plane.py @@ -10,11 +10,6 @@ from compas.geometry import close -# ============================================================================= -# Constructors -# ============================================================================= - - @pytest.mark.parametrize( "xsize,ysize", [ @@ -60,6 +55,10 @@ def test_plane_data(): assert PlanarSurface.validate_data(other.data) +# ============================================================================= +# Constructors +# ============================================================================= + # ============================================================================= # Properties and Geometry # ============================================================================= From b5536ad2932631fa731fb982677d1926064bde92 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 09:27:50 +0200 Subject: [PATCH 54/68] spherical surface data tests --- tests/compas/geometry/test_surfaces_sphere.py | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/tests/compas/geometry/test_surfaces_sphere.py b/tests/compas/geometry/test_surfaces_sphere.py index ba37d0b2a93..9e9f9a471ad 100644 --- a/tests/compas/geometry/test_surfaces_sphere.py +++ b/tests/compas/geometry/test_surfaces_sphere.py @@ -1,20 +1,39 @@ import pytest +import json +import compas +from random import random from compas.utilities import linspace +from compas.geometry import Point # noqa: F401 +from compas.geometry import Vector # noqa: F401 from compas.geometry import Frame from compas.geometry import SphericalSurface +from compas.geometry import close -def test_create_spherical_surface(): - surf = SphericalSurface(radius=1.0) +@pytest.mark.parametrize( + "radius", + [ + 0, + 1, + random(), + ], +) +def test_spherical_surface(radius): + surf = SphericalSurface(radius) - assert surf.radius == 1.0 + assert surf.radius == radius assert surf.frame == Frame.worldXY() for u in linspace(0.0, 1.0, num=100): for v in linspace(0.0, 1.0, num=100): assert surf.point_at(u, v) == surf.point_at(u, v, world=False) + other = eval(repr(surf)) + + assert close(surf.radius, other.radius, tol=1e-12) + assert surf.frame == other.frame + @pytest.mark.parametrize( "frame", @@ -24,7 +43,7 @@ def test_create_spherical_surface(): Frame.worldYZ(), ], ) -def test_create_spherical_surface_frame(frame): +def test_spherical_surface_with_frame(frame): surf = SphericalSurface(radius=1.0, frame=frame) assert surf.radius == 1.0 @@ -34,6 +53,30 @@ def test_create_spherical_surface_frame(frame): for v in linspace(0.0, 1.0, num=100): assert surf.point_at(u, v) == surf.point_at(u, v, world=False).transformed(surf.transformation) + other = eval(repr(surf)) + + assert close(surf.radius, other.radius, tol=1e-12) + assert surf.frame == other.frame + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_spherical_surface_data(): + radius = random() + surf = SphericalSurface(radius=radius) + other = SphericalSurface.from_data(json.loads(json.dumps(surf.data))) + + assert surf.data == other.data + assert surf.radius == radius + assert surf.frame == Frame.worldXY() + + if not compas.IPY: + assert SphericalSurface.validate_data(surf.data) + assert SphericalSurface.validate_data(other.data) + # ============================================================================= # Constructors From c21d36cb3cc26ec522dd403564ebc2000e03d5da Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 09:40:23 +0200 Subject: [PATCH 55/68] torus data schema --- src/compas/geometry/surfaces/toroidal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compas/geometry/surfaces/toroidal.py b/src/compas/geometry/surfaces/toroidal.py index e7187496a3c..7707851e1fb 100644 --- a/src/compas/geometry/surfaces/toroidal.py +++ b/src/compas/geometry/surfaces/toroidal.py @@ -37,7 +37,7 @@ class ToroidalSurface(Surface): "radius_pipe": {"type": "number", "minimum": 0}, "frame": Frame.DATASCHEMA, }, - "required": ["radius", "frame"], + "required": ["radius_axis", "radius_pipe", "frame"], } # overwriting the __new__ method is necessary From ec98118127b586e55c0d1b8d1f80f7abb26cfe0f Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 09:40:35 +0200 Subject: [PATCH 56/68] toroidal surface data tests --- tests/compas/geometry/test_surfaces_torus.py | 89 ++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/compas/geometry/test_surfaces_torus.py b/tests/compas/geometry/test_surfaces_torus.py index 3427383961b..02b9f516089 100644 --- a/tests/compas/geometry/test_surfaces_torus.py +++ b/tests/compas/geometry/test_surfaces_torus.py @@ -1,3 +1,92 @@ +import pytest +import json +import compas +from random import random + +from compas.utilities import linspace +from compas.geometry import Point # noqa: F401 +from compas.geometry import Vector # noqa: F401 +from compas.geometry import Frame +from compas.geometry import ToroidalSurface +from compas.geometry import close + + +@pytest.mark.parametrize( + "radius_axis, radius_pipe", + [ + (0, 0), + (1, 0), + (0, 1), + (1, 1), + (random(), random()), + ], +) +def test_torus(radius_axis, radius_pipe): + torus = ToroidalSurface(radius_axis=radius_axis, radius_pipe=radius_pipe) + + assert torus.radius_axis == radius_axis + assert torus.radius_pipe == radius_pipe + assert torus.frame == Frame.worldXY() + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert torus.point_at(u, v) == torus.point_at(u, v, world=False) + + other = eval(repr(torus)) + + assert close(torus.radius_axis, other.radius_axis, tol=1e-12) + assert close(torus.radius_pipe, other.radius_pipe, tol=1e-12) + assert torus.frame == other.frame + + +@pytest.mark.parametrize( + "frame", + [ + Frame.worldXY(), + Frame.worldZX(), + Frame.worldYZ(), + ], +) +def test_torus_with_frame(frame): + torus = ToroidalSurface(radius_axis=1.0, radius_pipe=1.0, frame=frame) + + assert torus.radius_axis == 1.0 + assert torus.radius_pipe == 1.0 + assert torus.frame == frame + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert torus.point_at(u, v) == torus.point_at(u, v, world=False).transformed(torus.transformation) + + other = eval(repr(torus)) + + assert close(torus.radius_axis, other.radius_axis, tol=1e-12) + assert close(torus.radius_pipe, other.radius_pipe, tol=1e-12) + assert torus.frame == other.frame + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_torus_data(): + radius_axis = random() + radius_pipe = random() + frame = Frame.worldXY() + + torus = ToroidalSurface(radius_axis=radius_axis, radius_pipe=radius_pipe, frame=frame) + other = ToroidalSurface.from_data(json.loads(json.dumps(torus.data))) + + assert torus.radius_axis == other.radius_axis + assert torus.radius_pipe == other.radius_pipe + assert torus.frame == frame + + if not compas.IPY: + assert ToroidalSurface.validate_data(torus.data) + assert ToroidalSurface.validate_data(other.data) + + # ============================================================================= # Constructors # ============================================================================= From 0cc64657fa262ae71bf09bc3d0c55339c969447b Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 09:52:51 +0200 Subject: [PATCH 57/68] reorganize --- src/compas/geometry/quaternion.py | 98 +++++++++++++++---------------- 1 file changed, 46 insertions(+), 52 deletions(-) diff --git a/src/compas/geometry/quaternion.py b/src/compas/geometry/quaternion.py index 67e2a1eda82..8c5f4927cb6 100644 --- a/src/compas/geometry/quaternion.py +++ b/src/compas/geometry/quaternion.py @@ -132,20 +132,57 @@ def __init__(self, w, x, y, z, **kwargs): self.y = y self.z = z + def __repr__(self): + return "{0}({1}, {2}, {3}, {4})".format(type(self).__name__, self.w, self.x, self.y, self.z) + + def __eq__(self, other, tol=1e-05): + if not hasattr(other, "__iter__") or not hasattr(other, "__len__") or len(self) != len(other): + return False + for v1, v2 in zip(self, other): + if math.fabs(v1 - v2) > tol: + return False + return True + + def __getitem__(self, key): + if key == 0: + return self.w + if key == 1: + return self.x + if key == 2: + return self.y + if key == 3: + return self.z + raise KeyError + + def __setitem__(self, key, value): + if key == 0: + self.w = value + return + if key == 1: + self.x = value + return + if key == 2: + self.y = value + if key == 3: + self.z = value + raise KeyError + + def __iter__(self): + return iter(self.wxyz) + + def __len__(self): + return 4 + # ========================================================================== - # data + # Data # ========================================================================== @property def data(self): return {"w": self.w, "x": self.x, "y": self.y, "z": self.z} - @classmethod - def from_data(cls, data): - return cls(data["w"], data["x"], data["y"], data["z"]) - # ========================================================================== - # properties + # Properties # ========================================================================== @property @@ -197,52 +234,9 @@ def is_unit(self): return quaternion_is_unit(self) # ========================================================================== - # customization + # Operators # ========================================================================== - def __getitem__(self, key): - if key == 0: - return self.w - if key == 1: - return self.x - if key == 2: - return self.y - if key == 3: - return self.z - raise KeyError - - def __setitem__(self, key, value): - if key == 0: - self.w = value - return - if key == 1: - self.x = value - return - if key == 2: - self.y = value - if key == 3: - self.z = value - raise KeyError - - def __eq__(self, other, tol=1e-05): - if not hasattr(other, "__iter__") or not hasattr(other, "__len__") or len(self) != len(other): - return False - for v1, v2 in zip(self, other): - if math.fabs(v1 - v2) > tol: - return False - return True - - def __iter__(self): - return iter(self.wxyz) - - def __len__(self): - return 4 - - def __repr__(self): - return "Quaternion({:.{prec}f}, {:.{prec}f}, {:.{prec}f}, {:.{prec}f})".format( - self.w, self.x, self.y, self.z, prec=3 - ) - def __mul__(self, other): """Multiply operator for two quaternions. @@ -276,7 +270,7 @@ def __mul__(self, other): return Quaternion(*p) # ========================================================================== - # constructors + # Constructors # ========================================================================== @classmethod @@ -354,7 +348,7 @@ def from_rotation(cls, R): return cls.from_matrix(R.matrix) # ========================================================================== - # methods + # Methods # ========================================================================== def unitize(self): From 3d89d5f3fde85a55ea02701f2a4062b4d32e27d7 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 09:52:58 +0200 Subject: [PATCH 58/68] quaternion data tests --- tests/compas/geometry/test_quaternion.py | 84 ++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/compas/geometry/test_quaternion.py diff --git a/tests/compas/geometry/test_quaternion.py b/tests/compas/geometry/test_quaternion.py new file mode 100644 index 00000000000..c42c057f11d --- /dev/null +++ b/tests/compas/geometry/test_quaternion.py @@ -0,0 +1,84 @@ +import pytest +import json +import compas +from random import random + +from compas.geometry import Quaternion +from compas.geometry import close + + +@pytest.mark.parametrize( + "x,y,z,w", + [ + (0.0, 0.0, 0.0, 0.0), + (0.0, 0.0, 0.0, 1.0), + (1.0, 0.0, 0.0, 0.0), + (1.0, 0.0, 0.0, 1.0), + (0.0, 1.0, 0.0, 0.0), + (0.0, 1.0, 0.0, 1.0), + (0.0, 0.0, 1.0, 0.0), + (0.0, 0.0, 1.0, 1.0), + (1.0, 1.0, 1.0, 0.0), + (1.0, 1.0, 1.0, 1.0), + (random(), random(), random(), random()), + ], +) +def test_quaternion(w, x, y, z): + quaternion = Quaternion(w, x, y, z) + + assert quaternion.w == w + assert quaternion.x == x + assert quaternion.y == y + assert quaternion.z == z + + other = eval(repr(quaternion)) + + assert close(quaternion.w, other.w, tol=1e-12) + assert close(quaternion.x, other.x, tol=1e-12) + assert close(quaternion.y, other.y, tol=1e-12) + assert close(quaternion.z, other.z, tol=1e-12) + + +# ============================================================================= +# Data +# ============================================================================= + + +def test_quaternion_data(): + x = random() + y = random() + z = random() + w = random() + + quaternion = Quaternion(w, x, y, z) + other = Quaternion.from_data(json.loads(json.dumps(quaternion.data))) + + assert quaternion.w == other.w + assert quaternion.x == other.x + assert quaternion.y == other.y + assert quaternion.z == other.z + + if not compas.IPY: + assert Quaternion.validate_data(quaternion.data) + assert Quaternion.validate_data(other.data) + + +# ============================================================================= +# Constructors +# ============================================================================= + +# ============================================================================= +# Properties and Geometry +# ============================================================================= + +# ============================================================================= +# Accessors +# ============================================================================= + +# ============================================================================= +# Comparison +# ============================================================================= + +# ============================================================================= +# Other Methods +# ============================================================================= From ac57aab96c5a81b13e7a9471faf79508ffa776ef Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 09:53:41 +0200 Subject: [PATCH 59/68] remove space for ipy --- tests/compas/geometry/test_surfaces_torus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/compas/geometry/test_surfaces_torus.py b/tests/compas/geometry/test_surfaces_torus.py index 02b9f516089..306d1aa9912 100644 --- a/tests/compas/geometry/test_surfaces_torus.py +++ b/tests/compas/geometry/test_surfaces_torus.py @@ -12,7 +12,7 @@ @pytest.mark.parametrize( - "radius_axis, radius_pipe", + "radius_axis,radius_pipe", [ (0, 0), (1, 0), From 7fb2649c6f76246b33956cc197d70156eccfea19 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Tue, 15 Aug 2023 12:04:03 +0200 Subject: [PATCH 60/68] add param tests --- tests/compas/geometry/test_surfaces_cone.py | 33 ++++++++++++++++ .../compas/geometry/test_surfaces_cylinder.py | 39 ++++++++++++++++++- tests/compas/geometry/test_surfaces_plane.py | 33 ++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/tests/compas/geometry/test_surfaces_cone.py b/tests/compas/geometry/test_surfaces_cone.py index c98ab6fa863..305cbd40369 100644 --- a/tests/compas/geometry/test_surfaces_cone.py +++ b/tests/compas/geometry/test_surfaces_cone.py @@ -8,6 +8,7 @@ from compas.geometry import Frame from compas.geometry import ConicalSurface from compas.geometry import close +from compas.utilities import linspace @pytest.mark.parametrize( @@ -27,6 +28,38 @@ def test_cone(radius, height): assert cone.height == height assert cone.frame == Frame.worldXY() + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert cone.point_at(u, v) == cone.point_at(u, v, world=False) + + other = eval(repr(cone)) + + assert close(cone.radius, other.radius, tol=1e-12) + assert close(cone.height, other.height, tol=1e-12) + assert cone.frame == other.frame + + +@pytest.mark.parametrize( + "frame", + [ + Frame.worldXY(), + Frame.worldZX(), + Frame.worldYZ(), + ], +) +def test_cone_frame(frame): + radius = random() + height = random() + cone = ConicalSurface(radius=radius, height=height, frame=frame) + + assert cone.radius == radius + assert cone.height == height + assert cone.frame == frame + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert cone.point_at(u, v) == cone.point_at(u, v, world=False).transformed(cone.transformation) + other = eval(repr(cone)) assert close(cone.radius, other.radius, tol=1e-12) diff --git a/tests/compas/geometry/test_surfaces_cylinder.py b/tests/compas/geometry/test_surfaces_cylinder.py index e9a89e2d241..9a0b5441d05 100644 --- a/tests/compas/geometry/test_surfaces_cylinder.py +++ b/tests/compas/geometry/test_surfaces_cylinder.py @@ -8,9 +8,17 @@ from compas.geometry import Frame from compas.geometry import CylindricalSurface from compas.geometry import close +from compas.utilities import linspace -@pytest.mark.parametrize("radius", [0, 1, random()]) +@pytest.mark.parametrize( + "radius", + [ + 0, + 1, + random(), + ], +) def test_cylinder(radius): cylinder = CylindricalSurface(radius) @@ -19,6 +27,35 @@ def test_cylinder(radius): other = eval(repr(cylinder)) + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert cylinder.point_at(u, v) == cylinder.point_at(u, v, world=False) + + assert close(cylinder.radius, other.radius, tol=1e-12) + assert cylinder.frame == other.frame + + +@pytest.mark.parametrize( + "frame", + [ + Frame.worldXY(), + Frame.worldZX(), + Frame.worldYZ(), + ], +) +def test_cylinder_frame(frame): + radius = random() + cylinder = CylindricalSurface(radius, frame) + + assert cylinder.radius == radius + assert cylinder.frame == frame + + other = eval(repr(cylinder)) + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert cylinder.point_at(u, v) == cylinder.point_at(u, v, world=False).transformed(cylinder.transformation) + assert close(cylinder.radius, other.radius, tol=1e-12) assert cylinder.frame == other.frame diff --git a/tests/compas/geometry/test_surfaces_plane.py b/tests/compas/geometry/test_surfaces_plane.py index 70f6dbccb48..ed2903c954b 100644 --- a/tests/compas/geometry/test_surfaces_plane.py +++ b/tests/compas/geometry/test_surfaces_plane.py @@ -8,6 +8,7 @@ from compas.geometry import Frame from compas.geometry import PlanarSurface from compas.geometry import close +from compas.utilities import linspace @pytest.mark.parametrize( @@ -27,6 +28,38 @@ def test_plane(xsize, ysize): assert plane.ysize == ysize assert plane.frame == Frame.worldXY() + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert plane.point_at(u, v) == plane.point_at(u, v, world=False) + + other = eval(repr(plane)) + + assert close(plane.xsize, other.xsize, tol=1e-12) + assert close(plane.ysize, other.ysize, tol=1e-12) + assert plane.frame == other.frame + + +@pytest.mark.parametrize( + "frame", + [ + Frame.worldXY(), + Frame.worldZX(), + Frame.worldYZ(), + ], +) +def test_plane_frame(frame): + xsize = random() + ysize = random() + plane = PlanarSurface(xsize=xsize, ysize=ysize, frame=frame) + + assert plane.xsize == xsize + assert plane.ysize == ysize + assert plane.frame == frame + + for u in linspace(0.0, 1.0, num=100): + for v in linspace(0.0, 1.0, num=100): + assert plane.point_at(u, v) == plane.point_at(u, v, world=False).transformed(plane.transformation) + other = eval(repr(plane)) assert close(plane.xsize, other.xsize, tol=1e-12) From b8f3b8413b71ee3d772d59d51ee6103d4459c620 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Wed, 16 Aug 2023 09:19:55 +0200 Subject: [PATCH 61/68] fixed bug in insert vertex --- CHANGELOG.md | 1 + src/compas/datastructures/mesh/mesh.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b135f9568b6..1bd3009fd5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -172,6 +172,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Changed data property of `compas.datastructures.Halfface` to contain only JSON compatible data. * Changed `__repr__` of `compas.geometry.Point` and `compas.geometry.Vector` to not use limited precision (`compas.PRECISION`) to ensure proper object reconstruction through `eval(repr(point))`. * Changed `compas.datastructures.Graph.delete_edge` to delete invalid (u, u) edges and not delete edges in opposite directions (v, u) +* Fixed bug in `compas.datastructures.Mesh.insert_vertex`. ### Removed diff --git a/src/compas/datastructures/mesh/mesh.py b/src/compas/datastructures/mesh/mesh.py index 0b0990acc5f..b6fa26b3ac4 100644 --- a/src/compas/datastructures/mesh/mesh.py +++ b/src/compas/datastructures/mesh/mesh.py @@ -794,7 +794,7 @@ def insert_vertex(self, fkey, key=None, xyz=None, return_fkeys=False): w = self.add_vertex(key=key, x=x, y=y, z=z) for u, v in self.face_halfedges(fkey): fkeys.append(self.add_face([u, v, w])) - del self.face[fkey] + self.delete_face(fkey) if return_fkeys: return w, fkeys return w From f670d55b78e0e7edfe506c1107040fcf2885d3e5 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Wed, 16 Aug 2023 09:20:17 +0200 Subject: [PATCH 62/68] simplified edgedata recovery --- .../datastructures/halfedge/halfedge.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/compas/datastructures/halfedge/halfedge.py b/src/compas/datastructures/halfedge/halfedge.py index f91de5cecf0..07e550986a8 100644 --- a/src/compas/datastructures/halfedge/halfedge.py +++ b/src/compas/datastructures/halfedge/halfedge.py @@ -169,7 +169,7 @@ def from_data(cls, data): attr = facedata.get(fkey) or {} halfedge.add_face(vertices, fkey=fkey, attr_dict=attr) - halfedge.edgedata.update(edgedata) + halfedge.edgedata = edgedata halfedge._max_vertex = data.get("max_vertex", halfedge._max_vertex) halfedge._max_face = data.get("max_face", halfedge._max_face) @@ -500,13 +500,17 @@ def delete_face(self, fkey): """ for u, v in self.face_halfedges(fkey): - self.halfedge[u][v] = None - if self.halfedge[v][u] is None: - del self.halfedge[u][v] - del self.halfedge[v][u] - edge = "-".join(map(str, sorted([u, v]))) - if edge in self.edgedata: - del self.edgedata[edge] + if self.halfedge[u][v] == fkey: + # if the halfedge still points to the face + # this might not be the case of the deletion is executed + # during the procedure of adding a new (replacement) face + self.halfedge[u][v] = None + if self.halfedge[v][u] is None: + del self.halfedge[u][v] + del self.halfedge[v][u] + edge = "-".join(map(str, sorted([u, v]))) + if edge in self.edgedata: + del self.edgedata[edge] del self.face[fkey] if fkey in self.facedata: del self.facedata[fkey] From 85b8ebce1b552df9f1372520ab58ea4d66e58ff1 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Wed, 16 Aug 2023 09:20:33 +0200 Subject: [PATCH 63/68] data test mesh --- tests/compas/datastructures/test_mesh.py | 37 ++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/compas/datastructures/test_mesh.py b/tests/compas/datastructures/test_mesh.py index a69d9c39fd0..b87f2fa150a 100644 --- a/tests/compas/datastructures/test_mesh.py +++ b/tests/compas/datastructures/test_mesh.py @@ -1,8 +1,8 @@ import tempfile - import pytest - +import json import compas + from compas.datastructures import Mesh from compas.datastructures import meshes_join_and_weld from compas.geometry import Box @@ -173,6 +173,39 @@ def test_from_ploygons(): assert mesh.number_of_edges() == 5 +# -------------------------------------------------------------------------- +# data +# -------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "mesh", + [ + "tet", + "cube", + "box", + "hexagon", + "hexagongrid", + "triangleboundarychain", + ], +) +def test_mesh_data(mesh, request): + mesh = request.getfixturevalue(mesh) + other = Mesh.from_data(json.loads(json.dumps(mesh.data))) + + assert mesh.data == other.data + assert mesh.default_vertex_attributes == other.default_vertex_attributes + assert mesh.default_edge_attributes == other.default_edge_attributes + assert mesh.default_face_attributes == other.default_face_attributes + assert mesh.number_of_vertices() == other.number_of_vertices() + assert mesh.number_of_edges() == other.number_of_edges() + assert mesh.number_of_faces() == other.number_of_faces() + + if not compas.IPY: + assert Mesh.validate_data(mesh.data) + assert Mesh.validate_data(other.data) + + # -------------------------------------------------------------------------- # converters # -------------------------------------------------------------------------- From ff904c28a2ef498f22bb84fd94beec13e0869c3d Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Wed, 16 Aug 2023 09:56:06 +0200 Subject: [PATCH 64/68] request doesn't work in ipy --- tests/compas/datastructures/test_mesh.py | 60 +++++++++++++++++------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/tests/compas/datastructures/test_mesh.py b/tests/compas/datastructures/test_mesh.py index b87f2fa150a..065d86aa1c5 100644 --- a/tests/compas/datastructures/test_mesh.py +++ b/tests/compas/datastructures/test_mesh.py @@ -3,6 +3,7 @@ import json import compas + from compas.datastructures import Mesh from compas.datastructures import meshes_join_and_weld from compas.geometry import Box @@ -12,24 +13,35 @@ from compas.geometry import allclose -@pytest.fixture -def tet(): +def _tet(): return Mesh.from_polyhedron(4) @pytest.fixture -def cube(): +def tet(): + return _tet() + + +def _cube(): return Mesh.from_polyhedron(6) @pytest.fixture -def box(): +def cube(): + return _cube() + + +def _box(): box = Box.from_width_height_depth(2, 2, 2) return Mesh.from_shape(box) @pytest.fixture -def hexagon(): +def box(): + return _box() + + +def _hexagon(): polygon = Polygon.from_sides_and_radius_xy(6, 1) vertices = polygon.points vertices.append(polygon.centroid) @@ -38,7 +50,11 @@ def hexagon(): @pytest.fixture -def hexagongrid(): +def hexagon(): + return _hexagon() + + +def _hexagongrid(): polygon = Polygon.from_sides_and_radius_xy(6, 1) vertices = polygon.points vertices.append(polygon.centroid) @@ -61,7 +77,11 @@ def hexagongrid(): @pytest.fixture -def biohazard(): +def hexagongrid(): + return _hexagongrid() + + +def _biohazard(): polygon = Polygon.from_sides_and_radius_xy(6, 1) vertices = polygon.points vertices.append(polygon.centroid) @@ -70,7 +90,11 @@ def biohazard(): @pytest.fixture -def triangleboundarychain(): +def biohazard(): + return _biohazard() + + +def _triangleboundarychain(): mesh = Mesh.from_obj(compas.get("faces.obj")) faces = mesh.faces_on_boundaries()[0] for face in faces: @@ -78,6 +102,11 @@ def triangleboundarychain(): return mesh +@pytest.fixture +def triangleboundarychain(): + return _triangleboundarychain() + + # -------------------------------------------------------------------------- # constructors # -------------------------------------------------------------------------- @@ -181,16 +210,15 @@ def test_from_ploygons(): @pytest.mark.parametrize( "mesh", [ - "tet", - "cube", - "box", - "hexagon", - "hexagongrid", - "triangleboundarychain", + _tet(), + _cube(), + _box(), + _hexagon(), + _hexagongrid(), + _triangleboundarychain(), ], ) -def test_mesh_data(mesh, request): - mesh = request.getfixturevalue(mesh) +def test_mesh_data(mesh): other = Mesh.from_data(json.loads(json.dumps(mesh.data))) assert mesh.data == other.data From 4c587ad570b47066a5d20a96cf5c231e673a2b79 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Wed, 16 Aug 2023 10:35:44 +0200 Subject: [PATCH 65/68] network data tests --- tests/compas/datastructures/test_network.py | 92 +++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/tests/compas/datastructures/test_network.py b/tests/compas/datastructures/test_network.py index 6c6c38e55a3..b911974b27f 100644 --- a/tests/compas/datastructures/test_network.py +++ b/tests/compas/datastructures/test_network.py @@ -1,6 +1,15 @@ import pytest +import compas +import json +from random import random from compas.datastructures import Network +from compas.geometry import Pointcloud + + +# ============================================================================== +# Fixtures +# ============================================================================== @pytest.fixture @@ -23,6 +32,68 @@ def k5_network(): return network +# ============================================================================== +# Basics +# ============================================================================== + +# ============================================================================== +# Constructors +# ============================================================================== + + +@pytest.mark.parametrize( + "filepath", + [ + compas.get("lines.obj"), + compas.get("grid_irregular.obj"), + ], +) +def test_network_from_obj(filepath): + network = Network.from_obj(filepath) + assert network.number_of_nodes() > 0 + assert network.number_of_edges() > 0 + assert len(list(network.nodes())) == network._max_node + 1 + assert network.is_connected() + + +def test_network_from_pointcloud(): + cloud = Pointcloud.from_bounds(random(), random(), random(), int(100 * random())) + network = Network.from_pointcloud(cloud=cloud, degree=3) + assert network.number_of_nodes() == len(cloud) + for node in network.nodes(): + assert network.degree(node) >= 3 + + +# ============================================================================== +# Data +# ============================================================================== + + +def test_network_data(): + cloud = Pointcloud.from_bounds(random(), random(), random(), int(100 * random())) + network = Network.from_pointcloud(cloud=cloud, degree=3) + other = Network.from_data(json.loads(json.dumps(network.data))) + + assert network.data == other.data + + if not compas.IPY: + assert Network.validate_data(network.data) + assert Network.validate_data(other.data) + + +# ============================================================================== +# Properties +# ============================================================================== + +# ============================================================================== +# Accessors +# ============================================================================== + +# ============================================================================== +# Builders +# ============================================================================== + + def test_add_node(): network = Network() assert network.add_node(1) == 1 @@ -31,6 +102,27 @@ def test_add_node(): assert network.add_node(0, x=1) == 0 +# ============================================================================== +# Modifiers +# ============================================================================== + +# ============================================================================== +# Samples +# ============================================================================== + +# ============================================================================== +# Attributes +# ============================================================================== + +# ============================================================================== +# Conversion +# ============================================================================== + +# ============================================================================== +# Methods +# ============================================================================== + + def test_non_planar(k5_network): try: import planarity # noqa: F401 From 94fd7cb5ec731d07953969b2243053a994b15ea0 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Wed, 16 Aug 2023 10:43:24 +0200 Subject: [PATCH 66/68] use randint for random count --- tests/compas/datastructures/test_network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/compas/datastructures/test_network.py b/tests/compas/datastructures/test_network.py index b911974b27f..469846fe808 100644 --- a/tests/compas/datastructures/test_network.py +++ b/tests/compas/datastructures/test_network.py @@ -1,7 +1,7 @@ import pytest import compas import json -from random import random +from random import random, randint from compas.datastructures import Network from compas.geometry import Pointcloud @@ -57,7 +57,7 @@ def test_network_from_obj(filepath): def test_network_from_pointcloud(): - cloud = Pointcloud.from_bounds(random(), random(), random(), int(100 * random())) + cloud = Pointcloud.from_bounds(random(), random(), random(), randint(10, 100)) network = Network.from_pointcloud(cloud=cloud, degree=3) assert network.number_of_nodes() == len(cloud) for node in network.nodes(): @@ -70,7 +70,7 @@ def test_network_from_pointcloud(): def test_network_data(): - cloud = Pointcloud.from_bounds(random(), random(), random(), int(100 * random())) + cloud = Pointcloud.from_bounds(random(), random(), random(), randint(10, 100)) network = Network.from_pointcloud(cloud=cloud, degree=3) other = Network.from_data(json.loads(json.dumps(network.data))) From 6f2d7c02a68066568dea355e8296c5386b9d9248 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Wed, 16 Aug 2023 11:12:28 +0200 Subject: [PATCH 67/68] basic volmesh data test --- tests/compas/datastructures/test_volmesh.py | 66 ++++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/tests/compas/datastructures/test_volmesh.py b/tests/compas/datastructures/test_volmesh.py index ad4abb338f9..456ede4f05c 100644 --- a/tests/compas/datastructures/test_volmesh.py +++ b/tests/compas/datastructures/test_volmesh.py @@ -1,19 +1,67 @@ +import json import compas from compas.datastructures import VolMesh +# ============================================================================== +# Fixtures +# ============================================================================== + +# ============================================================================== +# Basics +# ============================================================================== + +# ============================================================================== +# Constructors +# ============================================================================== + +# ============================================================================== +# Data +# ============================================================================== + def test_volmesh_data(): - vmesh1 = VolMesh.from_obj(compas.get("boxes.obj")) + vmesh = VolMesh.from_obj(compas.get("boxes.obj")) + other = VolMesh.from_data(json.loads(json.dumps(vmesh.data))) + + assert vmesh.data == other.data + assert vmesh.number_of_vertices() == other.number_of_vertices() + assert vmesh.number_of_edges() == other.number_of_edges() + assert vmesh.number_of_faces() == other.number_of_faces() + assert vmesh.number_of_cells() == other.number_of_cells() + + if not compas.IPY: + assert VolMesh.validate_data(vmesh.data) + assert VolMesh.validate_data(other.data) + + +# ============================================================================== +# Properties +# ============================================================================== + +# ============================================================================== +# Accessors +# ============================================================================== + +# ============================================================================== +# Builders +# ============================================================================== - data1 = vmesh1.to_data() - data1_ = vmesh1.to_data() +# ============================================================================== +# Modifiers +# ============================================================================== - assert data1 == data1_ +# ============================================================================== +# Samples +# ============================================================================== - vmesh2 = VolMesh.from_data(data1_) +# ============================================================================== +# Attributes +# ============================================================================== - data2 = vmesh2.to_data() - data2_ = vmesh2.to_data() +# ============================================================================== +# Conversion +# ============================================================================== - assert data2 == data2_ - assert data1 == data2 +# ============================================================================== +# Methods +# ============================================================================== From 5a9a22ecf07b66b769d3f66fe7449816ce229745 Mon Sep 17 00:00:00 2001 From: tomvanmele Date: Wed, 16 Aug 2023 13:44:38 +0200 Subject: [PATCH 68/68] add features to part data --- src/compas/datastructures/assembly/part.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/compas/datastructures/assembly/part.py b/src/compas/datastructures/assembly/part.py index 417fbc1684b..323a794884b 100644 --- a/src/compas/datastructures/assembly/part.py +++ b/src/compas/datastructures/assembly/part.py @@ -158,6 +158,7 @@ class Part(Datastructure): "attributes": {"type": "object"}, "key": {"type": ["integer", "string"]}, "frame": Frame.DATASCHEMA, + "features": {"type": "array"}, }, "required": ["key", "frame"], } @@ -170,13 +171,13 @@ def __init__(self, name=None, frame=None, **kwargs): self.frame = frame or Frame.worldXY() self.features = [] - # features are not included here?! @property def data(self): return { "attributes": self.attributes, "key": self.key, "frame": self.frame.data, + "features": self.features, } @classmethod @@ -185,6 +186,7 @@ def from_data(cls, data): part.attributes.update(data["attributes"] or {}) part.key = data["key"] part.frame = Frame.from_data(data["frame"]) + part.features = data["features"] or [] return part def get_geometry(self, with_features=False):