Issue with add a new property to a feature in pygplates

Hello everyone:

I have attempted to create a new PropertyName and PropertyValue to assign it to a Feature in pygplates. I followed the steps in the document for pygplates (based on my understanding). However, I ran into a weird issue. I had no problems with creating a new property, assigning value to it and assigning the property with its value to a feature in pygplates. However, whenever I open the result gpml file in GPlates, then GPlates crashed completely. I herein enclose a few photos to demonstrate the problem. I wonder whether I can ask you all to give me an advice on this issue, please. Many thanks.

Hi Lavie,

Can you PM me a GPML file that reproduces the crash? And also maybe your pyGPlates code/snippet that creates the problematic feature.

I’m assuming you’re using GPlates 2.2.

Also can you tell me which pyGPlates revision you’re using. You can find that by typing:

python -c "import pygplates; print(pygplates.__version__)"

Thanks.

Regards,
John

Hi Lavie,

Thanks for sending the information.

The crash is caused by a bug in GPlates 2.1 that was fixed in 2.2. Using GPlates 2.2 I was able to load your GPML file without crashing.

Just so you know, when you add a property (using pyGPlates) that is not in the GPlates information model, similar to what you (correctly) did, such as…

number_of_points_property_name = pygplates.PropertyName.create_gpml('number_of_points')
number_of_points_property_value = pygplates.XsInteger(10)

feature.add(
    number_of_points_property_name,
    number_of_points_property_value,
    pygplates.VerifyInformationModel.no)

…then load it into GPlates 2.2, it will be uninterpreted (instead of 10 in the example above)…

image

Despite GPlates 2.1+ loosening up the information model to allow any feature to contain any property, it still restricts properties to those defined in the information model. Future GPlates versions will likely loosen this further to allow user-defined properties (which can already be created in pyGPlates using the pygplates.VerifyInformationModel.no flag, as you are doing). In which case the above example would show up in a future GPlates as 10 (instead of uninterpreted).

By the way, I like the way you use pyGPlates - keep up the good work!

Regards,
John

Dear John:

Thank you so much for your great help. Also, thank you so much to you and everyone for working hard to produce and share with us wonderful tools such as GPlates and pygplates.

Hi John,

I would like to use this option too, but am running into some problems. How do I get the property and its value back later on?

I did something like (with a different name and different value):

Then I would like to use the value later on in pyGPlates, to read I use:
number_of_points = feature.get_value(number_of_points)

This gives the error
NameError: name 'number_of_points' is not defined

Also 'gpml:number_of_points', gpml_number_of_points and pygplates.PropertyName.number_of_points result in errors.

So I checked with

for p in feature:
    print(p)

which returns a property, value combination of:
gpml:number_of_points [ gpml:number_of_points ]

In GPlates:
<number_of_points xmlns="http://www.gplates.org/gplates">10</number_of_points >

Hi Thomas,

You can use:

number_of_points_property_value = feature.get_value(number_of_points_property_name)
number_of_points = number_of_points_property_value.get_integer()

…where get_value() expects a PropertyName type. I’ve just used the same as was used to add to the feature. Then get_value() returns the XsInteger originally added to the feature, and calling get_integer() on that extracts the integer from within it.

It’s probably easier to combine the above two lines into one:

number_of_points = feature.get_value(number_of_points_property_name).get_integer()

So while that GPML looks fine, it will be un-interpreted when loaded into GPlates as noted above. Although I understand this won’t affect you if you stay in pyGPlates land.

Regards,
John

Hi John,

Thanks for the rapid reply!
Your suggestion works when I put it directly after assigning the property to the feature.
Now I would like to use the value in a different function. There I just can’t get it to work.
Do you have a suggestion for that too?

Cheers,
Thomas

Hi Thomas,

That might be because the variable number_of_points_property_name is no longer in scope in a different function.

In the new function you can just assign it the same as in the original function:

number_of_points_property_name = pygplates.PropertyName.create_gpml('number_of_points')

…and then use it:

number_of_points = feature.get_value(number_of_points_property_name).get_integer()

Regards,
John

Hi John,

I did try that before and it didn’t work (should have included that in the question). If I put in the new function:

number_of_points_property_name = pygplates.PropertyName.create_gpml('number_of_points')
print(number_of_points_property_name)
print(feature.get_value(number_of_points_property_name))
number_of_points = feature.get_value(number_of_points_property_name).get_integer()

It returns:

gpml:number_of_points
gpml:number_of_points
AttributeError: 'PropertyValue' Object has no attribute 'get_integer'

I did try number_of_points = feature.get_value(number_of_points_property_name).get_value(number_of_points_property_name).get_integer() but that results in the same error.
Regards,
Thomas

Hmm, can you show me your code that adds the property to the feature?

If I run the following it works fine:

import pygplates

feature = pygplates.Feature()

number_of_points_property_name = pygplates.PropertyName.create_gpml('number_of_points')
number_of_points_property_value = pygplates.XsInteger(10)

