Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Element Equivalence Interfaces #2003

Merged

Conversation

kwokcb
Copy link
Contributor

@kwokcb kwokcb commented Sep 4, 2024

Issue

Addresses issues: #1988 and #1981 dealing with comparison of element values by adding in a new "functionally" equivalent interface, such that functional equivalence means that the 2 elements perform the same computation. This differs from the existing operator== interface which checks for syntactical equivalence.

The key points for functional equivalence include:

  • The order of attributes specified on an Element does not matter.
  • The order of children Elements does not matter as long as these are immediate children of a Document or a Compound NodeGraph.
  • Equivalence can be performed based on runtime values as opposed to the string value used for text storage. For floating point numeric values equivalence can be performed within a given precision tolerance and/or numeric format.
  • Non-functional attributes may be explicitly skipped, such as UI formatting. UI numeric meta-data follows the same equivalence criteria as for input/output values (e.g. uimax or uimin). Name and Category equivalence cannot be skipped.

Changes

  • Adds a new isEquivalent method to Element.
  • Adds a new isAttributeEquivalent method for Element and override for ValueElement to allow for value vs string comparisons on the latter.
  • Adds equivalence options class: ElementEquivalanceOptions which can be passed in as an argument to isEquivalent
  • Adds an optional results / feedback class: ElementEquivalenceResult which can be passed in as an argument to isEquivalent and isAttributeEquivalent.

The following equivalence options are available:

  • Floating point format and precision for a float ValueElement. This is the same precision mechanism used for all value<->string conversions including values for code generation and file I/O.
  • The ability to skip a user provided list of attributes. e.g. skipping { xpos, ypos }.
  • Perform raw string compares for values (as operator==) currently does instead of value comparisons for a ValueElement.

Implementation Notes

  • Note that value comparisons convert from string to a concrete runtime type which will remove any
    extraneous string formatting such as leading and trailing spaces, 0 and leading + sign.
  • The the Value's string representation is used to perform the actual comparison allow for support of precision options.

Tests

A new unit test has been added with various numeric variations to test values, ordering, and precision.
The document pair used for testing looks like this:

<materialx version="1.39">
  <input name="input0" type="boolean" value="false">
  <input name="input1" type="color3" value="  1.0,   +2.0,  3.0   ">
  <input name="input2" type="color4" value="1.0,   2.00, 0.3000, -4">
  <input name="input3" type="filename" value="filename1">
  <input name="input4" type="float" uimin="  0.0100 " uimax="  01.0100 " value="  1.2e-10  ">
  <input name="input5" type="float" uimin="  0.0100 " uimax="  01.0100 " value="  00.1000  ">
  <input name="input6" type="integer" value="  12 ">
  <input name="input7" type="matrix33" value="01.0,         2.0,  0000.2310,    01.0,         2.0,  0000.2310, 01.0,         2.0,  0000.2310       ">
  <input name="input8" type="matrix44" value="01.0,         2.0,  0000.2310, 0.100, 01.0,         2.0,  0000.2310, 0.100, 01.0,         2.0,  0000.2310, 0.100, 01.0,         2.0,  0000.2310, 0.100">
  <input name="input9" type="string" value="mystring">
  <input name="input10" type="vector2" value="1.0,   0.012345608">
  <input name="input11" type="vector3" value="  1.0,   +2.0,  3.0   ">
  <input name="input12" type="vector4" value="1.0,   2.00, 0.3000, -4">
</materialx>

and

<materialx version="1.39">
  <input name="input0" type="boolean" value="false">
  <input name="input1" type="color3" value="1, 2, 3">
  <input name="input2" type="color4" value="1, 2, 0.3, -4">
  <input name="input3" type="filename" value="filename1">
  <input name="input4" type="float" value="1.2e-10" uimin="  0.01" uimax="  1.01">
  <input name="input5" type="float" value="0.1" uimin="  0.01" uimax="  1.01">
  <input name="input6" type="integer" value="12">
  <input name="input7" type="matrix33" value="1, 2, 0.231,  1, 2, 0.231,  1, 2, 0.231,  1, 2, 0.231">
  <input name="input8" type="matrix44" value="1, 2, 0.231, 0.1, 1, 2, 0.231, 0.1, 1, 2, 0.231, 0.1, 1, 2, 0.231, 0.1">
  <input name="input9" type="string" value="mystring">
  <input name="input10" type="vector2" value="1, 0.012345611">
  <input name="input11" type="vector3" value="1, 2, 3">
</materialx>

This command script was used to test the Python API:

import argparse
import sys, os

import MaterialX as mx

def main():
    parser = argparse.ArgumentParser(description="Test if two documents are functionally equivalent.")
    parser.add_argument(dest="inputFilename", help="Filename of the input document.")
    parser.add_argument(dest="inputFilename2", help="Filename of the input document to compare against.")
    parser.add_argument('-sa', '--skipAttributes', nargs='+', help="List of attributes to exclude from comparisons.")
    parser.add_argument('-sv', '--skipValueComparisons', action='store_true', help="Skip value comparisons. Default is False.")    
    parser.add_argument('-p', '--precision', type=int, default=None, help="Specify the precision for floating-point comparisons.", )

    opts = parser.parse_args()

    # Check if both files exist
    if not os.path.isfile(opts.inputFilename):
        print(f"File {(opts.inputFilename)} does not exist.")
        sys.exit(0)
    if not os.path.isfile(opts.inputFilename2):
        print(f"File {(opts.inputFilename2)} does not exist.")
        sys.exit(0)

    doc = mx.createDocument()
    try:
        mx.readFromXmlFile(doc, opts.inputFilename)
    except mx.ExceptionFileMissing as err:
        print(err)
        sys.exit(0)

    doc2 = mx.createDocument()
    try:
        mx.readFromXmlFile(doc2, opts.inputFilename2)
    except mx.ExceptionFileMissing as err:
        print(err)
        sys.exit(0)

    print(f'Version: {mx.getVersionString()}')    

    equivalence_opts = mx.ElementEquivalenceOptions()
    if opts.skipAttributes:
        for attr in opts.skipAttributes:
            equivalence_opts.skipAttributes.add(attr)
    if opts.skipValueComparisons:
        equivalence_opts.skipValueComparisons = True
    if opts.precision:
        equivalence_opts.precision = opts.precision

    equivalent, results = doc.isEquivalent(doc2, equivalence_opts)
    if equivalent:
        print(f"Documents are equivalent")
    else:
        print(results)
        print(f"Documents are not equivalent: {len(results)} differences found")
        for i in range(0, len(results)):
            difference = results[i]
            print(f"- Difference[{i}] : Path: '{difference.path1}' vs path: '{difference.path2}'. Difference Type: '{difference.differenceType}'"
                  + (f". Attribute: '{difference.attributeName}'" if difference.attributeName else "")) 
    
