diff --git a/src/PKSim.Core/Services/SimulationBuildingBlockUpdater.cs b/src/PKSim.Core/Services/SimulationBuildingBlockUpdater.cs index 8605d533a..2691f6c44 100644 --- a/src/PKSim.Core/Services/SimulationBuildingBlockUpdater.cs +++ b/src/PKSim.Core/Services/SimulationBuildingBlockUpdater.cs @@ -42,8 +42,10 @@ public interface ISimulationBuildingBlockUpdater /// Returns false if a simple parameter value update cannot be performed (i.e. structural change was made) /// /// Template building block as defined in repository - /// Used building block (the one based on the template building block at a given time) - /// + /// + /// Used building block (the one based on the template building block at a given time). It + /// it assumed that the object is loaded (e.g. reference to building block can be used + /// bool QuickUpdatePossibleFor(IPKSimBuildingBlock templateBuildingBlock, UsedBuildingBlock usedBuildingBlock); /// @@ -61,7 +63,7 @@ public interface ISimulationBuildingBlockUpdater bool BuildingBlockSupportsQuickUpdate(IPKSimBuildingBlock templateBuildingBlock); /// - /// Returns whether a building block comparison is available for the building block + /// Returns whether a building block comparison is available for the building block /// bool BuildingBlockSupportComparison(IPKSimBuildingBlock templateBuildingBlock); } @@ -140,7 +142,28 @@ public bool QuickUpdatePossibleFor(IPKSimBuildingBlock templateBuildingBlock, Us if (!BuildingBlockSupportsQuickUpdate(templateBuildingBlock)) return false; - return templateBuildingBlock.StructureVersion == usedBuildingBlock.StructureVersion; + //not the same structure, easy return + var sameStructureVersion = templateBuildingBlock.StructureVersion == usedBuildingBlock.StructureVersion; + if (!sameStructureVersion) + return false; + + //For individual, there is a special handling required as we need to also check that the used expression profile have the same structure version + if (templateBuildingBlock is not Individual individualTemplate) + return true; + + //It has to be available by construction + var usedIndividual = usedBuildingBlock.BuildingBlock as Individual; + //but we return false just in case :) + if (usedIndividual == null) + return false; + + //let's compare the expression profile in each individuals and see if they are comparable + return individualTemplate.AllExpressionProfiles().All(x => + { + //assume we can find it by name. Otherwise => structural change + var usedExpressionProfile = usedIndividual.AllExpressionProfiles().FindByName(x.Name); + return usedExpressionProfile != null && x.StructureVersion == usedExpressionProfile.StructureVersion; + }); } public void UpdateProtocolsInSimulation(Simulation simulation) @@ -151,7 +174,7 @@ public void UpdateProtocolsInSimulation(Simulation simulation) UpdateMultipleUsedBuildingBlockInSimulationFromTemplate(simulation, protocolProperties.Select(x => x.Protocol), PKSimBuildingBlockType.Protocol); var allSimulationProtocols = simulation.AllBuildingBlocks().ToList(); - //update selected protocol with references in simulation instead of templats + //update selected protocol with references in simulation instead of templates protocolProperties.Each(x => x.Protocol = allSimulationProtocols.FindByName(x.Protocol.Name)); } diff --git a/src/PKSim.Presentation/Services/SimulationTask.cs b/src/PKSim.Presentation/Services/SimulationTask.cs index bb55e6319..24f6e433d 100644 --- a/src/PKSim.Presentation/Services/SimulationTask.cs +++ b/src/PKSim.Presentation/Services/SimulationTask.cs @@ -95,8 +95,11 @@ public override Simulation AddToProject() _buildingBlockTask.Load(templateBuildingBlock); _buildingBlockTask.Load(simulation); + //Now that the simulation is loaded, we want to make sure that the captured ref to used building block also has a valid reference to the underlying building block + var loadedUsedBuildingBlock = simulation.UsedBuildingBlockByTemplateId(usedBuildingBlock.TemplateId); + //check if quick update possible. if yes =>perform quick update - if (_simulationBuildingBlockUpdater.QuickUpdatePossibleFor(templateBuildingBlock, usedBuildingBlock)) + if (_simulationBuildingBlockUpdater.QuickUpdatePossibleFor(templateBuildingBlock, loadedUsedBuildingBlock)) { var updateCommand = _blockParametersToSimulationUpdater.UpdateParametersFromBuildingBlockInSimulation(templateBuildingBlock, simulation); _buildingBlockTask.AddCommandToHistory(updateCommand); @@ -115,8 +118,11 @@ public override Simulation AddToProject() _buildingBlockTask.Load(templateBuildingBlock); _buildingBlockTask.Load(simulation); + //Now that the simulation is loaded, we want to make sure that the captured ref to used building block also has a valid reference to the underlying building block + var loadedUsedBuildingBlock = simulation.UsedBuildingBlockByTemplateId(usedBuildingBlock.TemplateId); + //check if quick update possible. if yes =>perform quick update - if (_simulationBuildingBlockUpdater.QuickUpdatePossibleFor(templateBuildingBlock, usedBuildingBlock)) + if (_simulationBuildingBlockUpdater.QuickUpdatePossibleFor(templateBuildingBlock, loadedUsedBuildingBlock)) { var updateCommand = _simulationParametersToBlockUpdater.UpdateParametersFromSimulationInBuildingBlock(simulation, templateBuildingBlock); _buildingBlockTask.AddCommandToHistory(updateCommand); diff --git a/tests/PKSim.Tests/Core/SimulationBuildingBlockUpdaterSpecs.cs b/tests/PKSim.Tests/Core/SimulationBuildingBlockUpdaterSpecs.cs index fab3cae93..57171ebc2 100644 --- a/tests/PKSim.Tests/Core/SimulationBuildingBlockUpdaterSpecs.cs +++ b/tests/PKSim.Tests/Core/SimulationBuildingBlockUpdaterSpecs.cs @@ -1,10 +1,10 @@ +using FakeItEasy; using OSPSuite.BDDHelper; using OSPSuite.BDDHelper.Extensions; -using FakeItEasy; +using OSPSuite.Core.Domain; using PKSim.Core.Mappers; using PKSim.Core.Model; using PKSim.Core.Services; -using OSPSuite.Core.Domain; namespace PKSim.Core { @@ -133,6 +133,94 @@ public void should_return_false_if_the_building_block_being_updated_is_a_populat } } + public abstract class concern_for_SimulationBuildingBlockUpdaterForIndividual : concern_for_SimulationBuildingBlockUpdater + { + protected Individual _templateIndividual; + protected UsedBuildingBlock _usedIndividual; + protected ExpressionProfile _templateExpression; + protected Individual _individual; + + protected override void Context() + { + base.Context(); + _templateIndividual = new Individual + { + Id = "templateId", + StructureVersion = 1 + }; + + _templateExpression = DomainHelperForSpecs.CreateExpressionProfile("Human", "CYP"); + _templateExpression.Id = "templateExpressionProfile"; + _templateExpression.StructureVersion = 5; + + _templateIndividual.AddExpressionProfile(_templateExpression); + + _individual = new Individual + { + StructureVersion = _templateIndividual.StructureVersion, + }; + + + _usedIndividual = new UsedBuildingBlock(_templateIndividual.Id, PKSimBuildingBlockType.Individual) + { + StructureVersion = _templateIndividual.StructureVersion, + BuildingBlock = _individual + }; + } + } + + public class When_asked_if_a_quick_update_possible_between_an_individual_an_a_used_individual_with_expression_profile_presenting_a_structural_change : concern_for_SimulationBuildingBlockUpdaterForIndividual + { + protected override void Context() + { + base.Context(); + var expression = DomainHelperForSpecs.CreateExpressionProfile("Human", "CYP"); + expression.StructureVersion = _templateExpression.StructureVersion + 1; + _individual.AddExpressionProfile(expression); + } + + [Observation] + public void should_not_allow_for_a_quick_update() + { + sut.QuickUpdatePossibleFor(_templateIndividual, _usedIndividual).ShouldBeFalse(); + } + } + + public class When_asked_if_a_quick_update_possible_between_an_individual_an_a_used_individual_with_expression_profile_presenting_no_structural_change : concern_for_SimulationBuildingBlockUpdaterForIndividual + { + protected override void Context() + { + base.Context(); + var expression = DomainHelperForSpecs.CreateExpressionProfile("Human", "CYP"); + expression.StructureVersion = _templateExpression.StructureVersion; + _individual.AddExpressionProfile(expression); + } + + [Observation] + public void should_allow_for_a_quick_update() + { + sut.QuickUpdatePossibleFor(_templateIndividual, _usedIndividual).ShouldBeTrue(); + } + } + + public class When_asked_if_a_quick_update_possible_between_an_individual_an_a_used_individual_with_expression_profile_missing_by_name : concern_for_SimulationBuildingBlockUpdaterForIndividual + { + protected override void Context() + { + base.Context(); + + var expression = DomainHelperForSpecs.CreateExpressionProfile("Human", "AnotherCYP"); + expression.StructureVersion = _templateExpression.StructureVersion; + _individual.AddExpressionProfile(expression); + } + + [Observation] + public void should_not_allow_for_a_quick_update() + { + sut.QuickUpdatePossibleFor(_templateIndividual, _usedIndividual).ShouldBeFalse(); + } + } + public class When_updating_the_formulation_defined_in_a_simulation_according_to_the_formulation_mapping : concern_for_SimulationBuildingBlockUpdater { private Simulation _simulation; @@ -169,23 +257,23 @@ public void should_add_one_instance_of_each_used_formulation_to_the_simulation() public class When_updating_the_formulation_defined_in_a_simulation_according_to_the_formulation_mapping_using_both_a_template_and_a_simulation_formulation_for_the_same_formulation : concern_for_SimulationBuildingBlockUpdater { private Simulation _simulation; - + protected override void Context() { base.Context(); var formulation = new Formulation().WithName("F"); - _simulation = new IndividualSimulation { Properties = new SimulationProperties() }; + _simulation = new IndividualSimulation {Properties = new SimulationProperties()}; var formulationUsedInSimulation = new Formulation().WithName("F"); var compoundProperties = new CompoundProperties(); - compoundProperties.ProtocolProperties.AddFormulationMapping(new FormulationMapping { Formulation = formulation }); - compoundProperties.ProtocolProperties.AddFormulationMapping(new FormulationMapping { Formulation = formulationUsedInSimulation }); + compoundProperties.ProtocolProperties.AddFormulationMapping(new FormulationMapping {Formulation = formulation}); + compoundProperties.ProtocolProperties.AddFormulationMapping(new FormulationMapping {Formulation = formulationUsedInSimulation}); _simulation.Properties.AddCompoundProperties(compoundProperties); } [Observation] public void should_throw_an_exception() { - The.Action(()=>sut.UpdateFormulationsInSimulation(_simulation)).ShouldThrowAn(); + The.Action(() => sut.UpdateFormulationsInSimulation(_simulation)).ShouldThrowAn(); } } diff --git a/tests/PKSim.Tests/Presentation/SimulationTaskSpecs.cs b/tests/PKSim.Tests/Presentation/SimulationTaskSpecs.cs index 83ed33148..9e3523903 100644 --- a/tests/PKSim.Tests/Presentation/SimulationTaskSpecs.cs +++ b/tests/PKSim.Tests/Presentation/SimulationTaskSpecs.cs @@ -70,6 +70,7 @@ public class When_the_simulation_task_is_asked_to_perform_an_update_from_a_templ private IPKSimBuildingBlock _templateBuildingBlock; private IPKSimCommand _updateCommand; private UsedBuildingBlock _usedBuildingBlock; + private UsedBuildingBlock _loadedUsedBuildingBlock; protected override void Context() { @@ -78,7 +79,10 @@ protected override void Context() _templateBuildingBlock = A.Fake(); _updateCommand = A.Fake(); _usedBuildingBlock = A.Fake(); - A.CallTo(() => _simulationBuildingBlockUpdater.QuickUpdatePossibleFor(_templateBuildingBlock, _usedBuildingBlock)).Returns(true); + _loadedUsedBuildingBlock = A.Fake(); + //make sure we a new instance to ensure usage of loaded used building block + A.CallTo(() => _simulation.UsedBuildingBlockByTemplateId(_usedBuildingBlock.TemplateId)).Returns(_loadedUsedBuildingBlock); + A.CallTo(() => _simulationBuildingBlockUpdater.QuickUpdatePossibleFor(_templateBuildingBlock, _loadedUsedBuildingBlock)).Returns(true); A.CallTo(() => _blockParametersToSimulationUpdater.UpdateParametersFromBuildingBlockInSimulation(_templateBuildingBlock, _simulation)).Returns(_updateCommand); }