feature.add(
    number_of_points_property_name,
    number_of_points_property_value,
    pygplates.VerifyInformationModel.no)

number_of_points_property_name = pygplates.PropertyName.create_gpml('number_of_points')
print(number_of_points_property_name)
print(feature.get_value(number_of_points_property_name))
number_of_points = feature.get_value(number_of_points_property_name).get_integer()
print(number_of_points)

…so it must be something in your call to feature.add().

Actually, I was wrong, that doesn’t look quite right. Instead you should be getting:

<gpml:number_of_points>10</gpml:number_of_points>

…for example if I add the following:

pygplates.FeatureCollection(feature).write('output.gpml')

…and then look at output.gpml with a text editor.

Hi John,

I have written a short code that produces the same error. The crux seems to be the write (and read), until that was added, it didn’t reproduce this error.
I use revision 28 Python 3.7 in Ubuntu Linux (the version with the beaver on the desktop).

initial_feature = pygplates.Feature.create_reconstructable_feature(
    pygplates.FeatureType.gpml_isochron,
    pygplates.PolylineOnSphere([pygplates.PointOnSphere([0,0]), pygplates.PointOnSphere([30,30])]),
    valid_time = (100, 0),
    reconstruction_plate_id = 101,
    conjugate_plate_id = 201,
    reverse_reconstruct = (rotation_model, 100)
    )

begin_time, end_time = initial_feature.get_valid_time()

def create_new_feature(initial_feature, begin_time):
    geometry = initial_feature.get_geometry()

    feature = pygplates.Feature.create_reconstructable_feature(
         pygplates.FeatureType.gpml_isochron,
         geometry
    )

    isochron_age_property_name = pygplates.PropertyName.create_gpml('isochron_age')
    isochron_age_property_value = pygplates.XsDouble(begin_time)

    feature.add(
        isochron_age_property_name,
        isochron_age_property_value,
        pygplates.VerifyInformationModel.no)

    return feature

def read_feature(feature):
    reconstructed_features = []
    pygplates.reconstruct(feature, rotation_model, reconstructed_features, 30)
    for geometry in reconstructed_features:
        feature = geometry.get_feature()
    
    isochron_age_property_name = pygplates.PropertyName.create_gpml('isochron_age')
    for p in feature:
        print(p)

    print(isochron_age_property_name)
    print(feature.get_value(isochron_age_property_name))
    isochron_age = feature.get_value(isochron_age_property_name).get_value(isochron_age_property_name).get_double()
    print(isochron_age)

feature = create_new_feature(initial_feature, begin_time)
pygplates.FeatureCollection(feature).write('../data/temp/output.gpml')

feature_collection = pygplates.FeatureCollection('../data/temp/output.gpml')
for feature in feature_collection:
    read_feature(feature)

Hi Thomas,

Oh I see what the problem is (thanks for posting your code by the way). Actually this is the first time I’ve noticed this problem. The problem has to do with writing out to GPML and then reading back in again (that is, properties not defined in the GPlates information model). On reading back in, they are not interpreted/parsed because it’s not a property that’s recognised/defined by the GPlates information model (so it doesn’t really know how to read/create it). This is different than directly adding a property to a feature using pyGPlates (with pygplates.VerifyInformationModel.no) such as:

feature.add(
    pygplates.PropertyName.create_gpml('isochron_age'),
    pygplates.XsDouble(begin_time),
    pygplates.VerifyInformationModel.no)

…because there you know the type (XsDouble) of the property (ie, you obviously know begin_time is a float and not something else like a string). However when the same property is read from a GPML file, the only information pyGPlates has is:

<gpml:isochron_age>100</gpml:isochron_age>

…which says nothing about the type (ie, 100 could be a string or a number). If this property had been in the information model then GPlates would know that gpml:isochron_age was a number (and be able to load it into an XsDouble or XsInteger rather than an XsString or something else). There are ways to define the type in XML but, for simple non-structural types in GPlates, this is how we currently do it.

This is basically an issue we are starting to deal with, which is loosening up restrictions of the GPlates information model to better allow arbitrary user-defined properties (and also easier/better mapping between attributes in other formats, like Shapefiles, and the GPlates information model).

In the meantime, a workaround is to co-opt one of the existing properties in the GPlates information model. Or, even better, use the Shapefile attributes (which is actually one of those properties whose value is a dictionary of attributes). These attributes will survive writing out to GPML and reading back in again (because their contents are rather simple, and they are not dictated by the GPlates information model):

output_feature.set_shapefile_attribute('isochron_age', begin_time)

# Write feature out to GPML and read back in again.
pygplates.FeatureCollection(output_feature).write('tmp.gpml')
input_feature = list(pygplates.FeatureCollection('tmp.gpml'))[0]

begin_time = input_feature.get_shapefile_attribute('isochron_age')

You can write as many Shapefile attributes as you want. Each attribute can be either a string, integer or float (in your case begin_time is a float). I guess just be careful not to clobber any existing Shapefile attribute (eg, if your feature originally came from a Shapefile that had some of its own attributes that you want to keep around).

Regards,
John