What are the Model object and how to use them ?#

Philosophy#

When we started to develop the sepal_ui framework we face the input and output storage problem. Python is a very versatile language but base variable are not mutable objects. It means that if a value is manipulated inside a function, the changes won’t be kept outside this very functions. the concept is easier to grasp with a simple example

def double(a):
    """multiply the value of "a" by two"""

    a *= 2

    return a

a = 4
res = double(a)

print(a)
print(res)

>>>> 4
>>>> 8

The value of “a” cannot be modified inside the function. For those who have practiced other languages such as C/C++, Fortran or Java, you know that it’s possible to choose if the variable is passed to the function by reference or by value. The flexibility of Python doesn’t authorize this difference, there are just mutable and not mutable objects. 2 solution were possible: either we return all the results from function to function (which could rapidly lead to a high number of tuples in the return statement) or use Model objects to store inputs of our widgets and output of our processes.

Model object#

In this context we decided to store input and output in dedicated custom object created for this sole purpose. Each of them should be bind to tiles and used in the called processes.

a default model could look like

from sepal_ui import model
from traitlets import Any # other types are available but Any can digest anything

class CustomIo(model.Model): # the model class embed some useful function as 'export', 'import' and 'bind'

    # inputs
    username = Any(None).tag(sync=True)
    password = Any(None).tag(sync=True)

    # output
    connection_url = Any(None).tag(sync=True)

And that’s all !

Example case#

We will here describe a complete example of the usage of a model.

in a model component, I create my custom Model.

# component/io/my_model.py

from sepal_ui import model
from traitlets import Any

class MyModel(model.Model)

    input = Any(None).tag(sync=True)
    output = Any(None).tag(sync=True)

    def __init__(self, default_value = None):

        self.input = default_value

I will also create a dummy script to use in my tile

# component/scripts/double.py

def double(model):

    return model.input * 2

Now I can create a custom tile that will use the MyModel object as an input storage (linking my_model.input to a slider). This MyModel will then be used in the _on_click method of my tile. This function will modify the value of the my_model.output trait.

# component/tile/my_tile.py

import ipyvuetify as v
from sepal_ui import sepalwidgets as sw
from sepal_ui.scripts.utils import loading_button

from component.scripts import *

class MyTile(sw.Tile):

    def __init__(self, model, **kwargs):

        self.slider = v.Slider()
        self.model = model.bind(self.slider, 'input') # save the model as an attribute of the tile

        super()._init__('my_tile', 'Tile title', [self.slider], sw.Btn(), sw.Alert())

        self.btn.on_event('click', self._on_click)

    @loading_button()
    def _on_click(self, widget, data, event):

        self.model.output = io.double(io)

        return

Now let’s test our code in situation. W’ll gather everything in a partial layout and see how the model object is changed persistently by the tile function

# double_ui.ipynb

from component import model
from component import tile

my_model = MyModel(default_value = 5)
my_tile = MyTile(my_model)

# fake the behaviour of the btn
my_tile.btn.fire_event('click', None)

print(my_model.__dict__['_trait_values'])

>>>
{
    'input': 5
    'output': 10
}

The output have been persistently modified and can be used in other tiles in the final process built in ui.ipynb or no_ui.ipynb

Use the model object for testing purpose#

When a new tile is created it can be bothering to launch the full app to gather all the information that we need to test our new component. A good practice is to use faked model object in the partial ui files to reproduce the output of a previous step.

let’s assume that you process require 2 model object, a custom one and the aoi_model object coming from the aoi_ui.ipynb.

# my_ui.ipynb

from component import model
from component import tile

my_model = MyModel()
my_tile = MyTile(my_model, aoi_tile.view.model)

Then to test your partial UI, you need a set aoi_tile object with a asset_id value. In its current state, your notebook will raise an error as aoi_tile.view.model is not set. You can add it in a debugging cell at the very beginning of the my_ui.ipynb.

# my_ui.ipynb

# for debug only
from sepal_ui import aoi

aoi_tile = aoi.Aoitile(asset = 'users/yourself/anAsset')

Now you have a perfectly working stand-alone notebook to test your process

Warning

Don’t forget to comment or delete this cell when you finish testing. If not, the output of your first steps will be overwritten in the ui and you will always end-up using the default one.

Advanced usage of io object#

model objects are Python objects so they can also embed specific methods to help you build a better app.

In this framework the AOI selection is hard-coded in the AoiIModel object and the AoiView object. If you look at the documentation of the lib you’ll see that AoiModel has a lot of embedded useful method that you can call anywhere. e.g: with the AoiIo.total_bounds method, you can get the AOI bounding box coordinates.

from sepal_ui import aoi
aoi_model = aoi.AoiModel(asset = 'users/yourself/anAsset')
bb = aoi_model.total_bounds()

In our previous example the double function is not a very useful scripts. instead we should have added it to the AOI member methods

# component/io/my_model.py

from sepal_ui import model
from traitlets import Any

class MyModel(model.Model)

    input = Any(None).tag(sync=True)
    output = Any(None).tag(sync=True)

    def __init__(self, default_value = None):

        self.input = default_value

    def double(self):

        return self.input * 2