Properties

Properties are the primary way to model the data in IPKISS and understanding the basic behavior is an important step towards being proficient in creating complex PCells. This guide explains why we’re using properties, how to use them in your design and what you should pay attention to.

Why use properties

When we’re designing PCells we need to model its parameters. An important characteristic of a parameter is that it often only accepts a select range of values. When the parameter corresponds to something physical the value often makes no sense when it’s zero or negative. To avoid that users create designs which have no meaning, we want to inhibit that the user assigns an invalid value to the parameter. This is exactly what we use properties for: restricting the range and type of values you can assign to a parameter

In addition IPKISS’ properties help you as well with the following tasks:

  • providing defaults for your parameters
  • adding documentation for your parameters
  • implement caching to ensure that calculations don’t need to be run twice ( when not required )

Properties are a powerful tool and its functionality is frequently applied in the IPKISS codebase, and not exclusively for modelling parameters.

Note

If you’re familiar with programming languages like C++ or Java, you’ll know that those languages use static typechecking. This means that when you’re instantiating an object, you need to specify the type. The compiler will use this information to verify that you’re values with the right type. Python however, is a dynamically typed programming language and variables are not declared with a certain type. This brings advantages, but also means that you can’t use the help of the compiler to detect a few common errors.

In this guide we give a first guide to the most essential use of properties. For a more complete overview on which properties and restrictions are available, please consult the Reference Documentation.

DefinitionProperty

Almost all properties derive from DefinitionProperty. Other properties exist, but they’re tailored towards specific use cases and hence we discourage using them. DefinitionProperty and its derivates are virtually always the best choice to model your parameters. This section explains the behavior of DefinitionProperty in detail, to help you to fully understand properties. If you’re just looking for a specific property to model your parameters, we suggest that you take a look at corresponding reference.

Using DefinitionProperty

DefinitionProperties always need to be a member of a subclass of StrongPropertyInitializer ( sometimes we abbreviate this to SPI ). If you’re adding properties to other objects, your code will not work. When using properties for your designs, in almost all cases you will add properties to a subclass of PCell or to a View. In this guide however, we’ll consistently use StrongPropertyInitializer as we want to discuss the generic behavior of DefinitionProperty.

Tip

to check if a class is a subclass of a class, python provides the hande issubclass function. Below a very short example:

>>> import ipkiss3.all as i3
>>> # PCell is a subclass of StrongPropertyInitializer, hence it supports properties
>>> issubclass(i3.PCell, i3.StrongPropertyInitializer)
True

Creating a very basic DefinitionProperty with name my_prop and adding it to an SPI class is simple:

import ipkiss3.all as i3
class MyClass(i3.StrongPropertyInitializer):

    my_prop = i3.DefinitionProperty()

# now we can instantiate the class by specifying the value
my_cls = MyClass(my_prop="hello world")

# and we can access the value of the property
my_prop_value = my_cls.my_prop

This does however not have a lot of added value, and it’s very similar to a normal python class, with a my_prop attribute. If we would want to replicate our MyClass without using SPI we could write the following code:

class MyClass(object):
  def __init__(self, my_prop=None):
    # the __init__ function will we called when you're instantiating a class
    self.my_prop = None

But DefinitionProperty provides a few parameters that unlock its power. Let’s give an example of a more advanced property:

import ipkiss3.all as i3
from ipcore.properties.processors import ProcessorInt
class MyClass(i3.StrongPropertyInitializer):

    my_prop = i3.DefinitionProperty(default=42,
                                    restriction=i3.RESTRICT_INT,
                                    preprocess=i3.ProcessorInt(),
                                    doc="a more advanced property")

The behavior of this property is more advanced, so let us explore it with a few well chosen examples, we’ll go into depth in each of its aspects in the following sections.

>>> my_cls = MyClass()
>>> my_cls.my_prop
42
>>> # let's try to assign a float
>>> my_cls.my_prop = 43.34
>>> my_cls.my_prop
43
>>> # let's try to assign a string now
>>> my_cls.my_prop = "45"
>>> my_cls.my_prop
45
>>> # what would this do:
>>> my_cls.my_prop = "not an integer"
ValueError: invalid literal for int() with base 10: 'not an integer'

As you see, this behavior is quite different from what you would expect from regular python objects. In what follows next we’ll tell you more about all arguments you can specify to control DefinitionProperty’s behavior

Default Values

An important capability of DefinitionProperty is specifying default values. Whenever you didn’t explicitely specify the value for your properties, when instantiating a View or PCell, IPKISS will use the default value instead. We provide two ways to specify the default value of a property:

  1. by providing the keyword argument ‘default’
  2. by implementing the _default_{property-name} on your View or PCell.
import ipkiss3.all as i3
class MyCell(i3.PCell):
    prop_a = i3.DefinitionProperty(default='a')
    prop_b = i3.DefinitionProperty()
    prop_c = i3.DefinitionProperty(default='d')

    def _default_prop_b(self):
      return 'b'

    def _default_prop_c(self):
      # _default_*** methods take precedence over the default arg.
      return 'c'

The behavior of these properties is not really surprising, for prop_a the default will be ‘a’ for prop_b the default will be ‘b’:

>>> cell1 = MyCell()
>>> (cell1.prop_a, cell1.prop_b)
('a', 'b')
>>> cell2 = MyCell(prop_b='c')
>>> (cell2.prop_a, cell2.prop_b)
('a', 'c')
>>> cell2.prop_c
'c'

The property prop_c shows us that if you specify both a _default_method and a default argument, then the _default_method will be used.

