Loading a Design#

Rangekeeper provides API access to Speckle. This enables loading and extending a 3D design with Rangekeeper in order to produce its financial valuation, as well as execute any automated decision-making or optimisation processes, and sending the results back to Speckle.

Object Model#

Before walking through the I/O methods, it is important to understand the requirements Rangekeeper places on how a design must be structured, so that the Flows and Streams of a DCF Proforma can be appropriately attributed to the design’s objects (especially its representations of physical spaces, components, and equipment).

Entities and Relationships#

A holistic representation of a real estate asset is that it is a web of inter-related ‘entities’; where each entity is a ‘thing’ and can have multiple relationships with other entities. For example, a floor is a type of entity (say, a “Space”) and it has relationships with floors above and below it, as well as relationships to its sub-entities (e.g. rooms), or even what services it (e.g. the mechanical systems controlling its air conditioning)

_images/devModelGraph.jpg

Fig. 6 An entire Real Estate asset is represented as a huge web (graph) of Entitys and Relationships between those Entitys#

Entitys#

An Entity is a ‘thing’ that has substance, or is composed of sub-Entitys that have substance, material, mass, volume, existence, are ‘real’, etc.

Relationships#

Relationships describe some sort of (directed) association, connection, or link between Entitys. Each Relationship is described by its ‘type’; for instance, an Entity can be related to another by virtue of it “being contained (spatially)” by the other, or it could be “installed before” another Entity. There can be any number of Relationships between any number of Entitys.

Assemblys#

In order to clearly encapsulate ‘groups’ of intuitively-related Entitys, Rangekeeper has a concept of an Assembly, which is an object that defines a collection of Relationships and their associated Entitys. Assemblys are Entitys themselves, too –- so they can also be related to other Entitys. This means that Rangekeeper can traverse from Assembly to Assembly through overlapping Entitys, in a similar fashion to common-sense conceptualisation of how a real estate asset is structured.

_images/devModelAssembly.jpg

Fig. 7 An Assembly is a non-exclusive collection of Relationships, enabling the traversal of Entitys in the graph.#

Definitions#

Properties of Entitys#

All Entitys have the following properties:

  1. entityId: a unique & immutable identifier for the Entity over the whole of its lifetime.

  2. Name: human-readable text identifying the Entity

  3. Type: an entityType (which is a node of a tree of entityTypes)

  4. Attributes: a key-value store of specific properties of the Entity (eg, its area, geometry, material, cash flows, etc).

In addition, Assemblys record a set of Relationships about Entitys that are related to, or in, the Assembly.

  1. Relationships: a set of Relationships

Properties of Relationships#

Meanwhile, all Relationships have the following properties:

  1. Type: a RelationshipType, which is an item from a list of defined RelationshipTypes

  2. Source: the Entity that is the source of the Relationship

  3. Target: the Entity that is the target of the Relationship

Events#

Every Entity can exist in both space and time. Most Entitys will have (or its sub-Entitys will have, in the case of an Assembly) geometry/s that define the extent of space that the entity exists in.

To represent how an Entity exists in time, the Entity has Events recorded against it. For example, an Event may record the installation of the entity during construction, or it may record production of revenue during operation of the real estate project as an asset.

Flows and Streams are subclasses of Events.

_images/objModel.jpg

Fig. 8 Diagram of general overview of the founational concepts of Rangekeeper’s object model, in UML-style.#

Rationale and Implications#

Rangekeeper will use the Relationships defined in the Assembly/s of a design to structure the “drill-downs” and “roll-ups” (ie, the compositions) of Flows into Streams that are appropriate for the kinds of summations and metrics that the DCF Proforma model requires.

For example, an office building may produce revenue from multiple tenants. A tenant may occupy multiple floors, or a part (space) of a single floor. For Rangekeeper, each space would be an entity; with its Events property containing a set of Flows and/or Streams that represent revenues or costs associated with that space. Those may be aggregated into cash flows generated by the tenant of those spaces, and analysed as such, or they may be aggregated per floor, or per building, or per project.

This makes it simple to calculate things like the share of total revenue generated by each tenant; or the share of revenue generated by each floor. Likewise with costs.

The multi-faceted nature of the Entity-Relationship model enables multiple pathways for those aggregations to be specified, and thus enables an efficient and broad ability to query the model for the kinds of analyses pertinent to real estate valuation.

Technicalities#

To provide a Rangekeeper model with the Entity-Relationship organization, those need to either be defined in the design itself, or they need to be specified after receiving and loading the design.

Defining Entitys and Relationships in the Design Scenario#

Rangekeeper uses the Speckle service and data model for the exchange of designs, and scaffolds entitys off a Speckle Base

