diff --git a/BHoM_Engine.sln b/BHoM_Engine.sln index 7c6ddd0b8..16bbfe0ba 100644 --- a/BHoM_Engine.sln +++ b/BHoM_Engine.sln @@ -55,6 +55,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Analytical_Engine", "Analyt EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Physical_Engine", "Physical_Engine\Physical_Engine.csproj", "{F2073888-9BD8-4D8F-9B3C-27577B388530}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Diffing_Engine", "Diffing_Engine\Diffing_Engine.csproj", "{073DFD36-0829-4792-8C32-67BF692A9413}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -232,6 +234,14 @@ Global {F2073888-9BD8-4D8F-9B3C-27577B388530}.Release|Any CPU.Build.0 = Release|Any CPU {F2073888-9BD8-4D8F-9B3C-27577B388530}.Release|x86.ActiveCfg = Release|Any CPU {F2073888-9BD8-4D8F-9B3C-27577B388530}.Release|x86.Build.0 = Release|Any CPU + {073DFD36-0829-4792-8C32-67BF692A9413}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {073DFD36-0829-4792-8C32-67BF692A9413}.Debug|Any CPU.Build.0 = Debug|Any CPU + {073DFD36-0829-4792-8C32-67BF692A9413}.Debug|x86.ActiveCfg = Debug|Any CPU + {073DFD36-0829-4792-8C32-67BF692A9413}.Debug|x86.Build.0 = Debug|Any CPU + {073DFD36-0829-4792-8C32-67BF692A9413}.Release|Any CPU.ActiveCfg = Release|Any CPU + {073DFD36-0829-4792-8C32-67BF692A9413}.Release|Any CPU.Build.0 = Release|Any CPU + {073DFD36-0829-4792-8C32-67BF692A9413}.Release|x86.ActiveCfg = Release|Any CPU + {073DFD36-0829-4792-8C32-67BF692A9413}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BHoM_Engine/Create/RandomObject.cs b/BHoM_Engine/Create/RandomObject.cs index 09cc501fe..66e595bbc 100644 --- a/BHoM_Engine/Create/RandomObject.cs +++ b/BHoM_Engine/Create/RandomObject.cs @@ -94,7 +94,7 @@ private static void LinkInterfaces() private static object InitialiseObject(Type type, Random rnd, int depth = 0) { // Create object - object obj; + object obj = null; if (type.GetInterface("IImmutable") != null) return CreateImmutable(type, rnd, depth); else if (type.IsGenericType) @@ -102,9 +102,15 @@ private static object InitialiseObject(Type type, Random rnd, int depth = 0) type = GetType(type); obj = Activator.CreateInstance(type); } + else if (typeof(BH.oM.Base.IBHoMFragment).IsAssignableFrom(type)) + { + // Do not instantiate random Fragments to avoid "missing parameterless constructor" exception + return null; + } else obj = Activator.CreateInstance(type); + // Set its public properties foreach (PropertyInfo prop in type.GetProperties()) { diff --git a/Diffing_Engine/Compute/Diffing.cs b/Diffing_Engine/Compute/Diffing.cs new file mode 100644 index 000000000..dfb69d392 --- /dev/null +++ b/Diffing_Engine/Compute/Diffing.cs @@ -0,0 +1,180 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2019, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base; +using BH.Engine; +using BH.oM.Data.Collections; +using BH.oM.Diffing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Security.Cryptography; +using System.Reflection; +using BH.Engine.Serialiser; +using System.ComponentModel; +using BH.oM.Reflection.Attributes; +using BH.oM.Testing; +using BH.oM.Reflection; + +namespace BH.Engine.Diffing +{ + public static partial class Compute + { + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [Description("Returns a Delta object with the differences between the Stream objects and the provided object list.")] + [Input("previousStream", "A previous version of a Stream")] + [Input("currentStream", "A new version of a Stream")] + [Input("enablePropertyDiff", "If true, enables the Property-level diffing, which returns the differences down to the individual object properties.")] + [Input("exceptions", "List of strings specifying the names of the properties that should be ignored in the comparison, e.g. 'BHoM_Guid'")] + [Input("useDefaultExceptions", "If true, adds a list of default exceptions: 'BHoM_Guid', 'CustomData', 'Fragments'. Defaults to true.")] + public static Delta Diffing(Stream previousStream, Stream currentStream, bool enablePropertyDiff = true, List exceptions = null, bool useDefaultExceptions = true) + { + // Take the Stream's objects + List currentObjs = currentStream.Objects.ToList(); + List readObjs = previousStream.Objects.ToList(); + + // Make dictionary with object hashes to speed up the next lookups + Dictionary readObjs_dict = readObjs.ToDictionary(obj => obj.GetHashFragment().Hash, obj => obj); + + // Set exceptions + if (useDefaultExceptions) + Compute.SetDefaultExceptions(ref exceptions); + + // Dispatch the objects: new, modified or old + List toBeCreated = new List(); + List toBeUpdated = new List(); + List toBeDeleted = new List(); + var objModifiedProps = new Dictionary>>(); + + foreach (var obj in currentObjs) + { + var hashFragm = obj.GetHashFragment(); + + if (hashFragm?.PreviousHash == null) + { + toBeCreated.Add(obj); // It's a new object + } + + else if (hashFragm.PreviousHash == hashFragm.Hash) + { + // It's NOT been modified + } + + else if (hashFragm.PreviousHash != hashFragm.Hash) + { + toBeUpdated.Add(obj); // It's been modified + + if (enablePropertyDiff) + { + // Determine changed properties + IBHoMObject oldObjState = null; + readObjs_dict.TryGetValue(hashFragm.PreviousHash, out oldObjState); + + if (oldObjState == null) continue; + + IsEqualConfig config = new IsEqualConfig(); + config.PropertiesToIgnore = exceptions; + + var differentProps = BH.Engine.Testing.Query.DifferentProperties(obj, oldObjState, config); + + objModifiedProps.Add(hashFragm.Hash, differentProps); + } + } + else + { + throw new Exception("Could not find hash information to perform Diffing on some objects."); + } + } + + // If no modified property was found, set the field to null (otherwise will get empty list) + objModifiedProps = objModifiedProps.Count == 0 ? null : objModifiedProps; + + // All ReadObjs that cannot be found by hash in the previousHash of the CurrentObjs are toBeDeleted + Dictionary CurrentObjs_withPreviousHash_dict = currentObjs + .Where(obj => obj.GetHashFragment().PreviousHash != null) + .ToDictionary(obj => obj.GetHashFragment().PreviousHash, obj => obj); + + toBeDeleted = readObjs_dict.Keys.Except(CurrentObjs_withPreviousHash_dict.Keys) + .Where(k => readObjs_dict.ContainsKey(k)).Select(k => readObjs_dict[k]).ToList(); + + return new Delta(toBeCreated, toBeDeleted, toBeUpdated, objModifiedProps); + + } + + [Description("Dispatch objects in two sets into the ones exclusive to one set, the other, or both.")] + [Input("setA", "A previous version of a Stream")] + [Input("setB", "A new version of a Stream")] + [Input("exceptions", "List of strings specifying the names of the properties that should be ignored in the comparison, e.g. 'BHoM_Guid'")] + [Input("useDefaultExceptions", "If true, adds a list of default exceptions: 'BHoM_Guid', 'CustomData', 'Fragments'. Defaults to true.")] + [MultiOutputAttribute(0, "OnlySetA", "Object existing exclusively in setA")] + [MultiOutputAttribute(1, "Intersection-A", "Objects existing in both sets. Returns objects originally from set A.")] + [MultiOutputAttribute(2, "Intersection-B", "Objects existing in both sets. Returns objects originally from set B.")] + [MultiOutputAttribute(3, "OnlySetB", "Object existing exclusively in setB")] + public static Output, List, List, List> Diffing(IEnumerable setA, IEnumerable setB, List exceptions = null, bool useDefaultExceptions = true) + { + Stream streamA = BH.Engine.Diffing.Create.Stream(setA, null); + Stream streamB = BH.Engine.Diffing.Create.Stream(setB, null); + + VennDiagram diagram = Engine.Data.Create.VennDiagram(streamA.Objects, streamB.Objects, new HashFragmComparer()); + + return new Output, List, List, List> + { + Item1 = diagram.OnlySet1, + Item2 = diagram.Intersection.Select(tuple => tuple.Item1).ToList(), + Item3 = diagram.Intersection.Select(tuple => tuple.Item2).ToList(), + Item4 = diagram.OnlySet2, + }; + } + + + /***************************************************/ + + + /***************************************************/ + /**** Public Fields ****/ + /***************************************************/ + + public class HashFragmComparer : IEqualityComparer where T : IBHoMObject + { + public bool Equals(T x, T y) + { + string firstHash = x.GetHashFragment().Hash; + string secHash = y.GetHashFragment().Hash; + if (!string.IsNullOrEmpty(firstHash) && !string.IsNullOrEmpty(secHash) && firstHash == secHash) + return true; + else + return false; + } + public int GetHashCode(T obj) + { + return obj.GetHashFragment().GetHashCode(); + } + } + + /***************************************************/ + } +} diff --git a/Diffing_Engine/Compute/DiffingHash.cs b/Diffing_Engine/Compute/DiffingHash.cs new file mode 100644 index 000000000..5c153cd66 --- /dev/null +++ b/Diffing_Engine/Compute/DiffingHash.cs @@ -0,0 +1,75 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2019, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base; +using BH.oM.Data.Collections; +using BH.oM.Diffing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Security.Cryptography; +using System.Reflection; +using BH.Engine.Serialiser; +using BH.oM.Reflection.Attributes; +using System.ComponentModel; + +namespace BH.Engine.Diffing +{ + public static partial class Compute + { + ///***************************************************/ + ///**** Public Fields ****/ + ///***************************************************/ + + public static List defaultHashExceptions = new List() { "BHoM_Guid", "CustomData", "Fragments" }; + + ///***************************************************/ + ///**** Public Methods ****/ + ///***************************************************/ + + [Description("Computes the hash code required for the Diffing.")] + [Input("objects", "Objects the hash code should be calculated for")] + [Input("exceptions", "List of strings specifying the names of the properties that should be ignored in the calculation, e.g. BHoM_Guid")] + [Input("useDefaultExceptions", "If true, adds a list of default exceptions: 'BHoM_Guid', 'CustomData', 'Fragments'. Defaults to true.")] + public static string DiffingHash(this IBHoMObject obj, List exceptions = null, bool useDefaultExceptions = true) + { + if (exceptions == null || exceptions.Count == 0 && useDefaultExceptions) + SetDefaultExceptions(ref exceptions); + + return Compute.SHA256Hash(obj, exceptions); + } + + ///***************************************************/ + ///**** Private Methods ****/ + ///***************************************************/ + + internal static void SetDefaultExceptions(ref List exceptions) + { + if (exceptions == null) + exceptions = defaultHashExceptions; + else + exceptions.AddRange(defaultHashExceptions); + } + } +} diff --git a/Diffing_Engine/Compute/SHA256Hash.cs b/Diffing_Engine/Compute/SHA256Hash.cs new file mode 100644 index 000000000..d41232c03 --- /dev/null +++ b/Diffing_Engine/Compute/SHA256Hash.cs @@ -0,0 +1,74 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2019, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base; +using BH.oM.Data.Collections; +using BH.oM.Diffing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Security.Cryptography; +using System.Reflection; +using BH.Engine.Serialiser; +using BH.oM.Reflection.Attributes; +using System.ComponentModel; + +namespace BH.Engine.Diffing +{ + public static partial class Compute + { + ///***************************************************/ + ///**** Public Methods ****/ + ///***************************************************/ + + [Description("Computes the a SHA 256 hash code representing the object.")] + [Input("objects", "Object the hash code should be calculated for")] + [Input("exceptions", "List of strings specifying the names of the properties that should be ignored in the calculation, e.g. 'BHoM_Guid'")] + public static string SHA256Hash(IBHoMObject obj, List exceptions = null) + { + return SHA256Hash(obj.ToDiffingByteArray(exceptions)); + } + + ///***************************************************/ + ///**** Private Methods ****/ + ///***************************************************/ + + private static string SHA256Hash(byte[] inputObj) + { + StringBuilder sb = new StringBuilder(); + foreach (byte b in SHA256_byte(inputObj)) + sb.Append(b.ToString("X2")); + + return sb.ToString(); + } + + private static byte[] SHA256_byte(byte[] inputObj) + { + HashAlgorithm algorithm = System.Security.Cryptography.SHA256.Create(); + return algorithm.ComputeHash(inputObj); + } + + + } +} diff --git a/Diffing_Engine/Convert/ToDiffingByteArray.cs b/Diffing_Engine/Convert/ToDiffingByteArray.cs new file mode 100644 index 000000000..1df9ff29d --- /dev/null +++ b/Diffing_Engine/Convert/ToDiffingByteArray.cs @@ -0,0 +1,79 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2019, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base; +using BH.oM.Data.Collections; +using BH.oM.Diffing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Security.Cryptography; +using System.Reflection; +using BH.Engine.Serialiser; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using BH.oM.Reflection.Attributes; +using System.ComponentModel; +using MongoDB.Bson; + +namespace BH.Engine.Diffing +{ + public static partial class Convert + { + ///***************************************************/ + ///**** Public Methods ****/ + ///***************************************************/ + + public static byte[] ToDiffingByteArray(this object obj, PropertyInfo[] fieldsToIgnore = null) + { + List propList = fieldsToIgnore?.ToList(); + + if (propList == null || propList.Count == 0) + return BsonExtensionMethods.ToBson(obj.ToBsonDocument()); // .ToBsonDocument() for consistency with other cases + + List propNames = new List(); + propList.ForEach(prop => propNames.Add(prop.Name)); + + return ToDiffingByteArray(obj, propNames); + } + + public static byte[] ToDiffingByteArray(this object obj, List fieldsToIgnore) + { + if (fieldsToIgnore == null || fieldsToIgnore.Count == 0) + return BsonExtensionMethods.ToBson(obj); + + var objDoc = obj.ToBsonDocument(); + + fieldsToIgnore.ForEach(propName => + objDoc.Remove(propName) + ); + + return BsonExtensionMethods.ToBson(objDoc); + } + + + ///***************************************************/ + + } +} diff --git a/Diffing_Engine/Convert/ToDiffingJson.cs b/Diffing_Engine/Convert/ToDiffingJson.cs new file mode 100644 index 000000000..92ef22d9d --- /dev/null +++ b/Diffing_Engine/Convert/ToDiffingJson.cs @@ -0,0 +1,80 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2019, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base; +using BH.oM.Data.Collections; +using BH.oM.Diffing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Security.Cryptography; +using System.Reflection; +using BH.Engine.Serialiser; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using BH.oM.Reflection.Attributes; +using System.ComponentModel; + + +namespace BH.Engine.Diffing +{ + public static partial class Compute + { + ///***************************************************/ + ///**** Public Methods ****/ + ///***************************************************/ + + public static string ToDiffingJson(this object obj, PropertyInfo[] fieldsToNullify = null) + { + List propList = fieldsToNullify.ToList(); + if (propList == null && propList.Count == 0) + return BH.Engine.Serialiser.Convert.ToJson(obj); + + List propNames = new List(); + propList.ForEach(prop => propNames.Add(prop.Name)); + return ToDiffingJson(obj, propNames); + } + + public static string ToDiffingJson(this object obj, List fieldsToNullify) + { + if (fieldsToNullify == null && fieldsToNullify.Count == 0) + return BH.Engine.Serialiser.Convert.ToJson(obj); + + var jObject = JsonConvert.DeserializeObject(BH.Engine.Serialiser.Convert.ToJson(obj)); + + // Sets fields to be ignored as null, without altering the tree. + fieldsToNullify.ForEach(propName => + jObject.Properties() + .Where(attr => attr.Name.StartsWith(propName)) + .ToList() + .ForEach(attr => attr.Value = null) + ); + return jObject.ToString(); + } + + + ///***************************************************/ + + } +} diff --git a/Diffing_Engine/Create/Stream.cs b/Diffing_Engine/Create/Stream.cs new file mode 100644 index 000000000..aeec0c0be --- /dev/null +++ b/Diffing_Engine/Create/Stream.cs @@ -0,0 +1,73 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2019, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base; +using BH.oM.Reflection.Attributes; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BH.Engine.Diffing +{ + public static partial class Create + { + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [Description("Creates new Diffing Stream")] + [Input("objects", "Objects to be included in the Stream")] + [Input("streamId", "If not specified, streamId will be a GUID")] + public static BH.oM.Diffing.Stream Stream(IEnumerable objects, string comment = null) + { + return new BH.oM.Diffing.Stream(PrepareStreamObjects(objects), null, null, comment); + } + + [Description("Creates new Diffing Stream")] + [Input("objects", "Objects to be included in the Stream")] + [Input("streamId", "If not specified, streamId will be a GUID")] + [Input("revision", "If not specified, revision is initially set to 0")] + public static BH.oM.Diffing.Stream Stream(IEnumerable objects, string streamId = null, string revision = null, string comment = null) + { + return new BH.oM.Diffing.Stream(PrepareStreamObjects(objects), streamId, revision, comment); + } + + private static List PrepareStreamObjects(IEnumerable objects) + { + // Clone the current objects to preserve immutability + List objs_cloned = objects.Select(obj => BH.Engine.Base.Query.DeepClone(obj)).ToList(); + + // Calculate and set the hash fragment + Modify.SetHashFragment(objs_cloned); + + // Remove duplicates by hash + if (Query.RemoveDuplicatesByHash(objs_cloned)) + Reflection.Compute.RecordWarning("Some Objects were duplicates (same hash) and therefore have been discarded."); + + return objs_cloned; + } + + } +} diff --git a/Diffing_Engine/Diffing_Engine.csproj b/Diffing_Engine/Diffing_Engine.csproj new file mode 100644 index 000000000..f2fb238ba --- /dev/null +++ b/Diffing_Engine/Diffing_Engine.csproj @@ -0,0 +1,133 @@ + + + + + Debug + AnyCPU + {073DFD36-0829-4792-8C32-67BF692A9413} + Library + Properties + BH.Engine.Diffing + Diffing_Engine + v4.5.2 + 512 + true + + + true + full + false + ..\Build\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\..\BHoM\Build\BHoM.dll + False + + + ..\..\BHoM\Build\Data_oM.dll + False + + + ..\..\BHoM\Build\Diffing_oM.dll + + + False + ..\..\BHoM\Build\Geometry_oM.dll + False + + + ..\Build\MongoDB.Bson.dll + False + + + ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + False + + + False + ..\..\BHoM\Build\Reflection_oM.dll + False + + + ..\..\BHoM\Build\Structure_oM.dll + False + + + + + + + + + + + + + + ..\..\BHoM\Build\Testing_oM.dll + False + + + + + + + + + + + + + + + + + + + + + {1ad45c88-dd54-48e5-951f-55edfeb70e35} + BHoM_Engine + False + + + {8082ca2a-ac5c-4690-9f09-960e0d3e4102} + Data_Engine + + + {b0154405-9390-472d-9b5c-a2280823b18d} + Reflection_Engine + False + + + {b013f0da-7d21-4339-85fc-013edd518c6d} + Serialiser_Engine + False + + + {52a31a0a-e340-4909-aad6-228045b07bf3} + Structure_Engine + False + + + {51bff4a9-937a-40e8-ad83-d72025f173e6} + Testing_Engine + False + + + + + \ No newline at end of file diff --git a/Diffing_Engine/Modify/SetHashFragment.cs b/Diffing_Engine/Modify/SetHashFragment.cs new file mode 100644 index 000000000..9caa26919 --- /dev/null +++ b/Diffing_Engine/Modify/SetHashFragment.cs @@ -0,0 +1,67 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2019, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base; +using BH.oM.Data.Collections; +using BH.oM.Diffing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Security.Cryptography; +using System.Reflection; +using BH.Engine.Serialiser; +using BH.oM.Reflection.Attributes; +using System.ComponentModel; + +namespace BH.Engine.Diffing +{ + public static partial class Modify + { + ///***************************************************/ + ///**** Public Methods ****/ + ///***************************************************/ + + public static void SetHashFragment(IEnumerable objs, List exceptions = null, bool useDefaultExceptions = true) + { + if (useDefaultExceptions) + Compute.SetDefaultExceptions(ref exceptions); + + // Calculate and set the object hashes + foreach (var obj in objs) + { + string hash = BH.Engine.Diffing.Compute.DiffingHash(obj, exceptions, useDefaultExceptions); + + HashFragment existingFragm = obj.GetHashFragment(); + + if (existingFragm != null) + { + obj.Fragments.RemoveAll(fr => (fr as HashFragment) != null); + obj.Fragments.Add(new HashFragment(hash, existingFragm.Hash)); + } + else + obj.Fragments.Add(new HashFragment(hash, null)); + } + } + } +} diff --git a/Diffing_Engine/Modify/UpdateRevision.cs b/Diffing_Engine/Modify/UpdateRevision.cs new file mode 100644 index 000000000..98956589e --- /dev/null +++ b/Diffing_Engine/Modify/UpdateRevision.cs @@ -0,0 +1,61 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2019, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base; +using BH.oM.Reflection.Attributes; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BH.Engine.Diffing +{ + public static partial class Modify + { + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [Description("Returns a new Diffing Stream as a new revision of a previous Stream")] + [Input("stream", "Stream to be updated")] + [Input("objects", "Objects to be included in the updated version of the Stream")] + public static BH.oM.Diffing.Stream StreamRevision(BH.oM.Diffing.Stream stream, IEnumerable objects) + { + // Clone the current objects to preserve immutability + List objs_cloned = objects.Select(obj => BH.Engine.Base.Query.DeepClone(obj)).ToList(); + + // Calculate and set the hash fragment + BH.Engine.Diffing.Modify.SetHashFragment(objs_cloned); + + // Remove duplicates by hash + int numObjs = objs_cloned.Count(); + objs_cloned = objs_cloned.GroupBy(obj => obj.GetHashFragment().Hash).Select(gr => gr.First()).ToList(); + + if (numObjs != objs_cloned.Count()) + BH.Engine.Reflection.Compute.RecordWarning("Some Objects were duplicates (same hash) and therefore have been discarded."); + + return new BH.oM.Diffing.Stream(objs_cloned, stream.StreamId); + } + } +} diff --git a/Diffing_Engine/Properties/AssemblyInfo.cs b/Diffing_Engine/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..6c03bb0d2 --- /dev/null +++ b/Diffing_Engine/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Diffing_Engine")] +[assembly: AssemblyDescription("BHoM Diffing Engine")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Diffing_Engine")] +[assembly: AssemblyCopyright("Copyright © https://github.com/BHoM")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("073dfd36-0829-4792-8c32-67bf692a9413")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("2.3.1.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] diff --git a/Diffing_Engine/Query/GetHash.cs b/Diffing_Engine/Query/GetHash.cs new file mode 100644 index 000000000..5881b1a02 --- /dev/null +++ b/Diffing_Engine/Query/GetHash.cs @@ -0,0 +1,33 @@ +using BH.oM.Base; +using BH.oM.Diffing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BH.Engine.Diffing +{ + public static partial class Query + { + public static HashFragment GetHashFragment(this IBHoMObject obj) + { + int numOfHashFragments = 0; + + if (obj.Fragments.Exists(fragm => fragm?.GetType() == typeof(HashFragment))) + numOfHashFragments = obj.Fragments.OfType().Count(); + + if (numOfHashFragments == 0) + return null; + + if (numOfHashFragments > 1) + { + throw new Exception("BHoM objects may have only one Hash fragment."); + } + + return obj.Fragments.OfType().First(); + } + + } +} + diff --git a/Diffing_Engine/Query/RemoveDuplicatesByHash.cs b/Diffing_Engine/Query/RemoveDuplicatesByHash.cs new file mode 100644 index 000000000..0fe57e5c1 --- /dev/null +++ b/Diffing_Engine/Query/RemoveDuplicatesByHash.cs @@ -0,0 +1,53 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2019, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Base; +using BH.oM.Reflection.Attributes; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BH.Engine.Diffing +{ + public static partial class Query + { + /***************************************************/ + /**** Public Methods ****/ + /***************************************************/ + + [Description("Removes duplicates from a collection of objects. The comparison is made through their Diffing Hash.")] + [Input("objects", "Collection of objects whose duplicates have to be removed. If they don't already have an Hash assigned, it will be calculated.")] + public static bool RemoveDuplicatesByHash(IEnumerable objects) + { + int numObjs = objects.Count(); + objects = objects.GroupBy(obj => obj.GetHashFragment().Hash).Select(gr => gr.First()).ToList(); + + if (numObjs != objects.Count()) + return true; + + return false; + } + } +} diff --git a/Diffing_Engine/packages.config b/Diffing_Engine/packages.config new file mode 100644 index 000000000..986b3f183 --- /dev/null +++ b/Diffing_Engine/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Engine_Test/Diffing_Engine/GenerateRandomObjs.cs b/Engine_Test/Diffing_Engine/GenerateRandomObjs.cs new file mode 100644 index 000000000..e51f9d5b3 --- /dev/null +++ b/Engine_Test/Diffing_Engine/GenerateRandomObjs.cs @@ -0,0 +1,24 @@ +using BH.oM.Base; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Engine_Test +{ + internal static partial class TestDiffing + { + private static List GenerateRandomObjects(Type t, int count) + { + List objs = new List(); + + for (int i = 0; i < count; i++) + { + objs.Add(BH.Engine.Base.Create.RandomObject(t) as dynamic); + } + + return objs; + } + } +} diff --git a/Engine_Test/Diffing_Engine/Profiling01.cs b/Engine_Test/Diffing_Engine/Profiling01.cs new file mode 100644 index 000000000..1040311f8 --- /dev/null +++ b/Engine_Test/Diffing_Engine/Profiling01.cs @@ -0,0 +1,99 @@ +using BH.oM.Structure.Elements; +using BH.oM.Geometry; +using BH.Engine; +using BH.oM.Base; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using System.Security.Cryptography; +using BH.Engine.Serialiser; +using BH.oM.Diffing; +using System.Diagnostics; + +namespace Engine_Test +{ + internal static partial class TestDiffing + { + public static void Profiling01() + { + Console.WriteLine("Running Diffing_Engine Profiling01"); + + string path = @"C:\temp\Diffing_Engine-ProfilingTask01.txt"; + File.Delete(path); + + List numberOfObjects = new List() { 10, 100, 1000, 5000, 10000 }; //, 12250, 15000, 17250, 20000, 25000, 30000 }; + + bool propertyLevelDiffing = false; + for (int b = 0; b < 2; b++) + { + numberOfObjects.ForEach(i => + ProfilingTask(i, propertyLevelDiffing, path)); + + propertyLevelDiffing = !propertyLevelDiffing; + } + + Console.WriteLine("Profiling01 concluded."); + } + + public static void ProfilingTask(int totalObjs, bool propertyLevelDiffing, string path = null) + { + string introMessage = $"Profiling diffing for {totalObjs} randomly generated and modified objects."; + introMessage += propertyLevelDiffing ? " Includes collection-level and property-level diffing." : " Only collection-level diffing."; + Console.WriteLine(introMessage); + + if (path != null) + { + string fName = Path.GetFileNameWithoutExtension(path); + string ext = Path.GetExtension(path); + fName += propertyLevelDiffing ? "_propLevel" : "_onlyCollLevel"; + path = Path.Combine(Path.GetDirectoryName(path), fName + ext); + } + + // Generate random objects + List currentObjs = GenerateRandomObjects(typeof(Bar), totalObjs); + + // Create Stream. This assigns the Hashes. + var stream = BH.Engine.Diffing.Create.Stream(currentObjs, null); + + // Modify randomly half the total of objects. + var readObjs = stream.Objects.Cast().ToList(); + + var allIdxs = Enumerable.Range(0, currentObjs.Count).ToList(); + var randIdxs = allIdxs.OrderBy(g => Guid.NewGuid()).Take(currentObjs.Count / 2); + var remainingIdx = allIdxs.Except(randIdxs).ToList(); + + List changedList = randIdxs.Select(idx => readObjs.ElementAt(idx)).ToList(); + changedList.ForEach(obj => obj.Name = "ModifiedName"); + changedList.AddRange(remainingIdx.Select(idx => readObjs.ElementAt(idx)).Cast().ToList()); + + // Update stream revision + BH.oM.Diffing.Stream updatedStream = BH.Engine.Diffing.Modify.StreamRevision(stream, changedList); + + // Actual diffing + var timer = new Stopwatch(); + timer.Start(); + + Delta delta = BH.Engine.Diffing.Compute.Diffing(stream, updatedStream, propertyLevelDiffing, null, true); + + timer.Stop(); + + var ms = timer.ElapsedMilliseconds; + + string endMessage = $"Total elapsed milliseconds: {ms}"; + Console.WriteLine(endMessage); + + Debug.Assert(delta.ModifiedObjects.Count == totalObjs / 2, "Diffing didn't work."); + + if (path != null) + { + System.IO.File.AppendAllText(path, Environment.NewLine + introMessage + Environment.NewLine + endMessage); + Console.WriteLine($"Results appended in {path}"); + } + } + + } +} diff --git a/Engine_Test/Diffing_Engine/Test01.cs b/Engine_Test/Diffing_Engine/Test01.cs new file mode 100644 index 000000000..b37aef52d --- /dev/null +++ b/Engine_Test/Diffing_Engine/Test01.cs @@ -0,0 +1,89 @@ +using BH.oM.Structure.Elements; +using BH.oM.Geometry; +using BH.Engine; +using BH.oM.Base; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Security.Cryptography; +using BH.Engine.Diffing; +using System.Diagnostics; +using BH.oM.Diffing; +using BH.Engine.Diffing; + +namespace Engine_Test +{ + internal static partial class TestDiffing + { + public static void Test01(bool propertyLevelDiffing = true) + { + Console.WriteLine("Running Diffing_Engine Test01"); + + // 1. Suppose Alessio is creating 3 bars in Grasshopper, representing a Portal frame structure. + // These will be Alessio's "Current" objects. + List currentObjs_Alessio = new List(); + + for (int i = 0; i < 3; i++) + { + Bar obj = BH.Engine.Base.Create.RandomObject(typeof(Bar)) as Bar; + obj.Fragments = obj.Fragments.Where(fragm => fragm != null).ToList(); // (RandomObject bug workaround: it generates a random number of null fragments) + obj.Name = "bar_" + i.ToString(); + currentObjs_Alessio.Add(obj as dynamic); + } + + // 2. Alessio wants these bars to be part of a "Portal frame Stream" that will be tracking the objects for future changes. + // Alessio creates a stream + string comment = "Portal Frame Stream"; + Stream stream_Alessio = Create.Stream(currentObjs_Alessio, comment); // this will add the hash fragments to the objects + + // Alessio can now push the Stream. + + // 3. Eduardo is now asked to do some changes to the "Portal frame Project" created by Alessio. + // On his machine, Eduardo now PULLS the Stream from the external platform to read the existing objects. + IEnumerable readObjs_Eduardo = stream_Alessio.Objects.Select(obj => BH.Engine.Base.Query.DeepClone(obj) as IBHoMObject).ToList(); + + // Eduardo will now work on these objects. + List currentObjs_Eduardo = readObjs_Eduardo.ToList(); + + // 5. Eduardo now modifies one of the bars, deletes another one, and creates a new one. + // modifies bar_0 + currentObjs_Eduardo[0].Name = "modifiedBar_0"; + + // deletes bar_1 + currentObjs_Eduardo.RemoveAt(1); + + // adds a new bar + Bar newBar = BH.Engine.Base.Create.RandomObject(typeof(Bar)) as Bar; + newBar.Name = "newBar_1"; + currentObjs_Eduardo.Insert(1, newBar as dynamic); + + // 6. Eduardo updates the Stream Revision. + Stream stream_Eduardo = Modify.StreamRevision(stream_Alessio, currentObjs_Eduardo); + + // Eduardo can now push this Stream. + + // -------------------------------------------------------- // + + // Eduardo can also manually check the differences. + + Delta delta = Compute.Diffing(stream_Alessio, stream_Eduardo, propertyLevelDiffing, null, true); + + // 7. Now Eduardo can push his new delta object (like step 3). + // `delta.ToCreate` will have 1 object; `delta2.ToUpdate` 1 object; `delta2.ToDelete` 1 object; `delta2.Unchanged` 2 objects. + // You can also see which properties have changed for what objects: check `delta2.ModifiedPropsPerObject`. + Debug.Assert(delta.NewObjects.Count == 1, "Incorrect number of object identified as new/ToBeCreated."); + Debug.Assert(delta.ModifiedObjects.Count == 1, "Incorrect number of object identified as modified/ToBeUpdated."); + Debug.Assert(delta.OldObjects.Count == 1, "Incorrect number of object identified as old/ToBeDeleted."); + var modifiedPropsPerObj = delta.ModifiedPropsPerObject.First().Value; + Debug.Assert(modifiedPropsPerObj.Count == 1, "Incorrect number of changed properties identified by the property-level diffing."); + Debug.Assert(modifiedPropsPerObj.First().Key == "Name", "Error in property-level diffing"); + Debug.Assert(modifiedPropsPerObj.First().Value.Item1 as string == "modifiedBar_0", "Error in property-level diffing"); + + Console.WriteLine("Test01 concluded."); + } + + } +} diff --git a/Engine_Test/Engine_Test.csproj b/Engine_Test/Engine_Test.csproj index 5cab8d22a..55c35bfe9 100644 --- a/Engine_Test/Engine_Test.csproj +++ b/Engine_Test/Engine_Test.csproj @@ -53,6 +53,7 @@ False ..\..\BHoM\Build\Acoustic_oM.dll + False False @@ -62,9 +63,14 @@ False ..\..\BHoM\Build\BHoM.dll + + False + ..\..\BHoM\Build\Diffing_oM.dll + False ..\..\BHoM\Build\Geometry_oM.dll + False False @@ -72,11 +78,12 @@ ..\packages\MongoDB.Bson.2.4.4\lib\net45\MongoDB.Bson.dll - True + False False ..\..\BHoM\Build\Structure_oM.dll + False @@ -88,6 +95,9 @@ + + + @@ -104,10 +114,16 @@ {1ad45c88-dd54-48e5-951f-55edfeb70e35} BHoM_Engine + False + + + {073dfd36-0829-4792-8c32-67bf692a9413} + Diffing_Engine {89ab2dcb-ef87-4cba-b59c-c16a8a71d333} Geometry_Engine + False {f17df4d6-d155-4ffa-9917-ef1487faa978} @@ -116,14 +132,17 @@ {b0154405-9390-472d-9b5c-a2280823b18d} Reflection_Engine + False {b013f0da-7d21-4339-85fc-013edd518c6d} Serialiser_Engine + False {52a31a0a-e340-4909-aad6-228045b07bf3} Structure_Engine + False diff --git a/Engine_Test/Program.cs b/Engine_Test/Program.cs index 33b951fa6..5ec54cde6 100644 --- a/Engine_Test/Program.cs +++ b/Engine_Test/Program.cs @@ -47,17 +47,22 @@ class Program //static List testLines = new List(); //static List testPoly = new List(); - static void Main(string[] args) { - - TheatronTest test = new TheatronTest(); - test.Test(); - test.SutherLandHodgmanTest(); //TestSerialization(); //TestConstructorSpeed(); //TestDynamicExtentionCost(); //TestMethodExtraction(); + + /// ************************************/ + /// Diffing test and profiling methods + /// ************************************/ + + TestDiffing.Test01(); + TestDiffing.Profiling01(); + + /// ************************************/ + Console.Read(); } diff --git a/Testing_Engine/Query/IsEqual.cs b/Testing_Engine/Query/IsEqual.cs index 3dcedae9c..2a8427760 100644 --- a/Testing_Engine/Query/IsEqual.cs +++ b/Testing_Engine/Query/IsEqual.cs @@ -41,7 +41,7 @@ public static partial class Query /**** Public Methods ****/ /***************************************************/ - [Description("Checks two BHoMObjects for equality and returns the differences")] + [Description("Checks two BHoMObjects for equality property by property and returns the differences")] [Input("config", "Config to be used for the comparison. Can set numeric tolerance, wheter to check the guid, if custom data should be ignored and if any additional properties should be ignored")] [MultiOutputAttribute(0, "IsEqual", "Returns true if the two items are deemed to be equal")] [MultiOutputAttribute(1, "DiffProperty", "List of the names of the properties found to be different")] @@ -83,6 +83,39 @@ public static Output, List, List> IsEqual(thi } + [Description("Checks two BHoMObjects property by property and returns the differences")] + [Input("config", "Config to be used for the comparison. Can set numeric tolerance, wheter to check the guid, if custom data should be ignored and if any additional properties should be ignored")] + [Output("Dictionary whose key is the name of the property, and value is a tuple with its value in obj1 and obj2.")] + public static Dictionary> DifferentProperties(this IBHoMObject obj1, IBHoMObject obj2, IsEqualConfig config = null) + { + var dict = new Dictionary>(); + + //Use default config if null + config = config ?? new IsEqualConfig(); + + CompareLogic comparer = new CompareLogic(); + + comparer.Config.MaxDifferences = 1000; + comparer.Config.MembersToIgnore = config.PropertiesToIgnore; + comparer.Config.DoublePrecision = config.NumericTolerance; + + if (config.IgnoreCustomData) + { + comparer.Config.MembersToIgnore.Add("CustomData"); + } + + if (config.IgnoreGuid) + comparer.Config.TypesToIgnore.Add(typeof(Guid)); + + ComparisonResult result = comparer.Compare(obj1, obj2); + dict = result.Differences.ToDictionary(diff => diff.PropertyName, diff => new Tuple(diff.Object1, diff.Object2)); + + if (dict.Count == 0) + return null; + + return dict; + } + /***************************************************/ } }