Skip to content

Conversation

@Ghesselink
Copy link
Contributor

This rule is a bit more complex and less straightforward, some points of (potential) discussion;

  • When there are no units linked to the CRS, we consider the units based on the EPSG code.
  • Should we check other attributes as well? E.g. VerticalDatum might refer to a ESPG with different units compared to the project units?
  • In testfiles containing IfcGeographicCRS there is often no IfcMapConversion; do I miss something?
  • How do we handle project units in foot? They are often not associated directly through units but through IfcConversionBasedUnit (not through IfcProject.UnitsInContext.Unit).
  • What about millimetre/metre? This is done with a prefix in the project units and is considered in the step implementation.

Is this potential for a rule? the VerticalDatum of a CRS is described as:

Name by which the vertical datum is identified. The vertical datum is associated with the height axis of the coordinate reference system and indicates the reference plane and fundamental point defining the origin of a height system. It needs to be provided, if the Name identifier does not unambiguously define the vertical datum as well and if the coordinate reference system is a 3D reference system.

}

# determine the crs unit values
crs = context.model.by_type(crs_entity_type)[0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A model could potentially span two coordinate zones, therefore we shouldn't assume there will always just be one relevant crs to check. Suggested revision:

coord_reference_systems = context.model.by_type(crs_entity_type)
for crs in coord_reference_systems:
    ...

Comment on lines +22 to +23
#14=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_FOOT.);
#15=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_FOOT.);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be valid. Imperial units need to be defined as conversion-based from their SI equivalents. Then the conversion-based length and area units get assigned via IFCUNITASSIGNMENT similar to how angular degrees # 18 is included in # 19.

ifcopenshell.api.unit may be helpful in sorting this out.

#19=IFCUNITASSIGNMENT((#13,#14,#15,#18));
#20=IFCPROJECT('2X9wjf5oPB4PQjenCYrhHZ',#5,'',$,$,$,$,(#11),#19)
#21=IFCPROJECTEDCRS('EPSG:2277',$,'NAD83',$,'','3',$);
#22=IFCMAPCONVERSION(#11,#21,316131.64,5690966.11,1.,1.,0.,0.3046);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EPSG:2277 identifies the length unit as US Survey Foot which is not the same as international foot (exactly 0.3048 m). In either case, 0.3046 is incorrect. In this case it should be:

Python 3.12.6 (v3.12.6:a4a2d2b0d85, Sep  6 2024, 16:08:03) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 1200 / 3937
0.3048006096012192

Let the record state that I am merely providing information and not advocating for perpetuation of this obsolete length measure.

#20=IFCPROJECT('2X9wjf5oPB4PQjenCYrhHZ',#5,'',$,$,$,$,(#11),#19)
#21=IFCPROJECTEDCRS('EPSG:2277',$,'NAD83',$,'','3',$);
#22=IFCMAPCONVERSION(#11,#21,316131.64,5690966.11,1.,1.,0.,0.3046);
#23=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#9,#10);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appears to be a duplicate of # 11.

@civilx64
Copy link
Contributor

civilx64 commented Jun 9, 2025

How do we handle project units in foot? They are often not associated directly through units but through IfcConversionBasedUnit (not through IfcProject.UnitsInContext.Unit).

I left a comment about conversion factors and IfcConversionBasedUnit. Ultimately it should still be assigned to IfcProject via IfcUnitAssignment and therefore accessible through IfcProject.UnitsInContext.Unit. Can you point to a specific example test file(s) that does not have imperial values in UnitsInContext? Perhaps this was due to using an imperial unit enumeration with an IfcSIUnit entity?

@civilx64
Copy link
Contributor

civilx64 commented Jun 9, 2025

Should we check other attributes as well? E.g. VerticalDatum might refer to a ESPG with different units compared to the project units?

I am leaning towards checking all three attributes that potentially refer to an EPSG code, but I can't say how often this actually happens. If there are different units for HorizonalDatum and VerticalDatum, that would indicate the need to use IfcMapConversionScaled.

@civilx64
Copy link
Contributor

civilx64 commented Jun 9, 2025

In testfiles containing IfcGeographicCRS there is often no IfcMapConversion; do I miss something?

Read the docs for IfcCoordinateOperation if you have not already.

IfcGeographicCRS can be used with IfcRigidOperation.
See test/files/GRF/grf005/na-grf005-geographic_referencing_no_map_conversion.ifc for example.

Copy link
Collaborator

@aothms aothms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if you have questions, quite a few comments from my side and it's a bit chaotic. Most important: focus on length and expect Scale to be the quotient of the actual factors (not based on the equivalence of name strings).


Given A model with Schema 'IFC4.3'
Given An .IfcMapConversion.
Given There must be at least 1 instance(s) of .<IfcCoordinateReferenceSystem>.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bit fan of "Given There must be ..." can we rephrase this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IfcMapConversion.TargetCRS = (IfcCoordinateReferenceSystem) is also a mandatory attribute so I don't see how this could be different (or at least.. it's already handled by the schema check).