It also provides two means to define entitys and Relationships:

  1. A plugin to [McNeel’s Rhinoceros3D Grasshopper] (https://www.rhino3d.com/6/new/grasshopper/), that enables both automated and manual assignment of Relationships to geometries before their export to Speckle; and

  2. Manual assignment of Relationships to Entitys once they have been loaded from a Speckle stream in Rangekeeper.

Example Design#

An example property has been documented and uploaded to Speckle for us to use as a demonstration of the object model and its use in Rangekeeper.

This design is composed of two mixed-use buildings, that both share a common basement plinth, used for parking and shared mechanical and other services (e.g. garbage disposal, storage)

To access the design in Rangekeeper, we need to load it from Speckle. First load neccesary libraries:

import os

from IPython.display import IFrame

import rangekeeper as rk

Authenticate and Load the Design from Speckle#

# Authenticate with Speckle
speckle = rk.api.Speckle(
    host="speckle.xyz",
    token=os.getenv('SPECKLE_TOKEN')) # Note if you run this notebook yourself, you'll need to set this environment variable
 SpeckleClient( server: https://speckle.xyz, authenticated: True )

The design is stored in a Speckle ‘stream’, which is a container for a set of ‘commits’, or versions of the data.

stream_id = "c0f66c35e3"
commit_id = speckle.get_latest_commit_id(stream_id)
# Load the design data
data = speckle.get_commit(stream_id=stream_id) # Providing no commit_id defaults to the latest commit
data
Base(id: e7acaac21ae7e9369339900a4aaeb827, speckle_type: Base, totalChildrenCount: 3006)

Inspect the Design#

IFrame("https://speckle.xyz/embed?stream={0}&commit={1}".format(stream_id, commit_id), width='100%', height=800)
# Let's identify the root members of the:
roots = data.get_dynamic_member_names()
roots
['@property', '@context']

In this design, there are two members (Speckle Objects) at the root of the model:

  1. ‘@context’, and

  2. ‘@property’

Note

Speckle Objects whose member names begin with an @ indicate ‘detached’ data: See The Base Object: Detaching for more information.

For future reference, if we wanted to see their spatial representation, we can pass them in to the Speckle Viewer’s “objects” endpoint with their ids:

IFrame("https://speckle.xyz/streams/{0}/objects/{1}".format(stream_id, data['@property']['id']), width='100%', height=800)

We can even use the Speckle Viewer’s ‘filter’ functionality to see only the certain types of objects contained within the spaces of the design (try toggling the isolation mode for “floor” or “utilities” types):

label = 'type'
IFrame('https://speckle.xyz/streams/{0}/commits/{1}?filter=%7B%22propertyInfoKey%22%3A%22{2}%22%7D'.format(
    stream_id,
    commit_id,
    label), width='100%', height=800)

Rangekeeper Entitys in the Design#

Of course, not all Speckle Objects are Rangekeeper Entitys for us to use in our valuations and modeling. To make parsing the design easier, Rangekeeper provides some helper methods to convert the Speckle Objects into Rangekeeper Entitys:

# Return any Speckle Objects in the '@design' trunk that have 'entityId's:
parsed = rk.api.Speckle.parse(base=data['@property'])

# (Recursively) Convert the Speckle Objects into Rangekeeper Entities:
property = rk.api.Speckle.to_rk(
    bases=list(parsed.values()),
    name='property',
    type='archetype')

# (We can check that it is an Assembly:)
isinstance(property, rk.graph.Assembly)
Warning: Duplicate Entity 6496e279-88ca-49f4-b48c-6c0f156e0a58 [utilities] found.
Existing Entity is an Assembly while new Entity is not. Keeping Assembly.
Warning: Duplicate Entity 6496e279-88ca-49f4-b48c-6c0f156e0a58 [utilities] found.
Existing Entity is an Assembly while new Entity is not. Keeping Assembly.
Warning: Duplicate Entity 6496e279-88ca-49f4-b48c-6c0f156e0a58 [utilities] found.
Existing Entity is an Assembly while new Entity is not. Keeping Assembly.
Warning: Duplicate Entity 2b51847e-ec54-415d-afa0-ce32d74c6144 [plinthparking] found.
Existing Entity is an Assembly while new Entity is not. Keeping Assembly.
True

Querying the Design’s Object Graph#

Rangekeeper uses the NetworkX library to represent the object graph of the design, where each Assembly is a MultiDiGraph of Entity nodes and Relationship edges. This means we can query it pythonically:

# Get the "BuildingA" Assembly:
buildingA = [entity for (entityId, entity) in property.get_entities().items() if entity['name'] == 'buildingA'][0]
print('buildingA: {0}'.format(buildingA))
buildingA: Assembly: buildingA
Type: building
Members: ['type', 'name']
Entities: [('5ebb9736-5f18-4103-a5a4-040cd2e9b019', {'entity': Assembly: buildingA (Type: building)}), ('c0348c0f-a31f-421b-b6bc-0fe0d5ea947f', {'entity': Assembly: buildingAhotel (Type: space)}), ('1d0841bf-aa28-41d7-ba75-0ad0f3d57d21', {'entity': Assembly: buildingAretail (Type: space)}), ('7716632b-f43c-4ed2-9f84-26709361bbfe', {'entity': Assembly: buildingAresidential (Type: space)}), ('01aec121-ef19-4e6d-9f09-b2ce007b717d', {'entity': Assembly: buildingAretail (Type: space)}), ('6f6b227b-b28b-4412-89bd-0a9bbc47164e', {'entity': Assembly: buildingAparking (Type: space)}), ('7eec5208-fc0e-4dfd-bd0b-b1d226b2e97e', {'entity': Entity: buildingAcores (Type: utilities)})]
Relationships: [('5ebb9736-5f18-4103-a5a4-040cd2e9b019', 'c0348c0f-a31f-421b-b6bc-0fe0d5ea947f', 'spatiallyContains'), ('5ebb9736-5f18-4103-a5a4-040cd2e9b019', '1d0841bf-aa28-41d7-ba75-0ad0f3d57d21', 'spatiallyContains'), ('5ebb9736-5f18-4103-a5a4-040cd2e9b019', '7716632b-f43c-4ed2-9f84-26709361bbfe', 'spatiallyContains'), ('5ebb9736-5f18-4103-a5a4-040cd2e9b019', '01aec121-ef19-4e6d-9f09-b2ce007b717d', 'spatiallyContains'), ('5ebb9736-5f18-4103-a5a4-040cd2e9b019', '6f6b227b-b28b-4412-89bd-0a9bbc47164e', 'spatiallyContains'), ('5ebb9736-5f18-4103-a5a4-040cd2e9b019', '7eec5208-fc0e-4dfd-bd0b-b1d226b2e97e', 'spatiallyContains')]

Because we can also define directed, labelled Relationships between Entitys, we can identify relatives of Entitys accordingly:

# Get all relatives of BuildingA where BuildingA is the source of
# a 'spatiallyContains' relationship:
buildingA_containment = buildingA.get_relatives(
    outgoing=True,
    relationship_type='spatiallyContains',
    assembly=property)
print('buildingA Containment: \n {0}\n'.format(buildingA_containment))
buildingA Containment: 
 [Assembly: buildingAhotel (Type: space), Assembly: buildingAretail (Type: space), Assembly: buildingAresidential (Type: space), Assembly: buildingAretail (Type: space), Assembly: buildingAparking (Type: space), Entity: buildingAcores (Type: utilities)]
buildingAresi = [entity for entity in buildingA_containment if entity['name'] == 'buildingAresidential'][0]
print('buildingAresidential: {0}'.format(buildingAresi))
buildingAresidential: Assembly: buildingAresidential
Type: space
Members: ['type', 'volume', 'use', '@displayValue', 'name', 'renderMaterial']
Entities: [('7716632b-f43c-4ed2-9f84-26709361bbfe', {'entity': Assembly: buildingAresidential (Type: space)}), ('bf5a4444-6abd-4b89-b7a8-d758df5134fe', {'entity': Entity: buildingAresidentialFloor0 (Type: floor)}), ('dbf25b07-6f85-45bb-905c-de5b8562f5e7', {'entity': Entity: buildingAresidentialFloor1 (Type: floor)}), ('507e4ef2-fe13-44d7-af5f-bcf717b3de53', {'entity': Entity: buildingAresidentialFloor2 (Type: floor)}), ('bb815059-7516-45df-8fac-16f0e62242f3', {'entity': Entity: buildingAresidentialFloor3 (Type: floor)})]
Relationships: [('7716632b-f43c-4ed2-9f84-26709361bbfe', 'bf5a4444-6abd-4b89-b7a8-d758df5134fe', 'spatiallyContains'), ('7716632b-f43c-4ed2-9f84-26709361bbfe', 'dbf25b07-6f85-45bb-905c-de5b8562f5e7', 'spatiallyContains'), ('7716632b-f43c-4ed2-9f84-26709361bbfe', '507e4ef2-fe13-44d7-af5f-bcf717b3de53', 'spatiallyContains'), ('7716632b-f43c-4ed2-9f84-26709361bbfe', 'bb815059-7516-45df-8fac-16f0e62242f3', 'spatiallyContains')]

We can also visualize the design’s graph as well:

property.plot(name='assets/property')
IFrame(src="./property.html", width='100%', height=800)
assets/property.html