Units conversion issue

Hi All, @c.poupin @Mike.Buttery

In the following extracted part of my code, where I use WPF forms, I need the value of the concrete compressive strength entered by the user to calculate the YoungModulus for this material. For others purposes, I have already converted this value to Revit internal units (UnitTypeId.Megapascals) using the following function:

def _input_to_MPA(value):
    """Convert the textual value for Compressive strength of concrete to MPA."""
    return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Megapascals)

However, I am encountering difficulties in either reconverting this value back from internal units or determining a constant that allows me to compute the correct value for YoungModulus in megapascals using the formula:
image

# the variable contrainte in my code refers to the Compressive strength of concrete
E = 11000 * contrainte ** (1 / 3)

and according to this line in the code:

asset.SetYoungModulus(UnitUtils.ConvertToInternalUnits(11000 * float(c) ** (1 / 3),  UnitTypeId.Megapascals))

Here the part of the code wher I create the concrete material

class Create_Tank(Window):
    def __init__(self):
    ....
    ....
    def _collect_input_data(self):
        """Collect and convert input data from the UI"""
    ....
    ....
            try:
                self.data = {
                    "A": _input_to_meters(self.A_value.Text),
                    "B": _input_to_meters(self.B_value.Text),
                    "H": _input_to_meters(self.H_value.Text),
                    "ep": _input_to_meters(self.ep_value.Text),
                    "contrainte": _input_to_MPA(self.contrainte_value.Text)
     ....
     .... 
def _input_to_MPA(value):
    """Convert the textual value for Compressive strength of concrete to MPA."""
    return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Megapascals)

def get_or_create_concrete_material(contrainte, doc):
    """find or create the concrete material with the specific compressive strength requested by the user then return it's Id."""
    c = int(UnitUtils.ConvertFromInternalUnits(contrainte , UnitTypeId.Megapascals))
    print(float(c))
    material_name = "Béton - Coulé sur place - Béton{}".format(c)
    Materials = FilteredElementCollector(doc).WherePasses(ElementCategoryFilter(BuiltInCategory.OST_Materials))
    material = next((m for m in Materials if m.Name == material_name), None)
    if material:
        m_Id = material.Id
    else:
        with Transaction(doc, 'Create concrete material') as t:
            t.Start()
            m_Id = Material.Create(doc, "Béton - Coulé sur place - Béton{}".format(c))
            mat = doc.GetElement(m_Id)
            mat.MaterialClass = "BĂ©ton"
            mat.MaterialCategory = "BĂ©ton"
            mat.Color = Color(192, 192, 192)
            mat.Shininess = 128
            mat.Smoothness = 50
            asset = StructuralAsset("ConcreteStructuralAsset", StructuralAssetClass.Concrete)
            asset.ConcreteCompression = contrainte
            asset.Density = UnitUtils.ConvertToInternalUnits(2500, UnitTypeId.KilogramsPerCubicMeter)
            asset.SetPoissonRatio(0.2)
            asset.SetYoungModulus(UnitUtils.ConvertToInternalUnits(11000 * float(c)** (1 / 3),  UnitTypeId.Megapascals))
            struc = PropertySetElement.Create(doc, asset)
            mat.SetMaterialAspectByPropertySet(MaterialAspect.Structural, struc.Id)
            appar_Id = ElementId(177984)
            mat.AppearanceAssetId = appar_Id
            t.Commit()
    return m_Id              

As an example if I take contrainte = 27 MPA, the vale of E should be = 33000 MPA

I still have the wrong value as you can see below:

Thanks.

for what it’s worth I find that instead of setting these properties via the API it significantly easier and better from a QA standpoint to have a set of materials applied to the various types of a family you ship with the graph, and then load that family type into your project via the API. This way ALL material properties can be managed directly in the Revit UI and standards set, instead of having to fight the various levels of pain where the user might want to insert a value of 0.6674 MPA, causing the automation to fail. Let them pick from the preconfigured dropdown instead. :slight_smile:

