Skip to content

Commit e9186c2

Browse files
committed
Merge remote-tracking branch 'compas-dev/main' into main
2 parents 3eae1ec + e3c7f00 commit e9186c2

File tree

58 files changed

+594
-3306
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+594
-3306
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 1.15.0
2+
current_version = 1.15.1
33
message = Bump version to {new_version}
44
commit = True
55
tag = True

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
1011
### Added
1112

13+
* Added `Polyline.extend`, `Polyline.extended`, `Polyline.shorten`, `Polyline.shortened`.
14+
* Added `Data.sha256` for computing a hash value of data objects, for example for comparisons during version control.
15+
* Added optional `path` parameter to `compas.rpc.Proxy` to allow for non-package calls.
16+
* Added Grasshopper component to call RPC functions.
17+
18+
### Changed
19+
20+
* Set `jinja >= 3.0` to dev dependencies to fix docs build error.
21+
22+
### Removed
23+
24+
* Removed unused `compas_rhino.objects` (moved to `compas_ui`).
25+
* Removed unused `compas_rhino.ui` (moved to `compas_ui`).
26+
27+
28+
## [1.15.1] 2022-03-28
29+
30+
### Added
31+
32+
* Added optional `triangulated` flag to `Mesh.to_vertices_and_faces`.
33+
* Added geometry information of active meshes to the serialization/deserialization of robot model's `MeshDescriptor`.
34+
* Added Grasshopper component to draw any COMPAS object.
35+
* Added new icons to Grasshopper components and default to icon style.
36+
1237
### Changed
1338

39+
<<<<<<< HEAD
1440
* Fixed bug in `normal_polygon` in `compas.geometry`.
41+
=======
42+
* Fixed bug in Blender mesh conversion.
43+
* Changed Rhino plugin installer to check for and install required plugin packages.
44+
* Refactor robot model artists to use the same `Mesh.to_vertices_and_faces` everywhere.
45+
* Fix debug print on Blender artist.
46+
>>>>>>> main
1547
1648
### Removed
1749

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def patched_m2r2_setup(app):
4040
copyright = "Block Research Group - ETH Zurich"
4141
author = "Tom Van Mele"
4242

43-
release = "1.15.0"
43+
release = "1.15.1"
4444
version = ".".join(release.split(".")[0:2])
4545

4646
master_doc = "index"

docs/tutorial/rpc.rst

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,11 @@
22
Remote Procedure Calls
33
********************************************************************************
44

5-
* :mod:`compas.rpc`
6-
75
Remote Procedure Calls (``RPC``) is a mechanism to transparently execute code in
86
a remote process or computer. This is especially useful in scenarios where COMPAS
97
runs inside an IronPython host (eg. Rhino) and needs to execute code that only
108
runs on CPython (eg. code that requires ``numpy``).
119

12-
COMPAS provides **two ways to achieve** this: ``rcp`` and ``XFunc``.
13-
14-
Through ``Xfunc``, COMPAS provides a mechanism for calling Python functions through
15-
a separately launched sub-process.
16-
17-
A drawback of the ``Xfunc`` mechanism is that every call launches a new Python
18-
(sub)process with all the overhead that that entails. For infrequent calls to
19-
long-running processes this is not an issue. However, for frequent calls to functions
20-
that are expected to run quickly, this is not ideal.
21-
22-
The second mechanism is the ``rpc`` module. The principle of RPC is to start a server
23-
that handles all requests. The advantage is that once the server is started,
24-
no additional processes have to be launched and the server can handle the requests
25-
without any overhead. Therefore, the response time is much faster than with ``XFunc``.
26-
2710

2811
Basic Usage
2912
===========
@@ -64,6 +47,70 @@ The use of :mod:`compas.rpc` is not restricted to COMPAS packages only. ::
6447
Note that Numpy arrays are automatically converted to lists.
6548

6649

50+
Configuration Options
51+
=====================
52+
53+
The :class:`compas.rpc.Proxy` object has several configuration options.
54+
We will discuss only a few of those here.
55+
For a complete overview, please refer to the API docs (:mod:`compas.rpc`).
56+
57+
``python``
58+
----------
59+
60+
The :class:`compas.rpc.Proxy` object will automatically try to reconnect to an
61+
active instance of the command server, or start a new one if no active server can be found.
62+
By default, a newly started server will run an instance of the default Python interpreter
63+
of the active environment, for example, when running RPC from the command line;
64+
or of the Python interpreter specified in `compas_bootstrapper`, for example, when running RPC from Rhino.
65+
66+
In some cases, this might not be what you want, or might not result in the expected behaviour,
67+
for example when `compas_bootstrapper` does not exist.
68+
69+
To use a specific Python iterpreter, you can specify the path to an executable through the ``python`` parameter.
70+
71+
.. code-block:: python
72+
73+
>>> from compas.rpc import Proxy
74+
>>> proxy = Proxy(python=r"C:\\Users\\<username>\\anaconda3\\envs\\research\\python.exe")
75+
76+
77+
``path``
78+
--------
79+
80+
Sometimes you will want the server to run custom functions that are not (yet) part of a specific package.
81+
To allow the server to find such functions, you can specify an additional search path.
82+
83+
For example, if you have a Python script on your desktop,
84+
defining a wrapper for the k-means clustering algorithm of ``scikit-learn``,
85+
you can tell the command server where to find it using the ``path`` parameter.
86+
87+
.. code-block:: python
88+
89+
# C:\Users\<username>\Desktop\clustering.py
90+
91+
from sklearn.cluster import KMeans
92+
from numpy import array
93+
94+
95+
def cluster(points, n_clusters):
96+
kmeans = KMeans(n_clusters=n_clusters, n_init=2000, max_iter=1000).fit(array(cloud, dtype=float))
97+
clusters = {}
98+
for label, point in zip(kmeans.labels_, cloud):
99+
if label not in clusters:
100+
clusters[label] = []
101+
clusters[label].append(point)
102+
return clusters
103+
104+
105+
.. code-block:: python
106+
107+
>>> from compas.geometry import Pointcloud
108+
>>> from compas.rpc import Proxy
109+
>>> cloud = Pointcloud.from_bounds(10, 5, 3, 100)
110+
>>> proxy = Proxy(package='clustering', path=r'C:\\Users\\<username>\\Desktop')
111+
>>> clusters = proxy.cluster(cloud, 10)
112+
113+
67114
Supported data types
68115
====================
69116

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ sphinx_compas_theme >=0.15.18
1717
sphinx >=3.4
1818
twine
1919
wheel
20+
jinja2 >= 3.0
2021
-e .

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def read(*names, **kwargs):
2525

2626
setup(
2727
name='COMPAS',
28-
version='1.15.0',
28+
version='1.15.1',
2929
description='The COMPAS framework',
3030
long_description=long_description,
3131
long_description_content_type='text/markdown',

src/compas/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
__license__ = 'MIT License'
4242
__email__ = '[email protected]'
4343

44-
__version__ = '1.15.0'
44+
__version__ = '1.15.1'
4545

4646
version = LooseVersion(compas.__version__)
4747
versionstring = version.vstring.split('-')[0]

src/compas/data/data.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import os
66
import json
7+
import hashlib
78
from uuid import uuid4
89
from copy import deepcopy
910

@@ -112,12 +113,12 @@ def __init__(self, name=None):
112113

113114
def __getstate__(self):
114115
"""Return the object data for state serialization with older pickle protocols."""
115-
return {'__dict__': self.__dict__, 'dtype': self.dtype, 'data': self.data}
116+
return {"__dict__": self.__dict__, "dtype": self.dtype, "data": self.data}
116117

117118
def __setstate__(self, state):
118119
"""Assign a deserialized state to the object data to support older pickle protocols."""
119-
self.__dict__.update(state['__dict__'])
120-
self.data = state['data']
120+
self.__dict__.update(state["__dict__"])
121+
self.data = state["data"]
121122

122123
@property
123124
def DATASCHEMA(self):
@@ -133,18 +134,22 @@ def JSONSCHEMANAME(self):
133134
def JSONSCHEMA(self):
134135
"""dict : The schema of the JSON representation of the data of this object."""
135136
if not self._JSONSCHEMA:
136-
schema_filename = '{}.json'.format(self.JSONSCHEMANAME.lower())
137-
schema_path = os.path.join(os.path.dirname(__file__), 'schemas', schema_filename)
138-
with open(schema_path, 'r') as fp:
137+
schema_filename = "{}.json".format(self.JSONSCHEMANAME.lower())
138+
schema_path = os.path.join(
139+
os.path.dirname(__file__), "schemas", schema_filename
140+
)
141+
with open(schema_path, "r") as fp:
139142
self._JSONSCHEMA = json.load(fp)
140143
return self._JSONSCHEMA
141144

142145
@property
143146
def jsondefinitions(self):
144147
"""dict : Reusable schema definitions."""
145148
if not self._jsondefinitions:
146-
schema_path = os.path.join(os.path.dirname(__file__), 'schemas', 'compas.json')
147-
with open(schema_path, 'r') as fp:
149+
schema_path = os.path.join(
150+
os.path.dirname(__file__), "schemas", "compas.json"
151+
)
152+
with open(schema_path, "r") as fp:
148153
self._jsondefinitions = json.load(fp)
149154
return self._jsondefinitions
150155

@@ -153,13 +158,16 @@ def jsonvalidator(self):
153158
"""jsonschema.Draft7Validator : JSON schema validator for draft 7."""
154159
if not self._jsonvalidator:
155160
from jsonschema import RefResolver, Draft7Validator
161+
156162
resolver = RefResolver.from_schema(self.jsondefinitions)
157163
self._jsonvalidator = Draft7Validator(self.JSONSCHEMA, resolver=resolver)
158164
return self._jsonvalidator
159165

160166
@property
161167
def dtype(self):
162-
return '{}/{}'.format('.'.join(self.__class__.__module__.split('.')[:2]), self.__class__.__name__)
168+
return "{}/{}".format(
169+
".".join(self.__class__.__module__.split(".")[:2]), self.__class__.__name__
170+
)
163171

164172
@property
165173
def data(self):
@@ -321,6 +329,7 @@ def validate_data(self):
321329
322330
"""
323331
import schema
332+
324333
try:
325334
data = self.DATASCHEMA.validate(self.data)
326335
except schema.SchemaError as e:
@@ -342,6 +351,7 @@ def validate_json(self):
342351
343352
"""
344353
import jsonschema
354+
345355
jsonstring = json.dumps(self.data, cls=DataEncoder)
346356
jsondata = json.loads(jsonstring, cls=DataDecoder)
347357
try:
@@ -350,3 +360,35 @@ def validate_json(self):
350360
print("Validation against the JSON schema of this object failed.")
351361
raise e
352362
return jsonstring
363+
364+
def sha256(self, as_string=False):
365+
"""Compute a hash of the data for comparison during version control using the sha256 algorithm.
366+
367+
Parameters
368+
----------
369+
as_string : bool, optional
370+
If True, return the digest in hexadecimal format rather than as bytes.
371+
372+
Returns
373+
-------
374+
bytes | str
375+
376+
Examples
377+
--------
378+
>>> from compas.datastructures import Mesh
379+
>>> mesh = Mesh.from_obj(compas.get('faces.obj'))
380+
>>> v1 = mesh.sha256()
381+
>>> v2 = mesh.sha256()
382+
>>> mesh.vertex_attribute(mesh.vertex_sample(1)[0], 'z', 1)
383+
>>> v3 = mesh.sha256()
384+
>>> v1 == v2
385+
True
386+
>>> v2 == v3
387+
False
388+
389+
"""
390+
h = hashlib.sha256()
391+
h.update(self.jsonstring.encode())
392+
if as_string:
393+
return h.hexdigest()
394+
return h.digest()

src/compas/datastructures/mesh/mesh.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,9 +473,14 @@ def from_vertices_and_faces(cls, vertices, faces):
473473

474474
return mesh
475475

476-
def to_vertices_and_faces(self):
476+
def to_vertices_and_faces(self, triangulated=False):
477477
"""Return the vertices and faces of a mesh.
478478
479+
Parameters
480+
----------
481+
triangulated: bool, optional
482+
If True, triangulate the faces.
483+
479484
Returns
480485
-------
481486
list[list[float]]
@@ -486,7 +491,31 @@ def to_vertices_and_faces(self):
486491
"""
487492
key_index = self.key_index()
488493
vertices = [self.vertex_coordinates(key) for key in self.vertices()]
489-
faces = [[key_index[key] for key in self.face_vertices(fkey)] for fkey in self.faces()]
494+
495+
if not triangulated:
496+
faces = [[key_index[key] for key in self.face_vertices(fkey)] for fkey in self.faces()]
497+
return vertices, faces
498+
499+
faces = []
500+
501+
for fkey in self.faces():
502+
face_vertices = self.face_vertices(fkey)
503+
504+
if len(face_vertices) == 3:
505+
a, b, c = face_vertices
506+
faces.append([key_index[a], key_index[b], key_index[c]])
507+
elif len(face_vertices) == 4:
508+
a, b, c, d = face_vertices
509+
faces.append([key_index[a], key_index[b], key_index[c]])
510+
faces.append([key_index[a], key_index[c], key_index[d]])
511+
else:
512+
centroid = centroid_polygon([vertices[key_index[key]] for key in face_vertices])
513+
ckey = len(vertices)
514+
vertices.append(centroid)
515+
516+
for a, b in pairwise(face_vertices + face_vertices[:1]):
517+
faces.append([key_index[a], key_index[b], ckey])
518+
490519
return vertices, faces
491520

492521
@classmethod

0 commit comments

Comments
 (0)