在这个教程中,我们将使用 Speckle 数据并使用它来创建一个超级简单的仪表板。 我们将从Speckle流中接收几何图形,更新数据,并使用它来使用 Plotly 和 Dash 进行一些计算和简单绘图。
我们假设你具有 Python 和 Speckle 的一般知识。 如果有任何问题让你感到困惑,请回顾一下 Python 示例或 Speckle 概念。
如果您想跟随代码,可以在此处找到该项目的仓库。
1、从服务器接收对象
如果你已经学习过 Python 示例,就会知道如何从服务器接收对象。
作为复习,你需要创建一个 SpeckleClient 作为 API 的入口点。 然后,我们使用来自本地帐户的令牌对该客户端进行身份验证。 如果你还没有使用管理器添加本地帐户,可以转到 your-server.com/profile 并创建一个个人访问令牌以在此处使用。 然后我们将使用这个客户端来获取感兴趣的提交。
在这种情况下,我们将查看 Alan 在 Grasshopper 中生成并使用 Grasshopper Connector 发送到 Speckle 的弯曲建筑。 它有 10 个级别,分为单独的对象,每个对象包含立面、柱子、栏杆和楼板的字段。
下面的代码片段显示了如何验证客户端、获取我们感兴趣的提交以及使用服务器传输来接收提交对象。
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_default_account
from specklepy.transports.server import ServerTransport
# create and authenticate a client
client = SpeckleClient(host=HOST)
account = get_default_account()
client.authenticate_with_account(account)
# get the specified commit data
commit = client.commit.get(STREAM_ID, COMMIT_ID)
# create an authenticated server transport from the client and receive the commit obj
transport = ServerTransport(client=client, stream_id=STREAM_ID)
res = operations.receive(commit.referencedObject, transport)
# get the list of levels from the received object
levels = res["data"]
2、更新现有对象
现在我们已经从 Grasshopper 收到了这个建筑,让我们做一些修改。 数据已经被很好地组织到一个 Base 对象列表中,每个对象代表一个级别。 每个级别都有包含该级别的立面、柱、栏杆和楼板的属性。
{
"id": "idfcaf8b9e145241dsdfa915885d87cda2",
"speckle_type": "Base",
"data": [
{
"id": "ide6acabd37e865ce87a5sdf444d733877",
"speckle_type": "Base",
"@facade": [ { ... }, ... ],
"@columns": [ { ... }, ... ],
"@banister": { ... },
"@floorSlab": { ... }
},
{
...
}
]
}
假设我们想要进行一些估算的碳计算。 但是,目前还没有向Speckle流中的元素添加材料信息。 我们可以自己添加并为团队的其他成员更新流。
3、子类基础
为此,让我们为每种材质创建类,并将这些材质添加到流中每个对象的“材质”属性中。 显然,这是一个有点过头的例子。 只向每个对象添加一个字符串属性来指示材料,然后在运行计算时从数据库中查找材料属性会更有效。 但是,我想在这里找点乐子,并向你展示一些您可以使用 Base 类做的很酷的事情!
让我们从将材料定义为 Base 子类开始。 这确保它们将被正确序列化,并且它们将被 Base 类型注册表拾取。 然后我们可以为每个元素类型名称创建一个到相应材料类的 speckle_type 的映射。 speckle_type 由类预填充,默认为类名。
# density (kg/m^3) and embodied carbon (kg CO^2/kg) estimates
# from https://www.greenspec.co.uk/building-design/embodied-energy/
class Concrete(Base):
density: str = 2400
embodied_carbon = 0.159
class Glass(Base):
density: str = 2500
embodied_carbon = 0.85
class Steel(Base):
density: str = 7800
embodied_carbon = 1.37
MATERIALS_MAPPING = {
"@floorSlab": "Concrete",
"@banister": "Glass",
"@facade": "Glass",
"@columns": "Steel",
}
我们现在可以编写一个函数来遍历关卡的所有成员,并添加一种材质(如果它存在于我们的映射中)。
def add_materials_data(level: Base) -> Base:
# first, get all the attribute names
names = level.get_member_names()
# then iterate through them to check if they exist in our mapping
for name in names:
if name not in MATERIALS_MAPPING.keys():
break
# if they do, use this class method to get the class and init it
material = Base.get_registered_type(MATERIALS_MAPPING[name])()
# now we can add a `@material` attribute dynamically to each object.
# note that we're making it detachable with the `@`
prop = level[name]
if isinstance(prop, Base):
prop["@material"] = material
elif isinstance(prop, list):
for item in prop:
item["@material"] = material
return level
请注意,我们已经为感兴趣的每个元素添加了一个名为@material 的分离动态属性。我们已将其标记为可分离(以@ 开头),因此我们不会在此存储数百个相同材料类的副本流。
在我们的例子中,每个材料类的每个实例都是相同的,所以我们只需要存储一次。 但是,我们希望在多个元素中引用它们。 使用可分离属性是解决方案! 你在每个元素中获得了正确材质对象的引用,但只在流中存储唯一对象(在我们的例子中,每个材质类的一个实例)。
4、发送到Speckle流
我们现在可以使用我们编写的 add_materials_data() 方法来更新流中的所有关卡并将这些更新后的关卡发回。 让我们添加到已经写的内容上。
要发送我们更新后的建筑,我们需要创建一个父 Base 并将我们的嵌套级别列表放置在该父级中。 为了保持一致,我们将把它们添加到一个名为数据的字段中。
from specklepy.objects import Base
# add the materials data to our levels
levels = [add_materials_data(level) for level in levels]
# create a base object to hold the list of levels
base = Base(data=levels)
然后我们将使用 operations.send() 将此对象发送到流,然后使用 client.commit.create() 将我们的更改提交到流。 如果我们想发送到一个新的分支,我们可以首先使用 client.branch.create(stream_id, name, description) 创建一个。 下面的片段显示了这个完整的过程。
# recap from earlier
client = SpeckleClient(host=HOST)
account = get_default_account()
client.authenticate_with_account(account)
commit = client.commit.get(STREAM_ID, COMMIT_ID)
transport = ServerTransport(client=client, stream_id=STREAM_ID)
res = operations.receive(commit.referencedObject, transport)
# get the list of levels from the received object
levels = res["data"]
# add the materials data to our levels
levels = [add_materials_data(level) for level in levels]
# create a branch if you'd like
branch_name = "🐍 demo"
branches = client.branch.list(STREAM_ID)
has_res_branch = any(b.name == branch_name for b in branches)
if not has_res_branch:
client.branch.create(
STREAM_ID, name=branch_name, description="new stuff from py"
)
# create a base object to hold the list of levels
base = Base(data=levels)
# and send the data to the server and get back the hash of the object
obj_id = operations.send(base, [transport])
# now create a commit on that branch with your updated data!
commid_id = client.commit.create(
STREAM_ID,
obj_id,
branch_name,
message="add detached materials",
)
如果我们返回网络查看流,你会看到最新的提交。 几何图形应该看起来是一样的,因为它没有被修改,但是进入数据将显示新的材料属性。 正如你在下面的 gif 中看到的,Concrete 对象的 id 在不同的楼板上是相同的,因为属性是分离的并且对象是相同的。
现在你已将更改发送到服务器,与你合作的任何人也将能够提取你的更改并将这些更新的元素与材料数据一起使用!
5、尝试一些很酷的事情
我们现在已经了解了如何从 Python 接收、使用和发送数据,就像你可以使用任何其他 SDK 和连接器一样。 太好了,现在我们可以使用这些数据来制作一些很酷的东西了!
让我们使用 plotly绘制数据并使用 dash 将其显示在一个简单的页面上。 我不会详细介绍如何使用这些库本身,但大部分绘图代码只是根据其文档中的样板和示例进行了修改。
首先,让我们从对象中提取一些现有数据并绘制它们。 我们感兴趣的每个几何对象都有一个包含点列表的 Vertices 属性。 让我们在 3D 散点图上绘制楼板和柱子的顶点。 为此,我们需要通过遍历级别来构建一个包含我们感兴趣的值的 pandas 数据框。
现在我们可以将该数据框传递给 plotly express的 scatter_3d 方法并显示图形。
import plotly.express as px
df_vertices = construct_points_df(levels)
fig = px.scatter_3d(
df_vertices,
x="x",
y="y",
z="z",
color="element",
opacity=0.7,
title="Element Vertices (m)",
)
fig.show()
Plotly 图具有内置的交互性,因此这就是我们生成此 3D 散点图所需的全部代码。
接下来,让我们使用添加到每个流对象的材料属性。 让我们计算一个级别中每种元素类型的质量和隐含碳,并用结果构建一个数据框。
def construct_carbon_df(level: Base):
data = {"element": [], "volume": [], "mass": [], "embodied carbon": []}
# get the attributes on the level object
names = level.get_dynamic_member_names()
# iterate through and find the elements with a `volume` attribute
for name in names:
prop = level[name]
if isinstance(prop, Base):
if not hasattr(prop, "volume"):
break
# if it has a volume, use the material attribute to calculated
# the embodied carbon
data["volume"].append(prop.volume)
data["mass"].append(data["volume"][-1] * prop["@material"].density)
data["embodied carbon"].append(
data["mass"][-1] * prop["@material"].embodied_carbon
)
elif isinstance(prop, list):
if not hasattr(prop[0], "volume"):
break
data["volume"].append(sum(p.volume for p in prop))
data["mass"].append(data["volume"][-1] * prop[0]["@material"].density)
data["embodied carbon"].append(
data["mass"][-1] * prop[0]["@material"].embodied_carbon
)
data["element"].append(name[1:]) # removing the prepending `@`
return pd.DataFrame(data)
使用这个数据框,我们可以使用 plotly express的 pie() 和 bar() 方法来创建一些可爱的图。
# take the first level from our levels list and construct a data frame
df_carbon = construct_carbon_df(levels[0])
# let's add them all to a dict to keep the together
figures = {}
figures["volumes"] = px.pie(
df_carbon,
values="volume",
names="element",
color="element",
title="Volumes of Elements Per Floor (m3)",
)
figures["carbon bar"] = px.bar(
df_carbon,
x="element",
y="embodied carbon",
color="element",
title="Embodied Carbon Per Floor (kgC02)",
)
figures["carbon pie"] = px.pie(
df_carbon,
values="embodied carbon",
names="element",
color="element",
title="Embodied Carbon Per Floor (kgC02)",
)
我们要做的最后一件事是将数字添加到破折号应用程序中。 为此,我们只需采用样板 dash layout,用我们自己的图形替换示例图形,并在 /assets/style.css 中添加一些自定义 css。
body {
font-family: "Roboto", sans-serif;
margin: 0;
background-color: #f0f0f0;
}
.header {
background-color: #0a2948;
height: 256px;
display: flex;
flex-direction: column;
justify-content: center;
}
.header-title {
color: #fafafa;
font-size: 48px;
font-weight: bold;
text-align: center;
font-family: "Space Mono";
margin: 0 auto;
}
.header-description {
color: #fafafa;
margin: 4px auto;
text-align: center;
max-width: 450px;
}
.container {
display: flex;
flex-flow: row wrap;
justify-content: center;
max-width: 1200;
padding-right: 10px;
padding-left: 10px;
margin-top: 32px;
}
.card {
display: flex;
margin: 1em;
box-shadow: 0 20px 40px -14px rgba(0, 0, 0, 0.25);
@media {
width: 48%;
}
}
现在剩下的就是运行 app.py 并前往 http://127.0.0.1:8050/以查看你的精彩绘图。
6、结束语
我们已经为由 Speckle 数据提供支持的仪表板创建了基础! 这只是一个简单的开始,作为参考,可以在此处找到该项目的所有代码。