1 Like

@jacob.small

It’s a good idea, as you proposed. In fact I’m using pyrevit and preparing a Tank tool that needs to utilize a material with the concrete compression strength specified by the user. Since the listed Revit concrete materials are limited, I’m giving the user the option to create its own material.

Thanks.

SI units for a Pascal are kg / m.s² so the internal Revit units are kg / ft.s². Then 1/ft → 1/m = 1/0.3048 or 3.280839… and 1/m → 1/ft = 0.3048

2 Likes

@Mike.Buttery

I know how to use the ratio between feet and meters. To avoid confusion, I used ConvertToInternalUnits to convert values to Revit’s internal units and ConvertFromInternalUnits to convert values back from Revit’s internal units.

I made changes to the YoungModulus function in my code in an attempt to solve this issue.

The change is as follows:
From:

asset.SetYoungModulus(UnitUtils.ConvertToInternalUnits(11000 * float(c)** (1 / 3),  UnitTypeId.Megapascals))

To:

asset.SetYoungModulus(UnitUtils.ConvertToInternalUnits(11000 * (float(c) ** 1 / 3)/3,  UnitTypeId.Megapascals))

I get the correct value of YoungModulus only if the value of contrainte is 27 MPa, which correctly returns 33000 MPa (with no decimals). However, when I use a different value of contrainte, which results in a decimal value, the difference becomes significant.

For example:

  • If contrainte = 22 MPa:
    • The expected value of YoungModulus is 30822.43 MPa.
    • The value returned in Revit is 26 888,89 MPa.

Could this discrepancy be related to rounding settings in Revit? How can I fix this issue to ensure that decimal precision is maintained?"

here my units settings for pressure :

Thanks.

@christian.stan

Have you an idea how to fix it?

Thanks.

hello, accessory

here is a clue

import sys
import clr
clr.AddReference("RevitAPI")
import Autodesk
from Autodesk.Revit.DB import UnitUtils,UnitTypeId

contraintefrench = IN[0]
def essai(cont_fr):
    ci=UnitUtils.ConvertToInternalUnits(cont_fr,UnitTypeId.Megapascals)
    cf=UnitUtils.ConvertFromInternalUnits(ci,UnitTypeId.Megapascals)
    youngmodul_f=int(11000*cf**(1/3))
    return [ci,cf,youngmodul_f]

OUT = [essai(c) for c in contraintefrench]

Sincerely
christian.stan

1 Like

StructuralAsset.YoungModulus is stored as a XYZ, and does not have a unit associated with it so you do not have to do a conversion
Edit:
narrator: It does have a unit associated with it
I would review you code…
You convert using _input_to_MPA() to internal units (not MPa) when you create the dictionary, then convert back for the value c (truncating with int) and then convert again (with float) when setting YoungModulus

Why not keep everything as MPa until finally converting?

Order of operations is also important

E = 11000 * 27 ** (1 / 3)
# 33000.0
E = 11000 * 27**1 / 3
# 99000.0
2 Likes

@Mike.Buttery @c.poupin

I’m completely confused about how to get the correct value! I executed a separate script that only creates the material (without the WPF form) and tested it with multiple values of concrete compression strength. I got the expected values for the Young’s Modulus. However, I can’t get the correct values in my main script, even though I applied your suggestions, including converting to MPa until the final step, without using the _input_to_MPA function to perform the conversion!!

Here my separate script which create material:

Dynamo_Material
import clr
import sys
import System

#import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB
clr.AddReference('RevitNodes')
import Revit
clr.ImportExtensions(Revit.Elements)
clr.ImportExtensions(Revit.GeometryConversion)
clr.ImportExtensions(Revit.GeometryReferences)
doc = DocumentManager.Instance.CurrentDBDocument

materials = FilteredElementCollector(doc).WherePasses(ElementCategoryFilter(BuiltInCategory.OST_Materials))

material = [i for i in materials if i.Name == "Béton - Coulé sur place - Béton26"]