Given A model with Schema 'IFC4.3'
Given An .IfcMapConversion.
Given There must be at least 1 instance(s) of .<IfcCoordinateReferenceSystem>.
Given The <unit> unit(s) of the project ^is not^ equal to the <unit> unit(s) of the .<IfcCoordinateReferenceSystem>.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Given The <unit> unit(s) of the project ^is not^ equal to the <unit> unit(s) of the .<IfcCoordinateReferenceSystem>.
Given The <length_or_angle> unit of the project ^is not^ equal to the <length_or_angle> unit of the .<IfcCoordinateReferenceSystem>.

Maybe this reads better?

return map

# determine the project unit values
project = context.model.by_type("IfcProject")[0]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[0] is always scary of course. Maybe we can work our way around this...

Comment on lines +19 to +20
Then .Scale. ^is not^ empty
Then .Scale. ^is not^ 1.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than requiring not empty and not 1.0, can we require the actual value that is needed? I.e the one conversion factor divide by the other.

Scenario Outline: When the length unit of the Local CRS (from IfcProject) is not equal to the length unit of the Projected CRS, then the IfcMapCOnversion.Scale must be provided and cannot be 1.0

Given A model with Schema 'IFC4.3'
Given An .IfcMapConversion.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, MapConversion is a bit strange in the sense the SourceSRC is a SELECT between (CoordinateRef, RepresentationContext).

I think this allows us to workaround the by_type(IfcProject)[0], because we can do MapConv --Source--> RepContext -> Project/Context -> Unit.

So I think we should also condition this check to only execute when MapConv.Source is the RepContext (I'm not aware of any scenario in which this is not the case, but still).

Comment on lines +24 to +25
| length | IfcProjectedCRS |
| angle | IfcGeographicCRS |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I look at the mapconv docs I don't see any reference to scaling angular units and I don't think this actually is a thing.

NOTE The Scale can be used when the length unit for the 3 axes of the map coordinate system are not identical with the length unit established for this project (see IfcProject.UnitsInContext) - for example to convert feet into metres. If omitted, the scale factor 1.0 is assumed.

https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/IfcMapConversion.htm

So I think the rule only has to check length

if length := crs_units.get('length'):
prefix = getattr(length, 'Prefix', '')
name = getattr(length, 'Name', '').lower()
crs_units['length'] = (f"{prefix}{name}" if prefix else name).lower()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of focussing on names not being equal, i'd rather focus on the conversion factors

I.e https://github.com/IfcOpenShell/IfcOpenShell/blob/7b1aa207d842af2edc8d5dd5daa921a96d69ea91/src/ifcopenshell-python/ifcopenshell/util/unit.py#L680

Divide that with the unit scale reported by proj and that's the value of Scale you expect.

civilx64 and others added 2 commits November 25, 2025 19:00
Co-authored-by: Thomas Krijnen <t.krijnen@gmail.com>
Co-authored-by: Thomas Krijnen <t.krijnen@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants