From 5b55a086662b1fe736136213ef97edbfd1715909 Mon Sep 17 00:00:00 2001 From: Joris de Keijser Date: Mon, 6 Jan 2025 12:08:55 +0100 Subject: [PATCH] feat: add choice based matching (#322) --- R/Dockerfile | 4 +- R/apiEntryPoint.R | 1 + R/choiceBasedMatching.R | 250 ++++++++++++++++++ R/smaa.R | 2 +- R/util/constraint.R | 2 + R/util/pvf.R | 1 - .../CurrentScenarioContext.tsx | 5 +- .../CurrentTab/Preferences/Preferences.tsx | 10 +- .../PreferencesWeightsButtons.tsx | 19 ++ .../ScenariosContext/TElicitationMethod.ts | 1 + .../ScenariosContext/TPreferencesView.ts | 5 +- .../ScenariosContext/preferencesUtil.test.ts | 42 ++- .../ScenariosContext/preferencesUtil.ts | 5 +- .../SettingsContext/ISettingsContext.ts | 1 + .../SettingsContext/SettingsContext.tsx | 1 + .../SettingsContext/SettingsUtil.test.ts | 3 +- .../Workspace/SettingsContext/SettingsUtil.ts | 3 +- .../IWorkspaceSettingsContext.ts | 3 +- .../CbmPieChartToggle/CbmPieChartToggle.tsx | 29 ++ .../SharedComponents/InlineHelp/lexicon.ts | 5 + index.ts | 2 +- node-backend/choiceBasedMatchingHandler.ts | 31 +++ node-backend/getRequiredRights.ts | 158 +++++------ node-backend/patavi.ts | 3 +- node-backend/pataviRouter.ts | 2 + package.json | 4 +- public/mcda-page-titles.json | 3 +- shared/interface/IAnswerAndQuestion.ts | 6 + .../interface/IChoiceBasedMatchingQuestion.ts | 5 + shared/interface/IChoiceBasedMatchingState.ts | 9 + shared/interface/IReducedCriterion.ts | 6 + .../Patavi/IChoiceBasedMatchingCommand.ts | 6 + .../Scenario/IUpperRatioConstraint.ts | 7 + shared/interface/Settings/ISettings.ts | 1 + shared/types/PataviCommands.ts | 4 +- shared/types/preferences.ts | 4 +- yarn.lock | 6 +- 37 files changed, 549 insertions(+), 100 deletions(-) create mode 100644 R/choiceBasedMatching.R create mode 100644 app/ts/WorkspaceSettings/CbmPieChartToggle/CbmPieChartToggle.tsx create mode 100644 node-backend/choiceBasedMatchingHandler.ts create mode 100644 shared/interface/IAnswerAndQuestion.ts create mode 100644 shared/interface/IChoiceBasedMatchingQuestion.ts create mode 100644 shared/interface/IChoiceBasedMatchingState.ts create mode 100644 shared/interface/IReducedCriterion.ts create mode 100644 shared/interface/Patavi/IChoiceBasedMatchingCommand.ts create mode 100644 shared/interface/Scenario/IUpperRatioConstraint.ts diff --git a/R/Dockerfile b/R/Dockerfile index 6b5d23cf1..2c620504a 100644 --- a/R/Dockerfile +++ b/R/Dockerfile @@ -9,13 +9,15 @@ RUN DEBIAN_FRONTEND=noninteractive apt install -y -q libgmp-dev RUN R -e 'install.packages("hitandrun", repos="http://cran.rstudio.com/"); if (!require("hitandrun")) quit(save="no", status=8)' RUN R -e 'install.packages("smaa", repos="http://cran.rstudio.com/"); if (!require("smaa")) quit(save="no", status=8)' +RUN R -e 'install.packages("abind", repos="http://cran.rstudio.com/"); if (!require("abind")) quit(save="no", status=8)' ADD *.R /tmp/ ADD util/*.R /tmp/ +RUN rm /tmp/apiEntryPoint.R RUN cat /tmp/*.R > /var/lib/patavi/smaa_service.R USER patavi WORKDIR /var/lib/patavi -ENTRYPOINT ["patavi-worker", "--method", "smaa_v2", "-n", "1", "--file", "/var/lib/patavi/smaa_service.R", "--rserve", "--packages", "MASS,rcdd,hitandrun,smaa"] +ENTRYPOINT ["patavi-worker", "--method", "smaa_v2", "-n", "1", "--file", "/var/lib/patavi/smaa_service.R", "--rserve", "--packages", "MASS,rcdd,hitandrun,smaa,abind"] diff --git a/R/apiEntryPoint.R b/R/apiEntryPoint.R index 2c6829178..d29d04e0b 100644 --- a/R/apiEntryPoint.R +++ b/R/apiEntryPoint.R @@ -4,6 +4,7 @@ library(hitandrun) smaa_v2 <- function(params) { allowed <- c( + 'choiceBasedMatching', 'deterministic', 'indifferenceCurve', 'matchingElicitationCurve', diff --git a/R/choiceBasedMatching.R b/R/choiceBasedMatching.R new file mode 100644 index 000000000..c2935e4d6 --- /dev/null +++ b/R/choiceBasedMatching.R @@ -0,0 +1,250 @@ +run_choiceBasedMatching <- function(params) { + numberOfCriteria <- length(params[['criteria']]) + criterionIds <- c() + for (criterion in params[['criteria']]) { + criterionIds <- append(criterionIds, criterion[['id']]) + } + + pvf <- lapply(params[['criteria']], createPvf) + names(pvf) <- criterionIds + + constraintsFromHistory <- generateConstraintsFromHistory(params, criterionIds, pvf) + edgeLengths <- calculateEdgeLengths(constraintsFromHistory) + + fromR <- params + + if (isDoneEliciting(params)) { + fromR[['preferences']] <- retrieveUpperBoundConstraints(constraintsFromHistory, numberOfCriteria, criterionIds) + } else { + nextQuestion <- generateNextQuestion(params, constraintsFromHistory, edgeLengths, criterionIds, pvf) + fromR[['answersAndQuestions']] <- append(fromR[['answersAndQuestions']], list(list(question = nextQuestion))) + } + return(fromR) +} + +isDoneEliciting <- function(params) { + numberOfCriteria <- length(params[['criteria']]) + numberOfAnswers <- length(params[['answersAndQuestions']]) + if (numberOfCriteria <= 4) { + return(numberOfAnswers == (numberOfCriteria - 1) * 4) + } else { + return(numberOfAnswers == (numberOfCriteria - 2) * 4) + } +} + +retrieveUpperBoundConstraints <- function(constraint, numberOfCriteria, criterionIds) { + numberOfUpperBoundConstraints <- nrow(constraint[['constr']]) - (numberOfCriteria + 1) + preferences <- vector("list", numberOfUpperBoundConstraints) + for (index in 1:numberOfUpperBoundConstraints) { + criteria <- getCriteriaWithConstraints(criterionIds, constraint, index, numberOfCriteria) + bound <- getBound(constraint, index, numberOfCriteria) + preferences[[index]] <- list(type = "upper ratio", elicitationMethod = "choice", criteria = criteria, bound = bound) + } + return(preferences) +} + +getCriteriaWithConstraints <- function(criterionIds, constraint, index, numberOfCriteria) { + criterion1Id <- criterionIds[getConstraint(constraint, index, numberOfCriteria) > 0] + criterion2Id <- criterionIds[getConstraint(constraint, index, numberOfCriteria) < 0] + return(c(criterion1Id, criterion2Id)) +} + +getBound <- function(constraint, index, numberOfCriteria) { + bound <- -1 * constraint[['constr']][index + (numberOfCriteria + 1), getConstraint(constraint, index, numberOfCriteria) < 0] / + constraint[['constr']][index + (numberOfCriteria + 1), getConstraint(constraint, index, numberOfCriteria) > 0] + return(bound) +} + +getConstraint <- function(constraint, index, numberOfCriteria) { + return(constraint[['constr']][index + (numberOfCriteria + 1),]) +} + +generateNextQuestion <- function(params, constraints, edgeLengths, criterionIds, pvf) { + criterionIdsEdgeIndices <- calculateEdgeIndices(edgeLengths) + criterionIdsEdge <- criterionIds[criterionIdsEdgeIndices] + cutPoint <- calculateCutPoint(constraints, criterionIdsEdgeIndices) + + alternativeA <- rep(0, 2) + names(alternativeA) <- c("criterion1Value", "criterion2Value") + alternativeB <- alternativeA + + criterion1Range <- params[['criteria']][[which(criterionIds == criterionIdsEdge[1])]][['pvf']][['range']] + criterion2Range <- params[['criteria']][[which(criterionIds == criterionIdsEdge[2])]][['pvf']][['range']] + + alternativeA['criterion1Value'] <- selectQuestionValueFromRange(params, criterionIds, criterionIdsEdge[1], criterion1Range, 1, 2) + alternativeB['criterion1Value'] <- selectQuestionValueFromRange(params, criterionIds, criterionIdsEdge[1], criterion1Range, 2, 1) + + alternativeA['criterion2Value'] <- selectQuestionValueFromRange(params, criterionIds, criterionIdsEdge[2], criterion2Range, 2, 1) + alternativeB['criterion2Value'] <- selectQuestionValueFromRange(params, criterionIds, criterionIdsEdge[2], criterion2Range, 1, 2) + + if (cutPoint >= 1) { + alternativeB['criterion1Value'] <- calculateQuestionValue(pvf, criterionIdsEdge[1], 1 / cutPoint, criterion1Range) + } else { + alternativeA['criterion2Value'] <- calculateQuestionValue(pvf, criterionIdsEdge[2], cutPoint, criterion2Range) + } + + isFirstAlternativeA <- runif(1) <= 0.5 + if (isFirstAlternativeA) { + question <- list(A = alternativeA, B = alternativeB, criterionIds = criterionIdsEdge) + } else { + question <- list(A = alternativeB, B = alternativeA, criterionIds = criterionIdsEdge) + } + return(question) +} + +calculateEdgeIndices <- function(edgeLengths) { + candidateEdges <- which(edgeLengths == max(edgeLengths), arr.ind = T) + edge <- sample.int(nrow(candidateEdges), 1) + criterionIdsEdgeIndices <- c(min(candidateEdges[edge,]), max(candidateEdges[edge,])) + return(criterionIdsEdgeIndices) +} + +calculateCutPoint <- function(constraints, criterionIdsEdgeIndices) { + samples <- hitandrun(constraints, 1e2) + cutPoint <- median(samples[, criterionIdsEdgeIndices[1]] / samples[, criterionIdsEdgeIndices[2]]) + return(cutPoint) +} + +selectQuestionValueFromRange <- function(params, criterionIds, criterionId, criterionRange, index1, index2) { + if (params[['criteria']][[which(criterionIds == criterionId)]][['pvf']][['direction']] == "increasing") { + value <- criterionRange[index1] + } else { + value <- criterionRange[index2] + } + return(value) +} + +calculateQuestionValue <- function(pvf, criterionId, cutPointValue, criterionRange) { + value <- uniroot(f = function(x) { pvf[[criterionId]](x) - cutPointValue }, interval = criterionRange)[['root']] + stepSize <- 10 ^ floor(log10(diff(criterionRange)) - 1) + roundedValue <- round(value / stepSize, 0) * stepSize + return(roundedValue) +} + +generateConstraintsFromHistory <- function(params, criterionIds, pvf) { + numberOfCriteria <- length(params[['criteria']]) + constraints <- simplexConstraints(numberOfCriteria) + if (length(params[['answersAndQuestions']]) > 0) { + for (answerAndQuestion in params[['answersAndQuestions']]) { + currentQuestionConstraint <- generateCurrentQuestionConstraint(answerAndQuestion, numberOfCriteria, criterionIds, pvf) + constraints <- mergeConstraints(constraints, currentQuestionConstraint) + } + } + return(constraints) +} + +generateCurrentQuestionConstraint <- function(answerAndQuestion, numberOfCriteria, criterionIds, pvf) { + currentQuestionConstraint <- rep(0, numberOfCriteria) + + criterionId1 <- which(answerAndQuestion[['question']][['criterionIds']][1] == criterionIds) + currentQuestionConstraint[criterionId1] <- calculateConstraint(pvf, criterionId1, answerAndQuestion, "criterion1Value") + + criterionId2 <- which(answerAndQuestion[['question']][['criterionIds']][2] == criterionIds) + currentQuestionConstraint[criterionId2] <- calculateConstraint(pvf, criterionId2, answerAndQuestion, "criterion2Value") + + if (answerAndQuestion[['answer']] == "B") { + currentQuestionConstraint <- -1 * currentQuestionConstraint + } + currentQuestionConstraint <- list(constr = currentQuestionConstraint, dir = "<=", rhs = 0) + return(currentQuestionConstraint) +} + +calculateConstraint <- function(pvf, criterionId, answerAndQuestion, criterionSelector) { + constraint <- pvf[[criterionId]](answerAndQuestion[['question']][['B']][criterionSelector]) - pvf[[criterionId]](answerAndQuestion[['question']][['A']][criterionSelector]) + return(constraint) +} + +euclideanDistance <- function(x, y) { + stopifnot(length(x) == length(y)) + result <- sqrt(sum((x - y) ^ 2)) + return(result) +} + +caculateRatioBounds <- function(constr, index1, index2) { + # Guide on how to transform the constraint set to maximize or minimize a weight ratio wi/wj: http://lpsolve.sourceforge.net/5.1/ratio.htm + A <- cbind(-constr[['rhs']], constr[['constr']]) + b <- rep(0, length(constr[['rhs']])) + tranformedConstraint <- list(constr = A, rhs = b, dir = constr[['dir']]) + + c <- rep(0, ncol(constr[['constr']])) + c[index2] <- 1 + cTransformed <- c(0, c) + y0Constr <- list(constr = cTransformed, rhs = 1, dir = "=") + + tranformedConstraint <- mergeConstraints(tranformedConstraint, y0Constr) + hrepTransformed <- makeH(a1 = tranformedConstraint[['constr']][tranformedConstraint[['dir']] == "<=",], b1 = tranformedConstraint[['rhs']][tranformedConstraint[['dir']] == "<="], + a2 = tranformedConstraint[['constr']][tranformedConstraint[['dir']] == "=",], b2 = tranformedConstraint[['rhs']][tranformedConstraint[['dir']] == "="]) + + objectiveFunction <- rep(0, length(cTransformed)) + objectiveFunction[index1 + 1] <- 1 + + # Obtain upper bound for wi/wj + upper <- lpcdd(hrepTransformed, objectiveFunction, minimize = F) + if (upper[['solution.type']] == "Optimal") { + weightsUpper <- upper[['primal.solution']][2:length(upper[['primal.solution']])] / upper[['primal.solution']][1] + upperRatioBound <- weightsUpper[index1] / weightsUpper[index2] + } else { + upperRatioBound <- Inf + } + + # Obtain lower bound for wi/wj + lower <- lpcdd(hrepTransformed, objectiveFunction, minimize = T) + weightsLower <- lower[['primal.solution']][2:length(lower[['primal.solution']])] / lower[['primal.solution']][1] + lowerRatioBound <- weightsLower[index1] / weightsLower[index2] + + ratioBounds <- c(lowerRatioBound, upperRatioBound) + return(ratioBounds) +} + +obtainAllRatioBounds <- function(constraints) { + numberOfAttributes <- ncol(constraints[['constr']]) + ratioBounds <- array(dim = c(numberOfAttributes, numberOfAttributes, 2)) + for (index1 in 1:numberOfAttributes) { + for (index2 in 1:numberOfAttributes) { + if (index1 == index2) { + ratioBounds[index1, index2,] <- c(1, 1) + } else { + ratioBounds[index1, index2,] <- caculateRatioBounds(constraints, index1, index2) + } + } + } + return(ratioBounds) +} + +calculateEdgeRatioBoundIntersection <- function(numberOfcriteria, index1, index2, bound) { + # Hyperplane: wi / wj = bound + # Formulas taken from: https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection + + pointHyperplane <- rep(0, numberOfcriteria) # origin is always on plane + vertex1 <- pointHyperplane + vertex1[index1] <- 1 + vertex2 <- pointHyperplane + vertex2[index2] <- 1 + + if (bound == 0) { + intersectionPoint <- vertex2 + } else { + if (bound == Inf) { + intersectionPoint <- vertex1 + } else { + normalHyperplane <- rep(0, numberOfcriteria) + normalHyperplane[index1] <- 1 + normalHyperplane[index2] <- -bound + intersectionPoint <- vertex1 + as.numeric(((pointHyperplane - vertex1) %*% normalHyperplane) / ((vertex1 - vertex2) %*% normalHyperplane)) * (vertex1 - vertex2) + } + } + return(intersectionPoint) +} + +calculateEdgeLengths <- function(constraints) { + numberOfcriteria <- ncol(constraints[['constr']]) + edgeLengths <- matrix(0, nrow = numberOfcriteria, ncol = numberOfcriteria) + allRatioBounds <- obtainAllRatioBounds(constraints) + for (index1 in 1:(numberOfcriteria - 1)) { + for (index2 in (index1 + 1):numberOfcriteria) { + edgeLengths[index1, index2] <- euclideanDistance(calculateEdgeRatioBoundIntersection(numberOfcriteria, index1, index2, allRatioBounds[index1, index2, 1]), calculateEdgeRatioBoundIntersection(numberOfcriteria, index1, index2, allRatioBounds[index1, index2, 2])) + edgeLengths[index2, index1] <- edgeLengths[index1, index2] + } + } + return(round(edgeLengths, 3)) +} diff --git a/R/smaa.R b/R/smaa.R index e70ef7e6c..00ebe39f4 100644 --- a/R/smaa.R +++ b/R/smaa.R @@ -53,7 +53,7 @@ applyMeasurementUncertainty <- function(params, criteria, measurements) { for (criterion in criteria) { medianMeasurements[, criterion] <- pvf[[criterion]](medianMeasurements[, criterion]) } - for (i in 1:hitAndRunSamples) { + for (i in 1:dim(measurements)[1]) { measurements[i,,] <- medianMeasurements } return(measurements) diff --git a/R/util/constraint.R b/R/util/constraint.R index a49485255..8f03b27da 100644 --- a/R/util/constraint.R +++ b/R/util/constraint.R @@ -9,6 +9,8 @@ genHARconstraint <- function(statement, criteria) { return(getRatioBoundConstraint(statement$bounds, numberOfCriteria, index1, index2)) } else if (statement$type == "exact swing") { return(getRatioConstraint(numberOfCriteria, index1, index2, statement$ratio)) + } else if (statement$type == "upper ratio") { + return(upperRatioConstraint(numberOfCriteria, index1, index2, statement$bound)) } } diff --git a/R/util/pvf.R b/R/util/pvf.R index a66f00404..4663e27ad 100644 --- a/R/util/pvf.R +++ b/R/util/pvf.R @@ -1,5 +1,4 @@ # import sample form sampler.R -# import createPvf from pvf.R createPvf <- function(criterion) { pvf <- criterion$pvf diff --git a/app/ts/McdaApp/Workspace/CurrentScenarioContext/CurrentScenarioContext.tsx b/app/ts/McdaApp/Workspace/CurrentScenarioContext/CurrentScenarioContext.tsx index dba682673..30b382340 100644 --- a/app/ts/McdaApp/Workspace/CurrentScenarioContext/CurrentScenarioContext.tsx +++ b/app/ts/McdaApp/Workspace/CurrentScenarioContext/CurrentScenarioContext.tsx @@ -115,7 +115,10 @@ export function CurrentScenarioContextProviderComponent({ const getWeights = useCallback( (scenario: IMcdaScenario, pvfs: Record): void => { - if (scenario.state.prefs[0]?.elicitationMethod === 'imprecise') { + if ( + scenario.state.prefs[0]?.elicitationMethod === 'imprecise' || + scenario.state.prefs[0]?.elicitationMethod === 'choice' + ) { getWeightsFromPatavi(scenario, pvfs); } else { getWeightsThroughCalculation(scenario); diff --git a/app/ts/McdaApp/Workspace/CurrentTab/Preferences/Preferences.tsx b/app/ts/McdaApp/Workspace/CurrentTab/Preferences/Preferences.tsx index eeabb2728..109515727 100644 --- a/app/ts/McdaApp/Workspace/CurrentTab/Preferences/Preferences.tsx +++ b/app/ts/McdaApp/Workspace/CurrentTab/Preferences/Preferences.tsx @@ -15,6 +15,7 @@ import {EquivalentChangeContextProviderComponent} from './EquivalentChange/Equiv import AdvancedPartialValueFunction from './PartialValueFunctions/AdvancedPartialValueFunctions/AdvancedPartialValueFunction'; import {AdvancedPartialValueFunctionContextProviderComponent} from './PartialValueFunctions/AdvancedPartialValueFunctions/AdvancedPartialValueFunctionContext/AdvancedPartialValueFunctionContext'; import PreferencesView from './PreferencesView/PreferencesView'; +import {ErrorContext} from 'app/ts/Error/ErrorContext'; export default function Preferences() { const {filteredCriteria, stepSizesByCriterion} = useContext( @@ -22,7 +23,9 @@ export default function Preferences() { ); const {setActiveView, currentScenario, activeView, pvfs, updateScenario} = useContext(CurrentScenarioContext); - const {showPercentages} = useContext(SettingsContext); + const {showPercentages, showCbmPieChart} = useContext(SettingsContext); + const {setErrorMessage} = useContext(ErrorContext); + const { workspace: { properties: {title} @@ -64,6 +67,9 @@ export default function Preferences() { case 'ranking': document.title = 'Ranking'; break; + case 'choice': + document.title = 'Choice-based matching'; + break; case 'threshold': document.title = 'Threshold technique elicitation'; break; @@ -98,6 +104,8 @@ export default function Preferences() { manualLexicon={lexicon} manualHost={'@MCDA_HOST'} manualPath="/manual.html" + showCbmPieChart={showCbmPieChart} + setErrorMessage={setErrorMessage} /> ); } else { diff --git a/app/ts/McdaApp/Workspace/CurrentTab/Preferences/PreferencesWeights/PreferencesWeightsButtons/PreferencesWeightsButtons.tsx b/app/ts/McdaApp/Workspace/CurrentTab/Preferences/PreferencesWeights/PreferencesWeightsButtons/PreferencesWeightsButtons.tsx index e3359e250..fc9cf57ef 100644 --- a/app/ts/McdaApp/Workspace/CurrentTab/Preferences/PreferencesWeights/PreferencesWeightsButtons/PreferencesWeightsButtons.tsx +++ b/app/ts/McdaApp/Workspace/CurrentTab/Preferences/PreferencesWeights/PreferencesWeightsButtons/PreferencesWeightsButtons.tsx @@ -32,6 +32,9 @@ export default function PreferencesWeightsButtons() { setActiveView('imprecise'); } + function handleChoiceBasedClick() { + setActiveView('choice'); + } function handleThresholdClick() { setActiveView('threshold'); } @@ -88,6 +91,22 @@ export default function PreferencesWeightsButtons() { Imprecise Swing Weighting + + + { }); }); + describe('hasNonLinearPvf', () => { + it('should return true if there is a nonlinear pvf', () => { + const pvfs = { + crit1Id: { + type: 'linear' + } as TPvf, + crit2Id: { + type: 'piecewise-linear' + } as TPvf + }; + expect(hasNonLinearPvf(pvfs)).toBe(true); + }); + }); + describe('filterScenariosWithPvfs', () => { it('should filter out all the scenarios without pvfs for every criterion', () => { const scenarios = { @@ -280,7 +294,7 @@ describe('PreferencesUtil', () => { expect(result).toEqual('None'); }); - it('should return "Ranking"', () => { + it('should return "Ranking" if elicitation method is ranking', () => { const preferences: TPreferences = [ {elicitationMethod: 'ranking'} as IRanking ]; @@ -288,7 +302,7 @@ describe('PreferencesUtil', () => { expect(result).toEqual('Ranking'); }); - it('should return "Precise Swing Weighting"', () => { + it('should return "Precise Swing Weighting" if elicitation method is precise swing weighting', () => { const preferences: TPreferences = [ {elicitationMethod: 'precise'} as IRanking ]; @@ -296,7 +310,7 @@ describe('PreferencesUtil', () => { expect(result).toEqual('Precise Swing Weighting'); }); - it('should return "Matching"', () => { + it('should return "Matching" if elicitation method is matcing', () => { const preferences: TPreferences = [ {elicitationMethod: 'matching'} as IRanking ]; @@ -304,13 +318,29 @@ describe('PreferencesUtil', () => { expect(result).toEqual('Matching'); }); - it('should return "Imprecise Swing Weighting"', () => { + it('should return "Imprecise Swing Weighting" if elicitation method is imprecise swing weighting', () => { const preferences: TPreferences = [ {elicitationMethod: 'imprecise'} as IRanking ]; const result = determineElicitationMethod(preferences); expect(result).toEqual('Imprecise Swing Weighting'); }); + + it('should return "Threshold" if elicitation method is threshold', () => { + const preferences: TPreferences = [ + {elicitationMethod: 'threshold'} as IRanking + ]; + const result = determineElicitationMethod(preferences); + expect(result).toEqual('Threshold'); + }); + + it('should return "Choice-based Matching" if elicitation method is chioce-based matching', () => { + const preferences: TPreferences = [ + {elicitationMethod: 'choice'} as IRanking + ]; + const result = determineElicitationMethod(preferences); + expect(result).toEqual('Choice-based Matching'); + }); }); describe('createScenarioWithPvf', () => { @@ -515,6 +545,10 @@ describe('PreferencesUtil', () => { expect(isElicitationView('threshold')).toBeTruthy(); }); + it('should return true if the view is choice', () => { + expect(isElicitationView('choice')).toBeTruthy(); + }); + it('should return false if the view is advancedPvf', () => { expect(isElicitationView('advancedPvf')).toBeFalsy(); }); diff --git a/app/ts/McdaApp/Workspace/ScenariosContext/preferencesUtil.ts b/app/ts/McdaApp/Workspace/ScenariosContext/preferencesUtil.ts index 601bd6ba4..6ed1694a7 100644 --- a/app/ts/McdaApp/Workspace/ScenariosContext/preferencesUtil.ts +++ b/app/ts/McdaApp/Workspace/ScenariosContext/preferencesUtil.ts @@ -127,6 +127,8 @@ export function determineElicitationMethod(preferences: TPreferences): string { return 'Matching'; case 'imprecise': return 'Imprecise Swing Weighting'; + case 'choice': + return 'Choice-based Matching'; case 'threshold': return 'Threshold'; } @@ -186,7 +188,8 @@ export function isElicitationView(activeView: TPreferencesView): boolean { activeView === 'imprecise' || activeView === 'matching' || activeView === 'ranking' || - activeView === 'threshold' + activeView === 'threshold' || + activeView === 'choice' ); } diff --git a/app/ts/McdaApp/Workspace/SettingsContext/ISettingsContext.ts b/app/ts/McdaApp/Workspace/SettingsContext/ISettingsContext.ts index 7f078726a..42e7d69e7 100644 --- a/app/ts/McdaApp/Workspace/SettingsContext/ISettingsContext.ts +++ b/app/ts/McdaApp/Workspace/SettingsContext/ISettingsContext.ts @@ -9,6 +9,7 @@ export default interface ISettingsContext { settings: ISettings; numberOfToggledColumns: number; showPercentages: boolean; + showCbmPieChart: boolean; toggledColumns: IToggledColumns; getUsePercentage: (dataSource: IDataSource) => boolean; updateSettings: ( diff --git a/app/ts/McdaApp/Workspace/SettingsContext/SettingsContext.tsx b/app/ts/McdaApp/Workspace/SettingsContext/SettingsContext.tsx index 4220a3f47..0f31718a8 100644 --- a/app/ts/McdaApp/Workspace/SettingsContext/SettingsContext.tsx +++ b/app/ts/McdaApp/Workspace/SettingsContext/SettingsContext.tsx @@ -111,6 +111,7 @@ export function SettingsContextProviderComponent({children}: {children: any}) { numberOfToggledColumns, settings, showPercentages: settings.showPercentages === 'percentage', + showCbmPieChart: settings.showCbmPieChart, toggledColumns, getUsePercentage, updateSettings diff --git a/app/ts/McdaApp/Workspace/SettingsContext/SettingsUtil.test.ts b/app/ts/McdaApp/Workspace/SettingsContext/SettingsUtil.test.ts index e0ce05206..ca8eae889 100644 --- a/app/ts/McdaApp/Workspace/SettingsContext/SettingsUtil.test.ts +++ b/app/ts/McdaApp/Workspace/SettingsContext/SettingsUtil.test.ts @@ -87,7 +87,8 @@ describe('SettingsUtil', () => { displayMode: 'enteredEffects', randomSeed: 1234, calculationMethod: 'median', - showPercentages: 'percentage' + showPercentages: 'percentage', + showCbmPieChart: false }, defaultToggledColumns: { references: true, diff --git a/app/ts/McdaApp/Workspace/SettingsContext/SettingsUtil.ts b/app/ts/McdaApp/Workspace/SettingsContext/SettingsUtil.ts index 19238f83e..a29561d8b 100644 --- a/app/ts/McdaApp/Workspace/SettingsContext/SettingsUtil.ts +++ b/app/ts/McdaApp/Workspace/SettingsContext/SettingsUtil.ts @@ -46,7 +46,8 @@ export function getDefaultSettings( displayMode: getInitialDisplayMode(isRelativeProblem, hasNoEffects), randomSeed: 1234, calculationMethod: 'median', - showPercentages: 'percentage' + showPercentages: 'percentage', + showCbmPieChart: false }; const defaultToggledColumns: IToggledColumns = { diff --git a/app/ts/McdaApp/Workspace/WorkspaceSettings/WorkspaceSettingsContext/IWorkspaceSettingsContext.ts b/app/ts/McdaApp/Workspace/WorkspaceSettings/WorkspaceSettingsContext/IWorkspaceSettingsContext.ts index 56f1ba78b..70cab9a5f 100644 --- a/app/ts/McdaApp/Workspace/WorkspaceSettings/WorkspaceSettingsContext/IWorkspaceSettingsContext.ts +++ b/app/ts/McdaApp/Workspace/WorkspaceSettings/WorkspaceSettingsContext/IWorkspaceSettingsContext.ts @@ -11,7 +11,8 @@ export type TSettings = | 'calculationMethod' | 'displayMode' | 'randomSeed' - | 'showPercentages'; + | 'showPercentages' + | 'showCbmPieChart'; export default interface IWorkspaceSettingsContext { isSaveButtonDisabled: boolean; diff --git a/app/ts/WorkspaceSettings/CbmPieChartToggle/CbmPieChartToggle.tsx b/app/ts/WorkspaceSettings/CbmPieChartToggle/CbmPieChartToggle.tsx new file mode 100644 index 000000000..6a78cab1c --- /dev/null +++ b/app/ts/WorkspaceSettings/CbmPieChartToggle/CbmPieChartToggle.tsx @@ -0,0 +1,29 @@ +import {Checkbox, Grid, Typography} from '@material-ui/core'; +import {WorkspaceSettingsContext} from 'app/ts/McdaApp/Workspace/WorkspaceSettings/WorkspaceSettingsContext/WorkspaceSettingsContext'; +import {ChangeEvent, useContext} from 'react'; + +export default function CbmPieChartToggle(): JSX.Element { + const { + localSettings: {showCbmPieChart}, + setSetting + } = useContext(WorkspaceSettingsContext); + + function handleRadioChanged(event: ChangeEvent): void { + setSetting('showCbmPieChart', event.target.checked); + } + + return ( + + + Show pie chart during choice-based matching + + + + + + ); +} diff --git a/app/ts/util/SharedComponents/InlineHelp/lexicon.ts b/app/ts/util/SharedComponents/InlineHelp/lexicon.ts index 6ef77395f..69e556cb0 100644 --- a/app/ts/util/SharedComponents/InlineHelp/lexicon.ts +++ b/app/ts/util/SharedComponents/InlineHelp/lexicon.ts @@ -26,6 +26,11 @@ export const lexicon: Record = { text: 'Table with the central weights and confidence factors for each alternative. For alternatives with empty rows, there was not a single combination of sampled weights and criteria measurements that made this alternative the preferered treatment. These alternatives therefore do not have a central weight vector.', link: '#mcda-central-weights' }, + 'choice-based-matching': { + title: 'Choice-based matching', + text: "The choice-based matching elicitation method uses an automatically-generated series of questions to approximate the user's preferences.", + link: '#mcda-choice-based-matching-elicitation' + }, 'confidence-factor': { title: 'Confidence factor', text: "The probability that an alternative is the highest ranked treatment if that alternative's central weight vector is used to rank the alternatives.", diff --git a/index.ts b/index.ts index 1dbfae45d..290da945f 100644 --- a/index.ts +++ b/index.ts @@ -173,7 +173,7 @@ function initApp(): void { function errorHandler( error: OurError, - request: Request, + _request: Request, response: Response, next: any ): void { diff --git a/node-backend/choiceBasedMatchingHandler.ts b/node-backend/choiceBasedMatchingHandler.ts new file mode 100644 index 000000000..826b6b9bf --- /dev/null +++ b/node-backend/choiceBasedMatchingHandler.ts @@ -0,0 +1,31 @@ +import IChoiceBasedMatchingState from '@shared/interface/IChoiceBasedMatchingState'; +import {Request, Response} from 'express'; +import logger from './logger'; +import {postAndHandleResults} from './patavi'; + +export function getChoiceBasedMatchingState( + request: Request, + response: Response +) { + const oldCBMstate: IChoiceBasedMatchingState = request.body; + + postAndHandleResults( + { + ...oldCBMstate, + method: 'choiceBasedMatching' + }, + (error, result) => { + if (error) { + errorHandler(response, 500, error.message); + } else { + response.json(result); + } + } + ); +} + +function errorHandler(response: Response, status: number, message: string) { + logger.error(message); + response.status(status); + response.send(message); +} diff --git a/node-backend/getRequiredRights.ts b/node-backend/getRequiredRights.ts index 8a64381c8..562169148 100644 --- a/node-backend/getRequiredRights.ts +++ b/node-backend/getRequiredRights.ts @@ -1,6 +1,15 @@ import IRights, {requiredRightType} from '@shared/interface/IRights'; import {Response} from 'express'; +const GET = 'GET'; +const POST = 'POST'; +const PUT = 'PUT'; +const DELETE = 'DELETE'; +const NONE = 'none'; +const READ = 'read'; +const WRITE = 'write'; +const OWNER = 'owner'; + export default function getRequiredRights( workspaceOwnerRightsNeeded: ( response: Response, @@ -16,206 +25,207 @@ export default function getRequiredRights( ) => void ): IRights[] { return [ - makeRights('/v2/patavi', 'POST', 'none'), - makeRights('/v2/patavi/weights', 'POST', 'none'), - makeRights('/v2/patavi/scales', 'POST', 'none'), - makeRights('/v2/patavi/smaaResults', 'POST', 'none'), - makeRights('/v2/patavi/deterministicResults', 'POST', 'none'), - makeRights('/v2/patavi/recalculateDeterministicResults', 'POST', 'none'), - makeRights('/v2/patavi/measurementsSensitivity', 'POST', 'none'), - makeRights('/v2/patavi/preferencesSensitivity', 'POST', 'none'), + makeRights('/v2/patavi', POST, NONE), + makeRights('/v2/patavi/choice-based-matching-state', POST, NONE), + makeRights('/v2/patavi/weights', POST, NONE), + makeRights('/v2/patavi/scales', POST, NONE), + makeRights('/v2/patavi/smaaResults', POST, NONE), + makeRights('/v2/patavi/deterministicResults', POST, NONE), + makeRights('/v2/patavi/recalculateDeterministicResults', POST, NONE), + makeRights('/v2/patavi/measurementsSensitivity', POST, NONE), + makeRights('/v2/patavi/preferencesSensitivity', POST, NONE), - makeRights('/v2/workspaces', 'GET', 'none'), - makeRights('/v2/workspaces', 'POST', 'none'), + makeRights('/v2/workspaces', GET, NONE), + makeRights('/v2/workspaces', POST, NONE), - makeRights('/v2/premades', 'GET', 'none'), - makeRights('/v2/workspaces/createPremade', 'POST', 'none'), + makeRights('/v2/premades', GET, NONE), + makeRights('/v2/workspaces/createPremade', POST, NONE), makeRights( '/v2/workspaces/:workspaceId', - 'GET', - 'read', + GET, + READ, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId', - 'POST', - 'write', + POST, + WRITE, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId', - 'DELETE', - 'owner', + DELETE, + OWNER, workspaceOwnerRightsNeeded ), - makeRights('/v2/inProgress', 'GET', 'none', inProgressOwnerRightsNeeded), - makeRights('/v2/inProgress', 'POST', 'none', inProgressOwnerRightsNeeded), + makeRights('/v2/inProgress', GET, NONE, inProgressOwnerRightsNeeded), + makeRights('/v2/inProgress', POST, NONE, inProgressOwnerRightsNeeded), makeRights( '/v2/inProgress/:inProgressId', - 'GET', - 'none', + GET, + NONE, inProgressOwnerRightsNeeded ), makeRights( '/v2/inProgress/:inProgressId', - 'DELETE', - 'none', + DELETE, + NONE, inProgressOwnerRightsNeeded ), makeRights( '/v2/inProgress/:inProgressId', - 'PUT', - 'none', + PUT, + NONE, inProgressOwnerRightsNeeded ), makeRights( '/v2/inProgress/:inProgressId/criteria/:criterionId', - 'PUT', - 'none', + PUT, + NONE, inProgressOwnerRightsNeeded ), makeRights( '/v2/inProgress/:inProgressId/criteria/:criterionId', - 'DELETE', - 'none', + DELETE, + NONE, inProgressOwnerRightsNeeded ), makeRights( '/v2/inProgress/:inProgressId/criteria/:criterionId/dataSources/:dataSourceId', - 'PUT', - 'none', + PUT, + NONE, inProgressOwnerRightsNeeded ), makeRights( '/v2/inProgress/:inProgressId/criteria/:criterionId/dataSources/:dataSourceId', - 'DELETE', - 'none', + DELETE, + NONE, inProgressOwnerRightsNeeded ), makeRights( '/v2/inProgress/:inProgressId/alternatives/:alternativeId', - 'PUT', - 'none', + PUT, + NONE, inProgressOwnerRightsNeeded ), makeRights( '/v2/inProgress/:inProgressId/alternatives/:alternativeId', - 'DELETE', - 'none', + DELETE, + NONE, inProgressOwnerRightsNeeded ), makeRights( '/v2/inProgress/:inProgressId/cells', - 'PUT', - 'none', + PUT, + NONE, inProgressOwnerRightsNeeded ), makeRights( '/v2/inProgress/:inProgressId/doCreateWorkspace', - 'POST', - 'none', + POST, + NONE, inProgressOwnerRightsNeeded ), makeRights( '/v2/inProgress/createCopy', - 'POST', - 'none', + POST, + NONE, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/ordering', - 'GET', - 'read', + GET, + READ, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/ordering', - 'PUT', - 'write', + PUT, + WRITE, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/workspaceSettings', - 'GET', - 'read', + GET, + READ, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/workspaceSettings', - 'PUT', - 'write', + PUT, + WRITE, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/problems', - 'GET', - 'read', + GET, + READ, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/problems/:subproblemId', - 'GET', - 'read', + GET, + READ, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/problems', - 'POST', - 'write', + POST, + WRITE, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/problems/:subproblemId', - 'POST', - 'write', + POST, + WRITE, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/problems/:subproblemId', - 'DELETE', - 'write', + DELETE, + WRITE, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/scenarios', - 'GET', - 'read', + GET, + READ, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/problems/:subproblemId/scenarios', - 'GET', - 'read', + GET, + READ, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/problems/:subproblemId/scenarios/:scenarioId', - 'GET', - 'read', + GET, + READ, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/problems/:subproblemId/scenarios', - 'POST', - 'write', + POST, + WRITE, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/problems/:subproblemId/scenarios/:scenarioId', - 'PUT', - 'write', + PUT, + WRITE, workspaceOwnerRightsNeeded ), makeRights( '/v2/workspaces/:workspaceId/problems/:subproblemId/scenarios/:scenarioId', - 'DELETE', - 'write', + DELETE, + WRITE, workspaceOwnerRightsNeeded ) ]; diff --git a/node-backend/patavi.ts b/node-backend/patavi.ts index 2a0361f31..ea9247973 100644 --- a/node-backend/patavi.ts +++ b/node-backend/patavi.ts @@ -4,9 +4,8 @@ import {ISmaaResults} from '@shared/interface/Patavi/ISmaaResults'; import {TPataviCommands} from '@shared/types/PataviCommands'; import {TPataviResults} from '@shared/types/PataviResults'; import Axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from 'axios'; -import {IncomingMessage} from 'http'; import _ from 'lodash'; -import {RawData, WebSocket, MessageEvent} from 'ws'; +import {MessageEvent, WebSocket} from 'ws'; import logger from './logger'; const {PATAVI_API_KEY} = process.env; diff --git a/node-backend/pataviRouter.ts b/node-backend/pataviRouter.ts index 91a020633..f98f58e85 100644 --- a/node-backend/pataviRouter.ts +++ b/node-backend/pataviRouter.ts @@ -1,10 +1,12 @@ import {Router} from 'express'; +import {getChoiceBasedMatchingState} from './choiceBasedMatchingHandler'; import IDB from './interface/IDB'; import PataviHandler from './pataviHandler'; export default function PataviRouter(db: IDB) { const {getWeights, getPataviResults} = PataviHandler(db); return Router() + .post('/choice-based-matching-state', getChoiceBasedMatchingState) .post('/deterministicResults', getPataviResults) .post('/measurementsSensitivity', getPataviResults) .post('/preferencesSensitivity', getPataviResults) diff --git a/package.json b/package.json index 760bf4013..80558ad01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mcda-web", - "version": "1.1.0", + "version": "1.1.1", "description": "MCDA elicitation web interface (remote storage)", "repository": { "type": "git", @@ -102,7 +102,7 @@ "mini-css-extract-plugin": "^1.3.4", "nightwatch": "^1.7.11", "optimize-css-assets-webpack-plugin": "^6.0.1", - "preference-elicitation": "drugis/preference-elicitation#1.0.9", + "preference-elicitation": "drugis/preference-elicitation#1.1.4", "raw-loader": "~4.0.2", "react": "^17.0.2", "react-c3-component": "^2.0.0", diff --git a/public/mcda-page-titles.json b/public/mcda-page-titles.json index ee858b326..8655143fc 100644 --- a/public/mcda-page-titles.json +++ b/public/mcda-page-titles.json @@ -12,5 +12,6 @@ "smaa-results": "SmaaResultsController", "swing-weighting": "SwingWeightingController", "matching": "MatchingElicitationController", - "partial-value-function": "PartialValueFunctionController" + "partial-value-function": "PartialValueFunctionController", + "choice-based-matching": "ChoiceBasedMatchingController" } diff --git a/shared/interface/IAnswerAndQuestion.ts b/shared/interface/IAnswerAndQuestion.ts new file mode 100644 index 000000000..e63e46bac --- /dev/null +++ b/shared/interface/IAnswerAndQuestion.ts @@ -0,0 +1,6 @@ +import IChoiceBasedMatchingQuestion from './IChoiceBasedMatchingQuestion'; + +export default interface IAnswerAndQuestion { + question: IChoiceBasedMatchingQuestion; + answer?: 'A' | 'B'; +} diff --git a/shared/interface/IChoiceBasedMatchingQuestion.ts b/shared/interface/IChoiceBasedMatchingQuestion.ts new file mode 100644 index 000000000..a6fea2864 --- /dev/null +++ b/shared/interface/IChoiceBasedMatchingQuestion.ts @@ -0,0 +1,5 @@ +export default interface IChoiceBasedMatchingQuestion { + A: {criterion1Value: number; criterion2Value: number}; + B: {criterion1Value: number; criterion2Value: number}; + criterionIds: [string, string]; +} diff --git a/shared/interface/IChoiceBasedMatchingState.ts b/shared/interface/IChoiceBasedMatchingState.ts new file mode 100644 index 000000000..96409014e --- /dev/null +++ b/shared/interface/IChoiceBasedMatchingState.ts @@ -0,0 +1,9 @@ +import IAnswerAndQuestion from './IAnswerAndQuestion'; +import IReducedCriterion from './IReducedCriterion'; +import IUpperRatioConstraint from './Scenario/IUpperRatioConstraint'; + +export default interface IChoiceBasedMatchingState { + preferences?: IUpperRatioConstraint[]; + answersAndQuestions: IAnswerAndQuestion[]; + criteria: IReducedCriterion[]; +} diff --git a/shared/interface/IReducedCriterion.ts b/shared/interface/IReducedCriterion.ts new file mode 100644 index 000000000..2fe0389b4 --- /dev/null +++ b/shared/interface/IReducedCriterion.ts @@ -0,0 +1,6 @@ +import {TPvf} from '@shared/interface/Problem/IPvf'; + +export default interface IReducedCriterion { + id: string; + pvf: TPvf; +} diff --git a/shared/interface/Patavi/IChoiceBasedMatchingCommand.ts b/shared/interface/Patavi/IChoiceBasedMatchingCommand.ts new file mode 100644 index 000000000..d0b9ecde1 --- /dev/null +++ b/shared/interface/Patavi/IChoiceBasedMatchingCommand.ts @@ -0,0 +1,6 @@ +import IChoiceBasedMatchingState from '../IChoiceBasedMatchingState'; + +export default interface IChoiceBasedMatchingCommand + extends IChoiceBasedMatchingState { + method: 'choiceBasedMatching'; +} diff --git a/shared/interface/Scenario/IUpperRatioConstraint.ts b/shared/interface/Scenario/IUpperRatioConstraint.ts new file mode 100644 index 000000000..c4052f567 --- /dev/null +++ b/shared/interface/Scenario/IUpperRatioConstraint.ts @@ -0,0 +1,7 @@ +import IPreference from './IPreference'; + +export default interface IUpperRatioConstraint extends IPreference { + type: 'upper ratio'; + bound: number; + criteria: [string, string]; +} diff --git a/shared/interface/Settings/ISettings.ts b/shared/interface/Settings/ISettings.ts index 679ab35bd..aa85c370c 100644 --- a/shared/interface/Settings/ISettings.ts +++ b/shared/interface/Settings/ISettings.ts @@ -5,6 +5,7 @@ import {TScalesCalculationMethod} from './TScalesCalculationMethod'; export default interface ISettings { calculationMethod: TScalesCalculationMethod; showPercentages: TPercentageOrDecimal; + showCbmPieChart: boolean; displayMode: TDisplayMode; randomSeed: number; } diff --git a/shared/types/PataviCommands.ts b/shared/types/PataviCommands.ts index fe5e45c12..1e876e022 100644 --- a/shared/types/PataviCommands.ts +++ b/shared/types/PataviCommands.ts @@ -1,3 +1,4 @@ +import IChoiceBasedMatchingCommand from '@shared/interface/Patavi/IChoiceBasedMatchingCommand'; import {IDeterministicResultsCommand} from '@shared/interface/Patavi/IDeterministicResultsCommand'; import {IMeasurementsSensitivityCommand} from '@shared/interface/Patavi/IMeasurementsSensitivityCommand'; import {IPataviProblem} from '@shared/interface/Patavi/IPataviProblem'; @@ -15,4 +16,5 @@ export type TPataviCommands = | IRecalculatedDeterministicResultsCommand | IMeasurementsSensitivityCommand | IPreferencesSensitivityCommand - | IScalesCommand; + | IScalesCommand + | IChoiceBasedMatchingCommand; diff --git a/shared/types/preferences.ts b/shared/types/preferences.ts index 70364c0e7..22577098f 100644 --- a/shared/types/preferences.ts +++ b/shared/types/preferences.ts @@ -1,10 +1,12 @@ import IExactSwingRatio from '@shared/interface/Scenario/IExactSwingRatio'; import IRanking from '@shared/interface/Scenario/IRanking'; import IRatioBoundConstraint from '@shared/interface/Scenario/IRatioBoundConstraint'; +import IUpperRatioConstraint from '@shared/interface/Scenario/IUpperRatioConstraint'; export type TPreferences = | IRanking[] | IExactSwingRatio[] - | IRatioBoundConstraint[]; + | IRatioBoundConstraint[] + | IUpperRatioConstraint[]; export type TPreference = IRanking | IExactSwingRatio | IRatioBoundConstraint; diff --git a/yarn.lock b/yarn.lock index b3a230d5d..292b06b87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7673,9 +7673,9 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" -preference-elicitation@drugis/preference-elicitation#1.0.9: - version "1.0.9" - resolved "https://codeload.github.com/drugis/preference-elicitation/tar.gz/e0d591321d17e3a732e0903e0cc2af866ba1c5d6" +preference-elicitation@drugis/preference-elicitation#1.1.4: + version "1.1.4" + resolved "https://codeload.github.com/drugis/preference-elicitation/tar.gz/0e9f188c3d810b45c4ba58edff1632050c2b4557" prelude-ls@^1.2.1: version "1.2.1"