if __name__ == '__main__':
    main()

source/MaterialXCore/Element.h Outdated Show resolved Hide resolved
source/MaterialXCore/Element.cpp Outdated Show resolved Hide resolved
source/MaterialXCore/Element.cpp Outdated Show resolved Hide resolved
- Separate out results into option results class argument.
- Add isAttributeEquivalent() to avoid duplication
@kwokcb
Copy link
Contributor Author

kwokcb commented Sep 23, 2024

Hi @ld-kerley , @jstone-lucasfilm

  • I'm curious if the logic here has been settled. If possible it would nice to get into the next patch release :).
  • I will need to port this to an equivalent Python utility so it can run in 1.39 unpatched -- but want to see if this is mostly locked down or not.
    Thanks !

Copy link
Contributor

@ld-kerley ld-kerley left a comment

Choose a reason for hiding this comment

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

Looking pretty good to me - just left a couple of very minor code style things.

source/MaterialXCore/Element.cpp Outdated Show resolved Hide resolved
source/MaterialXCore/Element.cpp Outdated Show resolved Hide resolved
- Use const instead of non-const
- Fix formatting
- Get rid of uneeded bool.
- Fix up naming of c1, c2
@kwokcb
Copy link
Contributor Author

kwokcb commented Oct 3, 2024

I added in Python bindings to avoid another PR. I have an equivalence script, but that's not necessary to add in unless you think it's worthwhile to ship with it

import argparse
import sys, os

import MaterialX as mx

def main():
    parser = argparse.ArgumentParser(description="Test if two documents are functionally equivalent.")
    parser.add_argument(dest="inputFilename", help="Filename of the input document.")
    parser.add_argument(dest="inputFilename2", help="Filename of the input document to compare against.")
    parser.add_argument('-sa', '--skipAttributes', nargs='+', help="List of attributes to exclude from comparisons.")
    parser.add_argument('-sv', '--skipValueComparisons', action='store_true', help="Skip value comparisons. Default is False.")    
    parser.add_argument('-p', '--precision', type=int, default=None, help="Specify the precision for floating-point comparisons.", )

    opts = parser.parse_args()

    # Check if both files exist
    if not os.path.isfile(opts.inputFilename):
        print(f"File {(opts.inputFilename)} does not exist.")
        sys.exit(0)
    if not os.path.isfile(opts.inputFilename2):
        print(f"File {(opts.inputFilename2)} does not exist.")
        sys.exit(0)

    doc = mx.createDocument()
    try:
        mx.readFromXmlFile(doc, opts.inputFilename)
    except mx.ExceptionFileMissing as err:
        print(err)
        sys.exit(0)

    doc2 = mx.createDocument()
    try:
        mx.readFromXmlFile(doc2, opts.inputFilename2)
    except mx.ExceptionFileMissing as err:
        print(err)
        sys.exit(0)

    equivalence_opts = mx.ElementEquivalenceOptions()
    if opts.skipAttributes:
        for attr in opts.skipAttributes:
            equivalence_opts.skipAttributes.add(attr)
    if opts.skipValueComparisons:
        equivalence_opts.skipValueComparisons = True
    if opts.precision:
        equivalence_opts.precision = opts.precision

    results = mx.ElementEquivalenceResult()
    equivalent = doc.isEquivalent(doc2, equivalence_opts)
    equivalent = doc.isEquivalent(doc2, equivalence_opts, results)
    if equivalent:
        print(f"Documents are equivalent")
    else:
        print(f"Documents are not equivalent: {results.differenceCount()} differences found")
        for i in range(0, results.differenceCount()):
            difference = results.getDifference(i)
            print(f"- Difference[{i}] : Path: {difference[0]} vs path: {difference[1]}. Difference Type: {difference[2]}"
                  + (f" attribute: {difference[3]}" if difference[3] else "")) 
    
if __name__ == '__main__':
    main()

jstone-lucasfilm and others added 5 commits October 3, 2024 16:10
- Cleanup single result class to be structures. Create a vector class for this.
- Fix Python bindings to return [ status, result } pair.
- Fixed a logic regression. Previous changed removed non-value compare in ValueElement::isAttributeEquivalent()
Copy link
Member

@jstone-lucasfilm jstone-lucasfilm left a comment

Choose a reason for hiding this comment

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

This looks good to me, thanks @kwokcb!

@jstone-lucasfilm jstone-lucasfilm merged commit a5073ac into AcademySoftwareFoundation:main Oct 7, 2024
34 checks passed
@kwokcb kwokcb deleted the value_equivalent branch October 7, 2024 17:56
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.

3 participants