The advantage of the default argument is that it’s concise and simple, everything is specified when you declare your property. Only when you want to implement more complex default behavior, you need to resort to _default_* methods. The usage of ‘_default_*’ methods has the following advantages:

  • allows you to implement logic, so the default value can depend on the value of other parameters/properties.
  • works well with inheritance, you can override the _default_method by applying inheritance.

Tip

You might have seen methods or properties that are prefixed with an underscore, this python convention tells you that you’re not supposed to use this method directly. We encourage you to apply this convention yourself, as it helps your colleagues to understand your code and what is not.

When you don’t specify a default value or a _default_method, IPKISS will tag this property as a required property. This means that you must specify the value whenever you’re instantiating your View or PCell, otherwise an error will be thrown.

Caution

IPKISS forces you to provide default values for all the properties of a View. A View is not allowed to have required properties.

Restrictions

As we mentioned earlier on this page, we want to restrict the range and types of values we can assign to our properties. IPKISS enables this through restrictions, restrictions are python objects that can validate a value. A few examples are:

  • validating that a value is positive or negative
  • validating that a value is of a certain type
  • validating that a value falls in a certain range
  • custom and specific restrictions

The following example shows that using restrictions is simple.

>>> import ipkiss3.all as i3
>>> class MyClass(i3.StrongPropertyInitializer):
...    prop = i3.DefinitionProperty(default='not a list', restriction=i3.RestrictType(list))
>>> my_cls = MyClass(prop=[])
>>> # assigning an integer will fail because it's not a list
>>> my_cls.prop = 12
TypeError: The 'prop' member on the 'MyClass' object must be of type 'list'. Got object of type 'int' instead.
>>> my_cls1 = MyClass()
>>> # we have not specified the value, so the default will be used, but the default is not of the correct type
>>> my_cls1.prop
TypeError: The 'prop' member on the 'MyClass' object must be of type 'list'. Got object of type 'str' instead.

IPKISS provides a whole range of predefined restrictions, it’s not in the scope of this guide to list them all. That’s what the reference is for. It’s however often not needed to use those at all. IPKISS also provides predefined properties that already have a restriction specified. PositiveNumberProperty is an often used property that internally combines a RestrictRange and a RestrictNumber to assure that you can only assign positive numbers. If you want to add an additonal restriction you can always to the following:

class MyClass(i3.StrongPropertyInitializer):
    """
    the prop property will only accept positive numbers smaller than 100
    """

    prop = i3.PositiveNumberProperty(default=4, restriction=RestrictRange(upper=100)

Preprocessors

Preprocessors can convert a value before it’s being assigned to a property, this can be useful when you’re assigning an invalid value that can easily be converted to a valid value. This is exactly what we did in our initial example. There we’re assigning a float, but only an integer is accepted. Without a preprocessor this example would fail, but a float can easily be transformed into an integer using, that’s were the preprocessor steps in. To add a preprocessor to a DefinitionProperty, you need to use the preprocess keyword.

IPKISS provides the following default preprocessors:

Caution

Preprocessors are not magic functions, they can only convert values that can be converted. a normal string e.g “some string” can never be converted to an integer.

Caching

In order to avoid that a value gets calculated unnecessarely when it has been calculated behavior, we’ve implemented caching. When using _default_xyz methods (with xyz the name of the property), IPKISS will store the result whenever this method is called for the first time. So if you access the property for a second time, ipkiss will not need to recalculate the method, because ipkiss is able to retrieve the cached version.

However sometimes we do need to recalculate the result, because the value might use other properties that in the mean time might have changed. As it’s difficult to deduce the dependencies between properties, we clear the cache once another property has been changed. You can easily explore this behavior with the following snippet:

cnt = 0
class MyClass(i3.StrongPropertyInitializer):

    prop_a = i3.DefinitionProperty(default='a')
    prop_aa = i3.DefinitionProperty()

    def _default_prop_aa(self):
      # we use the global keyword here, to be able to
      # access the 'cnt' variable.
      global cnt
      cnt = cnt + 1
      print("called '_default_prop_aa' {} times".format(cnt))
      return self.prop_a * 2

    prop_b = i3.DefinitionProperty(default='b')
>>> my_cls = MyClass()
>>> my_cls.prop_aa
called '_default_prop_aa' 1 times
'aa'
>>> my_cls.prop_aa
'aa'
>>> my_cls.prop_a = 'bb' # now the cached is cleared.
>>> my_cls.prop_aa # not cached anymore
called '_default_prop_' 2 times
'bbbb'

Derivates of DefinitionProperty

We’ve noted a few times that IPKISS has a few predefined properties that are special cases of DefinitionProperty. Most of the time they exhibit the same behavior as DefinitionProperty, but use a predefined restriction and/or preprocessor. We won’t discuss them in depth in this guide, but just list a few important ones, for more information we refer you to the reference.

PositiveNumberProperty([…]) DefinitionProperty restricted to be a positive (>0) int or float
NonNegativeNumberProperty([…]) DefinitionProperty restricted to be a positive or zero-valued (>=0) int or float
FloatProperty([internal_member_name, …]) DefinitionProperty restricted to be a floating point number (python float)
IntProperty([internal_member_name, …]) DefinitionProperty restricted to be an integer (python int)
StringProperty([internal_member_name, …]) DefinitionProperty restricted to be a string (python str)
ListProperty([internal_member_name, …]) Property restricted to be a python list object
NumpyArrayProperty([internal_member_name, …]) Property restricted to be a numpy array (numpy.ndarray)