Quick start with Dessia SDK
This is a technical document that explains the use of the dessia_common library for engineers.
Dessia is a platform that allows engineers to organize their knowledge in a similar way to how a developer structures their code using an object-oriented language. The platform offers a Python library named "dessia_common" with the goal of providing master objects that include the necessary fundamentals for proper functioning within the Dessia environment. These objects have been designed to provide all the methods necessary for proper functioning within the Dessia cloud platform, including the conversion of Python objects to JSON, conversion of JSON to a Python object, and defining equality between two objects. The document explains how to define object typing to enable the platform to automatically generate an appropriate form, and how to use the "_standalone_in_db" attribute to define whether an object is registered on its own or not. Finally, the document discusses the equality between objects and offers different methods for defining object equality, including using the "__hash__" method to compute a unique hash for each object.
1. Introduction
Dessia offers engineers a way to organize their knowledge in a similar way to how a developer structures their code using an object-oriented language. This approach aligns with the philosophy of MBSE, which uses the UML formalism (originally associated with object-oriented approaches) to provide architecture software for engineering. Other implementations of MBSE, such as the SysML formalism, exist, but they are a no-code approach that doesn't address the problems that Dessia tackles (such as CAD, exhaustive generation, and artificial intelligence).
Dessia offers a Python library named "dessia_common" with the goal of providing master objects that include the necessary fundamentals for the proper functioning of the Dessia platform. In other words, all objects developed within the framework of Dessia must inherit from these master objects, which incorporate all the methods necessary for recognizing your objects on the Dessia platform. Specifically, these objects have been designed to provide all the methods necessary for proper functioning within our cloud platform.
There are two main master objects:
- DessiaObject: master object for all objects without a 3D representation.
- PhysicalObject: master object for objects with a 3D representation.
These master objects natively integrate a set of methods to allow them to exist and be recognized by our platform. In summary, here are the main characteristics of these objects:
- Conversion of a Python object to JSON (necessary for using Dessia APIs).
- Conversion of JSON to a Python object.
- How to define equality between two objects.
- Presence of the object alone in our noSQL database.
- Generic method to customize the generation of 3D (in the case of PhysicalObject).
- 2D representation method.
The philosophy of Dessia is to break down any system into elementary objects. Each object can have either a physical representation (PhysicalObject) or not (DessiaObject), and each object will have a specific representation in the platform, which we call its display.
For example, an object that inherits from DessiaObject can have a display that includes the following elements:
- 2D conceptual representation (from the plot_data library).
- Graphics and selection system (from the plot_data library).
- Table.
- Free text detailing the object.
The graphical and 2D representations require the use of a specific formalism introduced in the "plot_data for dummies" section.
For an object that inherits from PhysicalObject, it will be possible to add a 3D representation to it. To do this, it will be necessary to detail from the "volmdlr for dummies" library how the 3D should be constructed.
Each user will then have to write their own objects by making them inherit from either PhysicalObject or DessiaObject. Next, you will need to detail all the inputs of the object, which we call its attributes. These elements should be detailed from the object constructor, and typing should be performed. Then you can add all the specific methods to the object.
For example, to write an object describing a bearing, it will be necessary to:
- List all its attributes (inner diameter, outer diameter, thickness, number of balls, etc.).
- List all the methods specific to bearings.
- Generation of its 3D representation.
- Calculation of static and dynamic loads.
- 2D representation of the bearing.
By respecting this formalism, the bearing object will offer a display that includes its 2D diagram and 3D representation.
2. Basics
When declaring a PhysicalObject type object, which represents a 3D model, the syntax is as follows:
from dessia_common.core import PhysicalObject
class Bearing(PhysicalObject):
_standalone_in_db = True
def __init__(self, internal_diameter: float,
external_diameter: float,
height: float,
name: str = ""):
self.internal_diameter = internal_diameter
self.external_diameter = external_diameter
self.height = height
PhysicalObject.__init__(self, name=name)
The first line of code imports the master PhysicalObject object from the dessia_common library, specifically from the core module.
Each object has two different behaviors regarding the databases present on the Dessia platform:
- Either the object is registered on its own (_standalone_in_db = True)
- Or the object is not registered on its own and will be registered from the parent object that contains it (_standalone_in_db = False).
This is a class attribute, and its value should remain unchanged throughout the object's lifecycle.
In the previous example, it didn't make sense to define the Bearing object as not _standalone_in_db because it wasn't included in a parent object. Therefore, it's important to define higher-level objects as _standalone_in_db, while contained objects can have either declaration. If we revisit the example of the Bearing object and introduce a Ball object to represent a ball, here's the possible syntax.
from dessia_common.core import PhysicalObject
class Ball(PhysicalObject):
_standalone_in_db = False
def __init__(self, diameter: float,
name: str = ""):
self.diameter = diameter
PhysicalObject.__init__(self, name=name)
class Bearing(PhysicalObject):
_standalone_in_db = True
def __init__(self, ball: Ball,
internal_diameter: float,
external_diameter: float,
height: float,
name: str = ""):
self.ball = ball
self.internal_diameter = internal_diameter
self.external_diameter = external_diameter
self.height = height
PhysicalObject.__init__(self, name=name)
Declaring an object in _standalone_in_db mode may take longer during the database insertion phase as it requires breaking down the global object into sub-objects. However, it's important to use this declaration if we intend to open this sub-object independently on the platform or reuse this object for other data settings.
It's important to note that using this declaration may increase the time required for database insertion. Therefore, it's recommended to consider the use case before deciding whether to declare the object in _standalone_in_db mode.
To gain a more detailed understanding of these mechanisms, it's essential to introduce the workings of the Dessia framework. In simple terms, our framework consists of three primary components:
- Python IDE (which incorporates the Dessia SDK libraries, including dessia_common)
- Cloud Instance (which is centered around a database and a Python emulator)
- Web Interface (preferably accessed via the Chrome browser)
Communication of objects between the Python IDE and the Cloud Instance is facilitated through the Dessia API (as detailed in the following section). Users can leverage the Dessia library, "dessia_api_client," to send objects to a Cloud Instance or import objects from a Python IDE. Upon sending an object, it is first converted into the JSON format, which serves as the standard web exchange format. This process, referred to as object serialization, is achieved through the "to_dict" method available in all DessiaObject and PhysicalObject objects.
Once the JSON file reaches the Cloud Instance, the reverse process, known as object deserialization, is performed through the "dict_to_object" method (also available in DessiaObject and PhysicalObject objects). A dedicated section on this topic, Serialization and Deserialization of Objects, provides further details.
3. Typings
Since Python is a dynamically-typed language and the Dessia platform utilizes forms for end users to input object attributes, it is necessary to introduce object typing to enable the platform to automatically generate an appropriate form, whether it be for a float, string, list, or array.
Thus, it is necessary to define the types of:
- All attributes present in the constructor method “__init__”
- All attributes present in the other methods of the object, as well as any possible return values.
The "__init__" method is the constructor for Python objects. In other words, it is the method that is executed when the object is instantiated.
If we consider the previous example of an object that represents a bearing, we can see that the constructor of the Ball object requires the definition of a diameter of type float and a name of type string, with a default value of an empty string. The Bearing object requires the definition of three float type attributes, a string type name, and a Ball object type ball (thus, the attribute will contain a Python Ball object).
from dessia_common.core import PhysicalObject
class Ball(PhysicalObject):
_standalone_in_db = False
def __init__(self, diameter: float,
name: str = ""):
self.diameter = diameter
PhysicalObject.__init__(self, name=name)
class Bearing(PhysicalObject):
_standalone_in_db = True
def __init__(self, ball: Ball,
internal_diameter: float,
external_diameter: float,
height: float,
name: str = ""):
self.ball = ball
self.internal_diameter = internal_diameter
self.external_diameter = external_diameter
self.height = height
PhysicalObject.__init__(self, name=name)
The declaration of the type is inserted after the attribute name by using a colon ":". Then, a default value can be defined by using the equal sign "=" after the type definition.
More generally, the Dessia_common library from Dessia offers a set of different types:
- Float, integer, boolean or string
- List or tuple of float, integer, boolean or string
- Dictionary with a string key and a built-in Python value
- List or tuple of objects inheriting from DessiaObject or PhysicalObject
- Union of objects inheriting from DessiaObject or PhysicalObject
- Generic file.
To type Python built-ins, we use "float" for floats, "int" for integers, "bool" for booleans, and "str" for strings. To type a list, we use "List" followed by the type name in square brackets for the elements in the list. For a tuple, we use "Tuple" also followed by square brackets to define the type of the elements in the tuple (it is necessary to define the type of each element in the tuple using a comma, as shown in the following example). The use of "List" and "Tuple" requires importing these functions from the "typing" package. Here's an example using these different types:
from typing import List, Tuple
class Bearing(PhysicalObject):
def __init__(self, balls: List[float],
positions: Tuple[float, float, float],
check: bool = True,
diameter: float = 0.01,
name: str = ""):
To type a Python dictionary, it is necessary to use "Dict", which is also imported from the "typing" package. For example, to type the following dictionary: { 'key1': 12.3, 'key2': 3.4 ... }
, we use the syntax "Dict[str, float]".
To type a list or tuple of DessiaObject or PhysicalObject objects, we use the syntax "List[Ball]" if we want to declare a list of "Ball" objects. Similarly, we use "Tuple[Ball, Bearing]" for a tuple whose first element is a "Ball" object and the second element is a "Bearing" object.
It is possible to type a union in cases where the exact type of the expected object is unknown. For example, by typing "Union[Ball, Bearing]", we define an attribute that can be either a "Ball" object or a "Bearing" object. Unions are not considered for built-in Python types. In such cases, we add multiple different attributes to handle this scenario.
If we want to type a file, several options are available:
- "BinaryFile" if we do not know the exact file extension
- "StringFile" if it is a text file with UTF-8 encoding and no specific extension. If we want to force the extension to be "txt", we can use "TextFile".
- "XLSXFile" if it is an Excel file with the extension "xlsx"
- "XLSFile" if it is an Excel file with the extension "xls"
- "XLSMFile" if it is an Excel file with the extension "xlsm"
- "CSVFile" if it is a "csv" file
- "MarkdownFile" if it is a markdown file
- "JsonFile" if it is a text file with the extension "json"
All of these types are present in the "files" module of dessia_common, so it is important to import them in the following manner: "from dessia_common.files import BinaryFile"
For example, if we want to read an Excel file from a class method, we can use the following syntax (it is necessary to import the "openpyxl" package to read Excel files):
from dessia_common.files import XLSXFile
from openpyxl import load_workbook
class Catalog(DessiaObject):
_standalone_in_db = True
def __init__(self, name: str = ''):
DessiaObject.__init__(self, name=name)
@classmethod
def read_exel(cls, excel: XLSXFile):
excel_catalog.seek(0)
wb2 = load_workbook(excel_catalog, data_only=True)
sheet_workbook = wb2['catalog']
Using a "classmethod" allows not to register the Excel file in the Dessia platform's database.