Quick start with Dessia SDK

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 provides 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 or not, and each object will have a specific representation in the platform, which we call its display.

For example, a class that inherits from DessiaObject can have displays 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 a class 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 built.

Each user will then have to write their own classes 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 added. Then you can add all the specific methods to the object.

For example, to write an class describing a bearing, it will be necessary to:

  1. List all its attributes (inner diameter, outer diameter, thickness, number of balls, etc.).
  2. 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, an object instance of bearing 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 class. 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)

Database

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. Typing

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

NamePython typeExample of valueImport from
Floatfloat1.2-
Integerint3-
BooleanboolTrue-
Stringstr"foo”-
def my_function(my_float: float = 1.2, my_integer: int = 3,
                my_boolean: bool = True, my_string: str = "foo")

3.2 Sequences

NamePython typeConstraintImport from
Homogeneous Sequencelist[T]- Cannot have several different Types
- No length constraint
-
Heterogeneous Sequencetuple[T, U, V]- Can have several different types
- Length is defined
- Order is defined
-
Homogeneous Arraylist[list[T]]- T must be “simple”, i.e a python builtin (int, float, str, bool)-
Dynamic Dictionarydict[str, T]- Keys are always of type str
- Values type (T) must be simple, i.e a python builtin (int, float, str, bool)
-
class Bearing(PhysicalObject):
    def __init__(self, balls: list[Ball], positions: tuple[float, float, float],
                 check: bool = True, diameter: float = 0.01, name: str = ""):

tuple has not constraint over member types. i-th value type is not necessarily a builtin as shown in the example.

3.3 Custom Classes

General case

In order to type a variable as an instance of one of your custom classes, you can simply reference the class as a type.

def my_function(ball: Ball)
    # Ball is an instance of the Ball class

Recursive typing and classes that are not yet fully defined

You might encounter one of the following cases.

# Case 1 : Recursive typing. You need to type a variable with the class itself.
# Let's take the exemple of a "generate" classmethod that returns an instance of the class.
 
class MyClass:
    @classmethod
    def generate_one(cls) -> MyClass:
        # This return typing fails in current Python versions because MyClass if not yet fully defined.
        # Your IDE should warn you about it.
        return cls()
 
 
# Case 2 : Typing with a class that is not defined yet.
 
class ClassA:
    def __init__(self, my_object: ClassB)
        # my_object's typing fails because ClassB is defined further down the module.
        self.my_object = my_object
 
class ClassB:
    pass

In these cases simply add the following statement at the start of the package.

from __future__ import annotations

This will ensure that the classes can be referenced before they are fully defined.

Example :

from __future__ import annotations
 
class ClassA:
    def __init__(self, my_object: ClassB):
        self.my_object = my_object
 
class ClassB:
    @classmethod
    def generate_one(cls) -> ClassB:
        return cls()
 
    @classmethod
    def generate_many(cls) -> list[ClassB]:
        return [cls()]

... is a perfectly valid code in Python 3.9. In a further release of Python, the import statement won't be needed anymore, but it still remains unplanned.

You could use the ForwardRef proxy to achieve the same result, but while working with dessia_common its use is discouraged by Python devs themselves.

3.4 Unions

NamePython typeMeaningConstraintImport from
UnionUnion[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
InstanceOfInstanceOf[T]Value is of type “any subclass from T”, including T itself.- T must derive from DessiaObjectdessia_common.typings

3.5 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.6 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 :

Color

NamePython typeMeaningImport fromConstraint
LiteralLiteral[”a”, “b”, “c”]Value is a string that is one of “a”, “b”, or “c”typing
KeyOfKeyOf[DICT]Value is one of DICT’s keysdessia_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 internal 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.