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.
Thus, it is necessary to define the types of:
- All attributes present in the constructor method “init”
- All arguments present in other methods of the class, 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, defaulted to an empty string. The Bearing object requires the definition of :
- 3 float attributes
- a attribute name (string)
- an object of 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)
Type declarations are inserted after the attribute name by using a colon ":". 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 (builtin)
- List or tuple of float, integer, boolean or string
- Dictionary with a string key and a builtin Python value
- List or tuple of objects inheriting from DessiaObject or PhysicalObject
- Union of objects inheriting from DessiaObject or PhysicalObject
- Generic file.
3.1 Builtins
| Name | Python type | Example of value | Import from | | ------- | ----------- | ---------------- | ----------- | --- | | Float | float | 1.2 | - | | Integer | int | 3 | - | | Boolean | bool | True | False | - | | String | str | "foo” | - |
def my_function(my_float: float = 1.2, my_integer: int = 3,
my_boolean: bool = True, my_string: str = "foo")
3.2 Sequences
Name | Python type | Constraint | Import from |
---|---|---|---|
Homogeneous Sequence | List[T] | - Cannot have several different Types. | |
- No length constraint | typing | ||
Heterogeneous Sequence | Tuple[T, U, V] | - Can have several different types |
- Length is defined
- Order is defined | typing |
| Homogeneous Array |
List[List[T]]
| T must be “simple”, i.e a python builtin (int
,float
,str
,bool
) | typing | | Dynamic Dictionary |Dict[str, T]
| - Keys are always of typestr
- Values type (T), i.e a python builtin (
int
,float
,str
,bool
) | typing |
from typing import List, Tuple
class Bearing(PhysicalObject):
def __init__(self, balls: List[Ball],
positions: Tuple[float, float, float],
check: bool = True,
diameter: float = 0.01,
name: str = ""):
List, Dict & Tuple are imported from the typing module.
Tuple has not constraint over member types. i-th value type is not necessarily a builtin as shown in the example.
3.3 Unions
Name | Python type | Meaning | Constraint | Import from |
---|---|---|---|---|
Union | Union[T, U, V] | Value can be of type T, U or V | - T, U & V should be related for this typing to make sense. | typing |
InstanceOf | InstanceOf[T] | Value is of type “any subclass from T”, including T itself. | - T must derive from DessiaObject | dessia_common.typings |
3.4 Files
If we want to type a file, several options are available:
- "BinaryFile" for binary-like file objects
- "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"
These can be found in the “files” module of “dessia_common”.
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 XLSXFilefrom 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']
3.5 Enumerations
Enumeration types will give you the ability to pick a string value from a restricted amount of possible choices, defined by the code. In the form, a dropdown will be shown with allowed values :
Name | Python type | Meaning | Import from | Constraint |
---|---|---|---|---|
Literal | Literal[”a”, “b”, “c”] | Value is a string that is one of “a”, “b”, or “c” | typing | - |
KeyOf | KeyOf[DICT] | Value is one of DICT’s keys | dessia_common.typings | - DICT is a dictionary object of type Dict[str, T] . |
- DICT is a constant |
Example of typical usage, that will give the previously shown dropdown input in both case :
class EnumWithLiteral(DessiaObject)
""" A dummy class to show proper usage of Literal Type. """
def __init__(self, color: Literal["red", "green", "blue"] = "red", name: str = ""):
self.color = color
super().__init__(name=name)
COLORS = {"red": (255, 0, 0), "green": (0, 255, 0), "blue": (0, 0, 255)}
class EnumWithKeyOf(DessiaObject):
""" A dummy class to show proper usage of KeyOf Type. """
def __init__(self, color_name: KeyOf[COLORS], name: str = ""):
self.color_name = color_name
super().__init__(name=name)
@property
def color(self):
return COLORS[self.color_name]
Note that in this example, COLORS is a dictionary that doesn’t comply with the constraint given in section 3.2. Its type is Dict[string, Tuple[float, float, float])
its values are “complex” (Tuple[float, float, float]
) and not of simple builtin type. This is because we never actually need to input its value in a platform form. It is only used as a intern variable in the code and, thus, does not need to comply with the previous constraints.
These constraints are only relevant if your a typing a class attribute or a function argument, which will likely be shown as a form input.
Nevertheless, we still advise you build your code with these constraints applied to a maximum of your objects, even internal ones, because they usually refer to good coding and structuring practices.