Explicitly add “units” to the platform

At the request of Matthijs Beekman I place this request here on the forum. Perhaps more customers are concerned about this. Maybe others already have a solution/workaround for this.

Description of the limitation and why it is relevant to address

As a developer I want to explicitly specify a unit for parametrization fields so that my code has less clutter and is more robust.

I think this is relevant for the VIKTOR platform because it removes boring code, allows for quicker development. It could also help prevent conversion bugs, making the platform more trustworthy for end users.

Current issues

Because external integrations don’t necessarily have the same unit as our parameters, conversions now appear in different places in our code:
floor_thickness_m = self.params.geometry.floor.thickness * 1e-03 # [m]

Sometimes those conversions are forgotten to add to the code, causing bugs.
Or sometimes a product owner / stakeholder wants a parameter to be changed from mm to m. Maybe great for UX, but it introduces quitte some code changes and more chances of bugs, leading to extra costs and lowered quality.

Submitter proposed design

Explicitly add “units” to the platform.

I’ve dealt with QUDT in the past and was a fan of it. It is an (extensive) way to record units (and more) in linked data. It also facilitates conversions between units. E.g. with quantity type “Temperature” it is easy to switch between Kelvin, ºC and ºF. But also at Length between meters, inches and millimetres.

That would allow for cleaner code like:
floor_thickness_m = self.params.geometry.floor.thickness.convert_to(qudt.unit.M)

You could then explicitly add a unit in the parameterization. Maybe even with a different ui_unit (similar to the current ui_name)

geometry.floor.thickness = NumberField(
    "Floor thickness",
    unit=qudt.unit.M,
    ui_unit=qudt.unit.MilliM,  # millimetre
    # suffix="mm",  << Made redundant by qudt.unit.MilliM.symbol
)

Added benefit: Automagical suffix parameter

As seen above, this also makes the suffix parameter less important, as it is automatically populated from the specified unit.

Added benefit: Automagical unit conversions for external integrations

When the platform itself can handle units, conversions for external integrations may even be omitted altogether. E.g. when a dfoundations.RectPile() is called with a NumberField param, the platform automagically convert the param’s value into meter.

Current workarounds

We could make use of Pint of Sympy’s physical quantities in our own code. Maybe even extend the NumberField and IntegerField classes. But that would probably only partially fix the issue. E.g. would this work with with Table and DynamicArray containing such an extended class?

I think it has more added value for the platform when this is a built-in functionality. It would prevent us developers all invent the same thing.

Thank you for adding this to the community, and opening up the possibility to discuss this.

Thanks for adding it here, interested to hear if other developers have similar requests

We find ourselves in a similar situation, having received a request from our users to introduce an OptionField to either “metric” or “imperial” to switch the unit system in the UI. We are planning to do this by displaying only the fields relevant to the selected unit system, in the same step, in the same Parametrization. But it is annoying to program as fields almost double.

At the same time, I don’t know what the best way to implement it on VIKTOR’s side would be as our background Python modules computation is metric, but can produce imperial design notes at the rendering step. So, we are afraid that a conversion by VIKTOR may by slightly different than our homemade conversion modules, resulting in a value difference between UI inputs and the design note.

Maybe the best way would be to handle units without handeling conversions (except for trivial conversions such as mm, cm, m). In other words, the Parameter class can have a “unit” argument, but is clueless for conversion between unit systems, and developers need to do the conversion themselves.

2 Likes

I agree with @kdurando on this, handling conversions with pint or any other method is not the hard part, the current issue with Viktor is the way of implementing that in the interface: we want to be able to switch a setting (OptionField) to switch the interface from imperial to metric. To do this, 2 solutions:

  • Create 2 different fields with visibility set according to the unit : good for UX but a nighhtmare for code (2 lines for 1 input + need a if condition to recover the value)
  • Create 1 field with both units specified in the suffix=‘(m | ft)’, good for clean code but the user may be lost.