if len(material) > 0:
    mat = m
    m_Id = mat.Id
else:
    t = Transaction(doc, 'Get/create concrete material')
    t.Start()
    m_Id = Material.Create(doc, "Béton - Coulé sur place - Béton26")
    mat = doc.GetElement(m_Id)
    mat.MaterialClass = "Concrete"
    asset = StructuralAsset("ConcreteStructuralAsset", StructuralAssetClass.Concrete)
    asset.ConcreteCompression = UnitUtils.ConvertToInternalUnits(26, UnitTypeId.Megapascals)
    asset.Density = UnitUtils.ConvertToInternalUnits(2500, UnitTypeId.KilogramsPerCubicMeter)
    asset.SetPoissonRatio(0.2)
    asset.SetYoungModulus(UnitUtils.ConvertToInternalUnits(11000*26**(1/3), UnitTypeId.Megapascals))
    struc = PropertySetElement.Create(doc, asset)
    mat.SetMaterialAspectByPropertySet(MaterialAspect.Structural, struc.Id)
    t.Commit()

OUT = 0

changes I made in my main script:

Main_script
....
....
def _collect_input_data(self):
        """Collect and convert input data from the UI"""
        ....
        ....
        else:
            try:
                self.data = {
                    ....
                    ....
                    # I dont used _input_to_MPA here to convert the text value to MPA
                    "contrainte": self.contrainte_value.Text
                }
            ....
            ....
def get_or_create_concrete_material(contrainte, doc):
    """find or create the concrete material with the specific compressive strength requested by the user then return it's Id."""
    # here I tried two options:
    # 1. use the variable 'contrainte' as it is in function's core then replace it by float(contrainte) for calculation
    # 2. introduce another variable 'c' and use it for calculations
    c = float(contrainte)
    material_name = "Béton - Coulé sur place - Béton{}".format(contrainte)
    Materials = FilteredElementCollector(doc).WherePasses(ElementCategoryFilter(BuiltInCategory.OST_Materials))
    material = next((m for m in Materials if m.Name == material_name), None)
    if material:
        m_Id = material.Id
    else:
        with Transaction(doc, 'Create concrete material') as t:
            t.Start()
            m_Id = Material.Create(doc, "Béton - Coulé sur place - Béton{}".format(contrainte))
            mat = doc.GetElement(m_Id)
            mat.MaterialClass = "BĂ©ton"
            mat.MaterialCategory = "BĂ©ton"
            mat.Color = Color(192, 192, 192)
            mat.Shininess = 128
            mat.Smoothness = 50
            asset = StructuralAsset("ConcreteStructuralAsset", StructuralAssetClass.Concrete)
            asset.ConcreteCompression = UnitUtils.ConvertToInternalUnits(c, UnitTypeId.Megapascals)
            asset.Density = UnitUtils.ConvertToInternalUnits(2500, UnitTypeId.KilogramsPerCubicMeter)
            asset.SetPoissonRatio(0.2)
            # option1
            #asset.SetYoungModulus(UnitUtils.ConvertToInternalUnits(11000 * float(contrainte) ** (1 / 3),  UnitTypeId.Megapascals))
            asset.SetYoungModulus(UnitUtils.ConvertToInternalUnits(11000* c** (1 / 3), UnitTypeId.Megapascals))
            asset.SetYoungModulus
            struc = PropertySetElement.Create(doc, asset)
            mat.SetMaterialAspectByPropertySet(MaterialAspect.Structural, struc.Id)
            appar_Id = ElementId(177984)
            mat.AppearanceAssetId = appar_Id
            t.Commit()
    return m_Id
....
....

Thanks.

You’re going to have to do some debugging. Comment out unnecessary lines, calculations etc. use a fixed value, use a default creator etc. then carefully step through the logic. Are you using global values inside the function? Is this a method inside a class or a function? Without all the code it is impossible to assess.
As a friendly reminder please see item 7 of the FAQs

What’s this for after you set the value?

In python syntax convention variables with capitals are used for classes

1 Like