From e937a5db4ec951e69d826599d33f5a65f6cedca2 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 23 Jul 2026 14:38:18 +1200 Subject: [PATCH 1/9] Fix typo in documentation. --- src/cmlibs/utils/zinc/general.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cmlibs/utils/zinc/general.py b/src/cmlibs/utils/zinc/general.py index 6da5de1..a8971d5 100644 --- a/src/cmlibs/utils/zinc/general.py +++ b/src/cmlibs/utils/zinc/general.py @@ -124,7 +124,7 @@ def create_node(field_module, data_object, identifier=-1, node_set_name='nodes', """ Create a Node in the field_module using the data_object. The data object must supply a 'get_field_names' method and a 'get_time_sequence' method. Derive a node data object from the 'AbstractNodeDataObject' class to ensure - that the data object class meets it's requirements. + that the data object class meets its requirements. Optionally use the identifier to set the identifier of the Node created, the time parameter to set the time value in the cache, or the node_set_name to specify which node set to use the default node set @@ -162,6 +162,7 @@ def create_node(field_module, data_object, identifier=-1, node_set_name='nodes', for i, field in enumerate(fields): field_name = field_names[i] field_value = getattr(data_object, field_name)() + # print(field_name, type(field_value), field_value, isinstance(field_value, ("".__class__, u"".__class__))) if isinstance(field_value, ("".__class__, u"".__class__)): field.assignString(field_cache, field_value) else: From 68846154a3500dbe66508ac653ffb9a932c78869 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 23 Jul 2026 14:38:55 +1200 Subject: [PATCH 2/9] Return a dict of matched group names from match_fitting_group_names. --- src/cmlibs/utils/zinc/group.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/cmlibs/utils/zinc/group.py b/src/cmlibs/utils/zinc/group.py index f7e8cfb..43ac3dd 100644 --- a/src/cmlibs/utils/zinc/group.py +++ b/src/cmlibs/utils/zinc/group.py @@ -503,20 +503,25 @@ def match_fitting_group_names(data_fieldmodule, model_fieldmodule, log_diagnosti :param data_fieldmodule: Data Fieldmodule whose group names may be modified. :param model_fieldmodule: Model Fieldmodule containing preferred group names. :param log_diagnostics: Set to True to write diagonstic messages about name matches and changes to logging. + :return: A dictionary of matched group names. """ # future: match with annotation terms model_names = [group.getName() for group in get_group_list(model_fieldmodule)] + matched_names = {} for data_group in get_group_list(data_fieldmodule): data_name = data_group.getName() compare_name = data_name.strip().casefold() for model_name in model_names: + # print('comparing "%s" to "%s" or "%s"' % (data_name, model_name, compare_name)) if model_name == data_name: + matched_names[model_name] = (data_name, None) if log_diagnostics: logger.info("Data group '" + data_name + "' found in model") break elif model_name.strip().casefold() == compare_name: result = data_group.setName(model_name) if result == RESULT_OK: + matched_names[model_name] = (data_name, compare_name) if log_diagnostics: logger.info("Data group '" + data_name + "' found in model as '" + model_name + "'. Renaming to match.") @@ -527,5 +532,8 @@ def match_fitting_group_names(data_fieldmodule, model_fieldmodule, log_diagnosti logger.error(" Reason: field of that name already exists.") break else: + # print('Data group "' + data_name + '" not found in model') if log_diagnostics: logger.info("Data group '" + data_name + "' not found in model") + + return matched_names From cab4b03f635c3e6917b025f8d213530204602988 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 23 Jul 2026 14:40:12 +1200 Subject: [PATCH 3/9] Allow a restriction of field names when using copy_fitting_data. --- src/cmlibs/utils/zinc/region.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/cmlibs/utils/zinc/region.py b/src/cmlibs/utils/zinc/region.py index 429217f..98c9e8f 100644 --- a/src/cmlibs/utils/zinc/region.py +++ b/src/cmlibs/utils/zinc/region.py @@ -11,18 +11,19 @@ def _find_missing(lst): def convert_nodes_to_datapoints(target_region, source_region, source_nodeset_type=Field.DOMAIN_TYPE_NODES, - destroy_after_conversion=True): + destroy_after_conversion=True, field_names=None): """ + When the source nodeset type is Field.DOMAIN_TYPE_DATAPOINTS, then datapoints are transferred from the Converts nodes in the source region to datapoints in the target region, renumbering any existing datapoints in target region to not clash. - When the source nodeset type is Field.DOMAIN_TYPE_DATAPOINTS, then datapoints are transferred from the source region to the target region. :param target_region: Zinc Region to read data into. Existing data points are renumbered to avoid nodes. :param source_region: Zinc Region containing nodes to transfer. - :param source_nodeset_type: Set to Field.DOMAIN_TYPE_DATAPOINTS or Field.DOMAIN_TYPE_NODES to transfer datapoints + :param source_nodeset_type: Set to Field.DOMAIN_TYPE_DATAPOINTS or Field.DOMAIN_TYPE_NODES to transfer datapoints or convert nodes. Datapoint transfer should only be to different regions [default: Field.DOMAIN_TYPE_NODES]. - :param destroy_after_conversion: Set to True to destroy nodes that have been successfully converted, or False + :param destroy_after_conversion: Set to True to destroy nodes that have been successfully converted, or False to leave intact in source region [default: True]. + :param field_names: A list of field names to output. """ source_fieldmodule = source_region.getFieldmodule() target_fieldmodule = target_region.getFieldmodule() @@ -46,11 +47,14 @@ def convert_nodes_to_datapoints(target_region, source_region, source_nodeset_typ datapoint_identifier = datapoint.getIdentifier() if datapoint_identifier in existing_nodes_identifiers_set and len(available_identifiers): next_identifier = available_identifiers.pop(0) + elif datapoint_identifier not in existing_nodes_identifiers_set: + next_identifier = datapoint_identifier else: max_identifier += 1 next_identifier = max_identifier - identifier_map[datapoint_identifier] = next_identifier + if next_identifier != datapoint_identifier: + identifier_map[datapoint_identifier] = next_identifier datapoint = datapoint_iterator.next() for current_identifier, new_identifier in identifier_map.items(): @@ -58,7 +62,7 @@ def convert_nodes_to_datapoints(target_region, source_region, source_nodeset_typ datapoint.setIdentifier(new_identifier) # transfer nodes as datapoints to target_region - buffer = write_to_buffer(source_region, resource_domain_type=source_nodeset_type) + buffer = write_to_buffer(source_region, resource_domain_type=source_nodeset_type, field_names=field_names) if source_nodeset_type == Field.DOMAIN_TYPE_NODES: buffer = buffer.replace(bytes("!#nodeset nodes", "utf-8"), bytes("!#nodeset datapoints", "utf-8")) result = read_from_buffer(target_region, buffer) @@ -68,17 +72,18 @@ def convert_nodes_to_datapoints(target_region, source_region, source_nodeset_typ nodes.destroyAllNodes() -def copy_fitting_data(target_region, source_region): +def copy_fitting_data(target_region, source_region, field_names=None): """ Copy nodes and data points from source_region to target_region, converting nodes to data points and offsetting data point identifiers to not clash. All groups and fields in use are transferred. This is used for setting up fitting problems where data needs to be in datapoints only. :param target_region: Zinc Region to read nodes/data into. :param source_region: Zinc Region containing nodes/data to transfer. Unmodified. + :param field_names: A list of field names to output. """ for domain_type in [Field.DOMAIN_TYPE_DATAPOINTS, Field.DOMAIN_TYPE_NODES]: convert_nodes_to_datapoints(target_region, source_region, source_nodeset_type=domain_type, - destroy_after_conversion=False) + destroy_after_conversion=False, field_names=field_names) def copy_nodeset(region, nodeset): From e68af710a1c1c76911c387081e160aa80eb24363 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 23 Jul 2026 14:41:12 +1200 Subject: [PATCH 4/9] Add tests fro transferring nodes and matching fitted group names. --- src/cmlibs/utils/zinc/field.py | 4 +- tests/test_zinc_group.py | 35 +++++- tests/test_zinc_region.py | 220 +++++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+), 6 deletions(-) diff --git a/src/cmlibs/utils/zinc/field.py b/src/cmlibs/utils/zinc/field.py index 8e88638..360bf35 100644 --- a/src/cmlibs/utils/zinc/field.py +++ b/src/cmlibs/utils/zinc/field.py @@ -554,7 +554,7 @@ def create_field_stored_mesh_location(fieldmodule: Fieldmodule, mesh: Mesh, name :param fieldmodule: Zinc fieldmodule to find or create field in. :param mesh: Mesh to store locations in, from same fieldmodule. - :param name: Name of new field. If not defined, defaults to "location\_" + mesh.getName(). + :param name: Name of new field. If not defined, defaults to "location_" + mesh.getName(). :param managed: Managed state of field. :return: Zinc FieldStoredMeshLocation """ @@ -576,7 +576,7 @@ def find_or_create_field_stored_mesh_location(fieldmodule: Fieldmodule, mesh: Me :param fieldmodule: Zinc fieldmodule to find or create field in. :param mesh: Mesh to store locations in, from same fieldmodule. - :param name: Name of new field. If not defined, defaults to "location\_" + mesh.getName(). + :param name: Name of new field. If not defined, defaults to "location_" + mesh.getName(). :param managed: Managed state of field if created here. """ if not name: diff --git a/tests/test_zinc_group.py b/tests/test_zinc_group.py index 9ca13ce..00e19d1 100644 --- a/tests/test_zinc_group.py +++ b/tests/test_zinc_group.py @@ -1,4 +1,3 @@ -import os import unittest from cmlibs.utils.zinc.field import find_or_create_field_group from cmlibs.utils.zinc.group import ( @@ -8,6 +7,7 @@ from cmlibs.zinc.element import Element from cmlibs.zinc.field import Field from cmlibs.zinc.result import RESULT_OK + from utilities import assert_almost_equal_list, get_test_resource_name @@ -136,7 +136,6 @@ def test_group_add_compare_group_local_contents(self): self.assertEqual(0, group1.getMeshGroup(mesh1d).getSize()) self.assertEqual(0, group1.getNodesetGroup(nodes).getSize()) - def test_match_fitting_group_names(self): """ Test utility functions for adding and comparing group local contents. @@ -154,8 +153,36 @@ def test_match_fitting_group_names(self): data_group_fred = find_or_create_field_group(data_fieldmodule, " fRed\t") data_group_two_names = find_or_create_field_group(data_fieldmodule, "\t two NAMES ") - match_fitting_group_names(data_fieldmodule, model_fieldmodule, log_diagnostics=True) - + names = match_fitting_group_names(data_fieldmodule, model_fieldmodule, log_diagnostics=True) self.assertEqual(data_group_bob.getName(), "bob") self.assertEqual(data_group_fred.getName(), "fred") self.assertEqual(data_group_two_names.getName(), "two names") + self.assertIn("two names", names) + self.assertEqual(names["two names"], ('\t two NAMES ', 'two names')) + self.assertIn("bob", names) + self.assertEqual(names["bob"], (' Bob', 'bob')) + + def test_match_fitting_group_names_cap(self): + """ + Test utility functions for adding and comparing group local contents. + """ + context = Context("test") + model_region = context.createRegion() + model_fieldmodule = model_region.getFieldmodule() + find_or_create_field_group(model_fieldmodule, "Bob", managed=True) + find_or_create_field_group(model_fieldmodule, "fRed", managed=True) + find_or_create_field_group(model_fieldmodule, "james", managed=True) + + data_region = context.createRegion() + data_fieldmodule = data_region.getFieldmodule() + data_group_bob = find_or_create_field_group(data_fieldmodule, "bob") + data_group_fred = find_or_create_field_group(data_fieldmodule, "fred") + find_or_create_field_group(data_fieldmodule, "james") + + names = match_fitting_group_names(data_fieldmodule, model_fieldmodule, log_diagnostics=True) + self.assertEqual(data_group_bob.getName(), "Bob") + self.assertEqual(data_group_fred.getName(), "fRed") + self.assertIn("Bob", names) + self.assertEqual(names["Bob"], ('bob', 'bob')) + self.assertIn("james", names) + self.assertEqual(names["james"], ('james', None)) diff --git a/tests/test_zinc_region.py b/tests/test_zinc_region.py index de63154..95026e6 100644 --- a/tests/test_zinc_region.py +++ b/tests/test_zinc_region.py @@ -139,6 +139,226 @@ def test_copy_dataset_II(self): self.assertEqual(4, datapoints.getSize()) self.assertEqual(4, target_datapoints.getSize()) + def test_transfer_nodes_IV(self): + """ + Test zinc region transferring nodes over datapoints. + """ + context = Context("test") + source_region = context.createRegion() + target_region = context.createRegion() + source_fieldmodule = source_region.getFieldmodule() + target_fieldmodule = target_region.getFieldmodule() + source_coordinates = find_or_create_field_coordinates(source_fieldmodule) + target_coordinates = find_or_create_field_coordinates(target_fieldmodule) + + nodes = source_fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + target_datapoints = target_fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) + + node_coordinates = [[1.0] * 3, [2.0] * 3, [3.0] * 3] + datapoint_coordinates = [[-1.0] * 3, [-2.0] * 3, [-3.0] * 3] + reidentify_nodes = {3: 4} + + create_nodes(source_coordinates, node_coordinates, node_set=nodes) + create_nodes(target_coordinates, datapoint_coordinates, node_set=target_datapoints) + + for node_identifier in reidentify_nodes: + node = nodes.findNodeByIdentifier(node_identifier) + node.setIdentifier(reidentify_nodes[node_identifier]) + + nodeset_group = source_fieldmodule.createFieldGroup() + nodeset_group.setName('nodes_group') + nodeset = nodeset_group.getOrCreateNodesetGroup(nodes) + nodeset.addNode(nodes.findNodeByIdentifier(2)) + nodeset.addNode(nodes.findNodeByIdentifier(4)) + + self.assertEqual(2, nodeset.getSize()) + + datapointsset_group = target_fieldmodule.createFieldGroup() + datapointsset_group.setName('datapoints_group') + datapointsset = datapointsset_group.getOrCreateNodesetGroup(target_datapoints) + datapointsset.addNode(target_datapoints.findNodeByIdentifier(2)) + datapointsset.addNode(target_datapoints.findNodeByIdentifier(3)) + + self.assertEqual(2, datapointsset.getSize()) + + self.assertEqual(3, nodes.getSize()) + self.assertEqual(3, target_datapoints.getSize()) + + convert_nodes_to_datapoints(target_region, source_region) + + self.assertEqual(0, nodes.getSize()) + self.assertEqual(6, target_datapoints.getSize()) + + ni = target_datapoints.createNodeiterator() + datapoint = ni.next() + fc = target_fieldmodule.createFieldcache() + while datapoint.isValid(): + current_identifier = datapoint.getIdentifier() + fc.setNode(datapoint) + _, values = target_coordinates.evaluateReal(fc, 3) + if current_identifier == 1: + self.assertEqual(1, values[0]) + elif current_identifier == 2: + self.assertEqual(2, values[0]) + elif current_identifier == 3: + self.assertEqual(-3, values[0]) + elif current_identifier == 4: + self.assertEqual(3, values[0]) + elif current_identifier == 5: + self.assertEqual(-1, values[0]) + elif current_identifier == 6: + self.assertEqual(-2, values[0]) + datapoint = ni.next() + + ni = datapointsset.createNodeiterator() + datapoint = ni.next() + while datapoint.isValid(): + identifier = datapoint.getIdentifier() + self.assertIn(identifier, [3, 6]) + datapoint = ni.next() + + nodeset_group = target_fieldmodule.findFieldByName("nodes_group").castGroup() + nodeset = nodeset_group.getNodesetGroup(target_datapoints) + ni = nodeset.createNodeiterator() + datapoint = ni.next() + while datapoint.isValid(): + identifier = datapoint.getIdentifier() + self.assertIn(identifier, [2, 4]) + datapoint = ni.next() + + def test_transfer_nodes_V(self): + """ + Test zinc region transferring nodes over datapoints. + """ + context = Context("test") + source_region = context.createRegion() + target_region = context.createRegion() + source_fieldmodule = source_region.getFieldmodule() + target_fieldmodule = target_region.getFieldmodule() + source_coordinates = find_or_create_field_coordinates(source_fieldmodule) + target_coordinates = find_or_create_field_coordinates(target_fieldmodule) + + nodes = source_fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + target_datapoints = target_fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) + + node_coordinates = [[1.0] * 3, [2.0] * 3, [3.0] * 3, [4.0] * 3, [5.0] * 3] + datapoint_coordinates = [[-1.0] * 3, [-2.0] * 3, [-3.0] * 3, [-4.0] * 3, [-5.0] * 3, [-6.0] * 3, [-7.0] * 3] + + create_nodes(source_coordinates, node_coordinates, node_set=nodes) + create_nodes(target_coordinates, datapoint_coordinates, node_set=target_datapoints) + + reidentify_nodes = {1: 6, 2: 7, 4: 8} + for node_identifier in reidentify_nodes: + node = nodes.findNodeByIdentifier(node_identifier) + node.setIdentifier(reidentify_nodes[node_identifier]) + + reidentify_nodes = {1: 8, 2: 9, 4: 10} + for node_identifier in reidentify_nodes: + node = target_datapoints.findNodeByIdentifier(node_identifier) + node.setIdentifier(reidentify_nodes[node_identifier]) + + self.assertEqual(5, nodes.getSize()) + self.assertEqual(7, target_datapoints.getSize()) + + convert_nodes_to_datapoints(target_region, source_region) + + self.assertEqual(0, nodes.getSize()) + self.assertEqual(12, target_datapoints.getSize()) + + expected_values = { + 1: [-3.0, -3.0, -3.0], + 2: [-5.0, -5.0, -5.0], + 3: [3.0, 3.0, 3.0], + 4: [-6.0, -6.0, -6.0], + 5: [5.0, 5.0, 5.0], + 6: [1.0, 1.0, 1.0], + 7: [2.0, 2.0, 2.0], + 8: [4.0, 4.0, 4.0], + 9: [-2.0, -2.0, -2.0], + 10: [-4.0, -4.0, -4.0], + 11: [-7.0, -7.0, -7.0], + 12: [-1.0, -1.0, -1.0], + } + ni = target_datapoints.createNodeiterator() + fc = target_fieldmodule.createFieldcache() + datapoint = ni.next() + while datapoint.isValid(): + fc.setNode(datapoint) + identifier = datapoint.getIdentifier() + _, value = target_coordinates.evaluateReal(fc, 3) + self.assertEqual(expected_values[identifier], value) + datapoint = ni.next() + + def test_transfer_nodes_VI(self): + """ + Test zinc region transferring nodes over datapoints filtering fields. + """ + context = Context("test") + source_region = context.createRegion() + target_region = context.createRegion() + source_fieldmodule = source_region.getFieldmodule() + target_fieldmodule = target_region.getFieldmodule() + source_coordinates = find_or_create_field_coordinates(source_fieldmodule) + alt_coordinates = find_or_create_field_coordinates(source_fieldmodule, name='alt_coordinates') + + source_datapoints = source_fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) + source_nodes = source_fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + + node_coordinates = [[1.0] * 3, [2.0] * 3, [3.0] * 3, [4.0] * 3, [5.0] * 3] + datapoint_coordinates = [[-1.0] * 3, [-2.0] * 3, [-3.0] * 3, [-4.0] * 3, [-5.0] * 3, [-6.0] * 3, [-7.0] * 3] + + group_1 = source_fieldmodule.createFieldGroup() + group_2 = source_fieldmodule.createFieldGroup() + group_3 = source_fieldmodule.createFieldGroup() + + group_1.setName("group_1") + group_2.setName("group_2") + group_3.setName("group_3") + + create_nodes(source_coordinates, datapoint_coordinates, node_set=source_datapoints) + create_nodes(alt_coordinates, node_coordinates, node_set=source_nodes) + + dataset_group = group_1.getOrCreateNodesetGroup(source_datapoints) + dataset_group.addNode(source_datapoints.findNodeByIdentifier(1)) + dataset_group.addNode(source_datapoints.findNodeByIdentifier(2)) + + dataset_group = group_2.getOrCreateNodesetGroup(source_datapoints) + dataset_group.addNode(source_datapoints.findNodeByIdentifier(5)) + dataset_group.addNode(source_datapoints.findNodeByIdentifier(6)) + + dataset_group = group_3.getOrCreateNodesetGroup(source_nodes) + dataset_group.addNode(source_datapoints.findNodeByIdentifier(2)) + dataset_group.addNode(source_datapoints.findNodeByIdentifier(3)) + + fi = target_fieldmodule.createFielditerator() + field = fi.next() + field_count = 0 + while field.isValid(): + field_count += 1 + print('b4:', field.getName()) + field = fi.next() + + fi = source_fieldmodule.createFielditerator() + field = fi.next() + while field.isValid(): + print('src:', field.getName()) + field = fi.next() + + self.assertEqual(0, field_count) + + convert_nodes_to_datapoints(target_region, source_region, source_nodeset_type=Field.DOMAIN_TYPE_DATAPOINTS, field_names=['group_1', 'coordinates']) + + fi = target_fieldmodule.createFielditerator() + field = fi.next() + field_count = 0 + while field.isValid(): + field_count += 1 + print('a4:', field.getName()) + field = fi.next() + + self.assertEqual(2, field_count) + + if __name__ == "__main__": unittest.main() From 5ab16e7362c17e42bf177c26c9bbf5ef3eb208ed Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 30 Jul 2026 13:26:10 +1200 Subject: [PATCH 5/9] Add an efficient file hashing routine. --- src/cmlibs/utils/misc.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/cmlibs/utils/misc.py diff --git a/src/cmlibs/utils/misc.py b/src/cmlibs/utils/misc.py new file mode 100644 index 0000000..fa52e6f --- /dev/null +++ b/src/cmlibs/utils/misc.py @@ -0,0 +1,20 @@ +import hashlib + + +# Source - https://stackoverflow.com/a/44873382 +# Posted by maxschlepzig, modified by community. See post 'Timeline' for change history +# Retrieved 2026-07-30, License - CC BY-SA 4.0 +def sha256sum(filename): + """ + An efficient file hashing utility that avoids line-ending issues. + + :param filename: The name of the files content to hash. + :return: The hash of the file contents. + """ + h = hashlib.sha256() + b = bytearray(1024 * 1024) + mv = memoryview(b) + with open(filename, 'rb', buffering=0) as f: + while n := f.readinto(mv): + h.update(mv[:n]) + return h.hexdigest() From b0c8638b09fcf5195f7c6984730e90f2d66d2cb9 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 30 Jul 2026 13:37:39 +1200 Subject: [PATCH 6/9] Add test for sha256sum function. --- tests/test_misc.py | 17 +++++++++++++++++ tests/test_zinc_region.py | 13 ++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 tests/test_misc.py diff --git a/tests/test_misc.py b/tests/test_misc.py new file mode 100644 index 0000000..7967b22 --- /dev/null +++ b/tests/test_misc.py @@ -0,0 +1,17 @@ +import unittest + +from cmlibs.utils.misc import sha256sum + +from utilities import get_test_resource_name + +class ZincUtilsTestCase(unittest.TestCase): + + def test_sha_calculation(self): + exf_file = get_test_resource_name('two_element_cube.exf') + sha256 = sha256sum(exf_file) + expected_sha256 = '4ebcf6ad54c7444855becbf43390fcd3f9e469c0ac31b5442810c91f91ec5ee5' + self.assertEqual(expected_sha256, sha256) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_zinc_region.py b/tests/test_zinc_region.py index 95026e6..558fc9b 100644 --- a/tests/test_zinc_region.py +++ b/tests/test_zinc_region.py @@ -335,16 +335,18 @@ def test_transfer_nodes_VI(self): field_count = 0 while field.isValid(): field_count += 1 - print('b4:', field.getName()) field = fi.next() + self.assertEqual(0, field_count) + fi = source_fieldmodule.createFielditerator() field = fi.next() + src_field_count = 0 while field.isValid(): - print('src:', field.getName()) + src_field_count += 1 field = fi.next() - self.assertEqual(0, field_count) + self.assertEqual(6, src_field_count) convert_nodes_to_datapoints(target_region, source_region, source_nodeset_type=Field.DOMAIN_TYPE_DATAPOINTS, field_names=['group_1', 'coordinates']) @@ -353,10 +355,11 @@ def test_transfer_nodes_VI(self): field_count = 0 while field.isValid(): field_count += 1 - print('a4:', field.getName()) field = fi.next() - self.assertEqual(2, field_count) + # Group fields do not get dropped by the stream information region field restriction. + # This means we only lose the 'alt_coordinates' field from the source region. + self.assertEqual(5, field_count) From 92243bd9401e810eca3e694bc3632d21728beb96 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 30 Jul 2026 13:49:32 +1200 Subject: [PATCH 7/9] Forcibly normalize line endings when calculating sha256 of file content. --- src/cmlibs/utils/misc.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/cmlibs/utils/misc.py b/src/cmlibs/utils/misc.py index fa52e6f..49e790a 100644 --- a/src/cmlibs/utils/misc.py +++ b/src/cmlibs/utils/misc.py @@ -5,16 +5,38 @@ # Posted by maxschlepzig, modified by community. See post 'Timeline' for change history # Retrieved 2026-07-30, License - CC BY-SA 4.0 def sha256sum(filename): + h = hashlib.sha256() + b = bytearray(1024 * 1024) + mv = memoryview(b) + with open(filename, 'rb', buffering=0) as f: + while n := f.readinto(mv): + h.update(mv[:n]) + return h.hexdigest() +import hashlib + +def sha256sum_normalized(filename, normalize=True): """ An efficient file hashing utility that avoids line-ending issues. :param filename: The name of the files content to hash. + :param normalize: Normalize line endings in the file. :return: The hash of the file contents. """ h = hashlib.sha256() - b = bytearray(1024 * 1024) - mv = memoryview(b) + with open(filename, 'rb', buffering=0) as f: - while n := f.readinto(mv): - h.update(mv[:n]) - return h.hexdigest() + last_was_cr = False + + while chunk := f.read(1024 * 1024): + if normalize: + chunk = chunk.replace(b"\r\n", b"\n") + + if last_was_cr and chunk.startswith(b"\n"): + chunk = chunk[1:] + + last_was_cr = chunk.endswith(b"\r") + chunk = chunk.replace(b"\r", b"\n") + + h.update(chunk) + + return h.hexdigest() \ No newline at end of file From 6def3108d2336800e3100cc863667f161ae0d089 Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 30 Jul 2026 13:51:54 +1200 Subject: [PATCH 8/9] Use normalized sha256 sum algorithm. --- src/cmlibs/utils/misc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cmlibs/utils/misc.py b/src/cmlibs/utils/misc.py index 49e790a..e837485 100644 --- a/src/cmlibs/utils/misc.py +++ b/src/cmlibs/utils/misc.py @@ -4,7 +4,7 @@ # Source - https://stackoverflow.com/a/44873382 # Posted by maxschlepzig, modified by community. See post 'Timeline' for change history # Retrieved 2026-07-30, License - CC BY-SA 4.0 -def sha256sum(filename): +def sha256sum_old(filename): h = hashlib.sha256() b = bytearray(1024 * 1024) mv = memoryview(b) @@ -14,7 +14,7 @@ def sha256sum(filename): return h.hexdigest() import hashlib -def sha256sum_normalized(filename, normalize=True): +def sha256sum(filename, normalize=True): """ An efficient file hashing utility that avoids line-ending issues. From 5b2089b92d5727e762149eb3fbbb95a03fe1701c Mon Sep 17 00:00:00 2001 From: Hugh Sorby Date: Thu, 30 Jul 2026 13:54:33 +1200 Subject: [PATCH 9/9] Tidy up misc.py. --- src/cmlibs/utils/misc.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/cmlibs/utils/misc.py b/src/cmlibs/utils/misc.py index e837485..0a83dfc 100644 --- a/src/cmlibs/utils/misc.py +++ b/src/cmlibs/utils/misc.py @@ -1,19 +1,6 @@ import hashlib -# Source - https://stackoverflow.com/a/44873382 -# Posted by maxschlepzig, modified by community. See post 'Timeline' for change history -# Retrieved 2026-07-30, License - CC BY-SA 4.0 -def sha256sum_old(filename): - h = hashlib.sha256() - b = bytearray(1024 * 1024) - mv = memoryview(b) - with open(filename, 'rb', buffering=0) as f: - while n := f.readinto(mv): - h.update(mv[:n]) - return h.hexdigest() -import hashlib - def sha256sum(filename, normalize=True): """ An efficient file hashing utility that avoids line-ending issues. @@ -39,4 +26,4 @@ def sha256sum(filename, normalize=True): h.update(chunk) - return h.hexdigest() \ No newline at end of file + return h.hexdigest()