So to solve this issue, it would be good enough to have the ability to define the suffix (or a new parameter) with a callback function (just like we can do it for min and max in a NumberField

1 Like

Also applicable to development done by @tom.slusarczyk

As well as work done by @Mike684 and @RahmanOzdemir

1 Like

I don’t know if I feel the same way @leomalet. Doing the conversion in code might not be hard, but that means those conversion would be sprinkled throughout the code. That could add the risk of forgetting a conversion, or somehow converting twice. It’s all easy to solve, but it would be nice if, as a developer, I didn’t even have to deal with it.

I see is the same as creating a Scia model: I could create the xml in code, but using viktor.external.scia.scia.Model saves a lot of time and probably saves me from a lot of mistakes.

Regarding the possibility to change the UI units, one could opt to make the ui_unit, from my earlier proposal, accept a function/callable, similar to visible.

Maybe a nightmare for Viktor to implement though, as the UI value wouldn’t be 1:1 anymore with the database value. But that is already the case with OptionListElement, so maybe not that hard. In any case, it would save us time as developers and increase quality. Recently we have to adjust some parameters from N to kN and it does take some effort to make sure nothing is overlooked.

Thanks everyone for chipping in and sharing thoughts. To bring this forward i believe it would be valuable to look at concrete examples next to each other and compare pros and cons. How would your code look like with potential platform additions? For this each example needs:

  • a field definition (single line of parametrization)
  • a simplified view method. how is the resulting param used?

i know @Sylvain has an example, can you share it here?

Hi @matthijs , think this is the example you are referring to. It’s a fairly straightforward one, where the unit is selected by means of a dropdown. Unit conversions are implemented by means of the pint package (as mentioned above)

from munch import Munch
from pint import UnitRegistry
from viktor import ViktorController
from viktor.parametrization import LineBreak
from viktor.parametrization import NumberField
from viktor.parametrization import OptionField
from viktor.parametrization import OptionListElement
from viktor.parametrization import OutputField
from viktor.parametrization import ViktorParametrization

ureg = UnitRegistry()
Q_ = ureg.Quantity

DEFAULT_PRESSURE = Q_(1.01312e5, "Pa")


def get_density_from_temperature(params: Munch, **kwargs: dict):
    """Formula that retrieves air density and pressure from temperature using the ideal gas law:

    p = rho * R_specific * T
    """
    if not params.temperature:
        return "Fill temperature first"
    if not params.temperature_unit:
        return "Fill temperature unit first"
    if not params.density_unit:
        return "Fill density unit first"
    temperature = Q_(params.temperature, params.temperature_unit.lower())
    specific_gas_constant = Q_(287, "J / (kg * K)")
    return (DEFAULT_PRESSURE / (specific_gas_constant * temperature.to("K"))).m_as(params.density_unit)


class Parametrization(ViktorParametrization):
    """Example unit conversion parameterization"""

    temperature = NumberField("Temperature")
    temperature_unit = OptionField("Temperature unit", options=["celsius", "kelvin", "fahrenheit"])
    new_line = LineBreak()
    default_air_density = OutputField("Air density", value=get_density_from_temperature)
    density_unit = OptionField(
        "Density unit",
        options=[
            OptionListElement(value="kg / m^3", label="kilogram / m^3"),
            OptionListElement(value="kg / L", label="kilogram / litre"),
        ],
    )


class Controller(ViktorController):
    label = "My Entity Type"
    parametrization = Parametrization
1 Like

The ability to assign a unit type (dynamically) would be great. In our backend code we use quantity types too and this would allow for even easier conversion. Manually setting the suffix would become less important and unit conversion would be easier.

In addition, as a styling note, we much prefer the suffix to be not in the description line but after the actual unit itself. Currently we style like this:

image

2 Likes

@matthijs Is there any additional information or update on the subjet please?

No there is no update at the moment. Will post here if anything changes

I agree that units are essential in engineering calculations. I have been working on an application in Viktor in which I used the Python package PINT to implement the units. I set it up so that the input values are entered as text fields including the units (e.g. 500mm) and then use PINT to parse the input and assign the value and the units to a variable. Any subsequent calculations using the variable then also use the units and calculated values also have the correct units. Output can then also be presented in preferred units. For example, input could be in US Customary units and output presented in SI units.
It would be nice if this was implemented by Viktor without additional programming effort.

2 Likes

@alastair.s (Arcadis) also showed interest in a feature similar to this request. The case has flow rates that can be expressed in units such as m^3/hour, l/s etc.