add algorithm settings

This commit is contained in:
sbriat 2023-04-12 14:59:25 +02:00
parent b7822cf88f
commit dd957763a3
12 changed files with 278 additions and 38 deletions

View File

@ -5,10 +5,39 @@ SERVICE_CONFIGURATION_DOMAIN=MATCHER
HEALTH_SERVICE_PORT=6005 HEALTH_SERVICE_PORT=6005
# DEFAULT CONFIGURATION # DEFAULT CONFIGURATION
# default identifier used for match requests
DEFAULT_IDENTIFIER=0 DEFAULT_IDENTIFIER=0
MARGIN_DURATION=900 # default timezone
VALIDITY_DURATION=365
DEFAULT_TIMEZONE=Europe/Paris DEFAULT_TIMEZONE=Europe/Paris
# default number of seats proposed as driver
DEFAULT_SEATS=3
# algorithm type
ALGORITHM=classic
# strict algorithm (if relevant with the algorithm type)
# if set to true, matches are made so that
# punctual ads match only with punctual ads and
# recurrent ads match only with recurrent ads
STRICT_ALGORITHM=0
# max distance in metres between driver
# route and passenger pick-up / drop-off
REMOTENESS=15000
# use passenger proportion
USE_PROPORTION=1
# minimal driver proportion
PROPORTION=0.3
# use azimuth calculation
USE_AZIMUTH=1
# azimuth margin
AZIMUTH_MARGIN=10
# margin duration in seconds
MARGIN_DURATION=900
# default validity duration (in days) for recurrent proposals
VALIDITY_DURATION=365
# max detour ratio
MAX_DETOUR_DISTANCE_RATIO=0.3
MAX_DETOUR_DURATION_RATIO=0.3
# PRISMA # PRISMA
DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher" DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=matcher"

View File

@ -14,6 +14,24 @@ export class DefaultParamsProvider {
MARGIN_DURATION: parseInt(this.configService.get('MARGIN_DURATION')), MARGIN_DURATION: parseInt(this.configService.get('MARGIN_DURATION')),
VALIDITY_DURATION: parseInt(this.configService.get('VALIDITY_DURATION')), VALIDITY_DURATION: parseInt(this.configService.get('VALIDITY_DURATION')),
DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'), DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'),
DEFAULT_SEATS: parseInt(this.configService.get('DEFAULT_SEATS')),
DEFAULT_ALGORITHM_SETTINGS: {
algorithm: this.configService.get('ALGORITHM'),
strict: !!parseInt(this.configService.get('STRICT_ALGORITHM')),
remoteness: parseInt(this.configService.get('REMOTENESS')),
useProportion: !!parseInt(this.configService.get('USE_PROPORTION')),
proportion: parseInt(this.configService.get('PROPORTION')),
useAzimuth: !!parseInt(this.configService.get('USE_AZIMUTH')),
azimuthMargin: parseInt(this.configService.get('AZIMUTH_MARGIN')),
maxDetourDistanceRatio: parseFloat(
this.configService.get('MAX_DETOUR_DISTANCE_RATIO'),
),
maxDetourDurationRatio: parseFloat(
this.configService.get('MAX_DETOUR_DURATION_RATIO'),
),
georouterType: this.configService.get('GEOROUTER_TYPE'),
georouterUrl: this.configService.get('GEOROUTER_URL'),
},
}; };
} }
} }

View File

@ -17,9 +17,16 @@ import { Algorithm } from './algorithm.enum';
import { IRequestTime } from '../interfaces/time-request.interface'; import { IRequestTime } from '../interfaces/time-request.interface';
import { IRequestPerson } from '../interfaces/person-request.interface'; import { IRequestPerson } from '../interfaces/person-request.interface';
import { IRequestGeography } from '../interfaces/geography-request.interface'; import { IRequestGeography } from '../interfaces/geography-request.interface';
import { IRequestRequirement } from '../interfaces/requirement-request.interface';
import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-request.interface';
export class MatchRequest export class MatchRequest
implements IRequestTime, IRequestPerson, IRequestGeography implements
IRequestTime,
IRequestPerson,
IRequestGeography,
IRequestRequirement,
IRequestAlgorithmSettings
{ {
@IsArray() @IsArray()
@AutoMap() @AutoMap()
@ -65,11 +72,15 @@ export class MatchRequest
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Min(1)
@Max(10)
@AutoMap() @AutoMap()
seatsPassenger: number; seatsPassenger: number;
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
@Min(1)
@Max(10)
@AutoMap() @AutoMap()
seatsDriver: number; seatsDriver: number;

View File

@ -0,0 +1,58 @@
import { Algorithm } from '../dtos/algorithm.enum';
import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-request.interface';
import { DefaultAlgorithmSettings } from '../interfaces/default-algorithm-settings.type';
import { TimingFrequency } from './timing';
export class AlgorithmSettings {
_algorithmSettingsRequest: IRequestAlgorithmSettings;
_strict: boolean;
algorithm: Algorithm;
restrict: TimingFrequency;
remoteness: number;
useProportion: boolean;
proportion: number;
useAzimuth: boolean;
azimuthMargin: number;
maxDetourDurationRatio: number;
maxDetourDistanceRatio: number;
georouterType: string;
georouterUrl: string;
constructor(
algorithmSettingsRequest: IRequestAlgorithmSettings,
defaultAlgorithmSettings: DefaultAlgorithmSettings,
frequency: TimingFrequency,
) {
this._algorithmSettingsRequest = algorithmSettingsRequest;
this.algorithm =
algorithmSettingsRequest.algorithm ?? defaultAlgorithmSettings.algorithm;
this._strict =
algorithmSettingsRequest.strict ?? defaultAlgorithmSettings.strict;
this.remoteness = algorithmSettingsRequest.remoteness
? Math.abs(algorithmSettingsRequest.remoteness)
: defaultAlgorithmSettings.remoteness;
this.useProportion =
algorithmSettingsRequest.useProportion ??
defaultAlgorithmSettings.useProportion;
this.proportion = algorithmSettingsRequest.proportion
? Math.abs(algorithmSettingsRequest.proportion)
: defaultAlgorithmSettings.proportion;
this.useAzimuth =
algorithmSettingsRequest.useAzimuth ??
defaultAlgorithmSettings.useAzimuth;
this.azimuthMargin = algorithmSettingsRequest.azimuthMargin
? Math.abs(algorithmSettingsRequest.azimuthMargin)
: defaultAlgorithmSettings.azimuthMargin;
this.maxDetourDistanceRatio =
algorithmSettingsRequest.maxDetourDistanceRatio ??
defaultAlgorithmSettings.maxDetourDistanceRatio;
this.maxDetourDurationRatio =
algorithmSettingsRequest.maxDetourDurationRatio ??
defaultAlgorithmSettings.maxDetourDurationRatio;
this.georouterType = defaultAlgorithmSettings.georouterType;
this.georouterUrl = defaultAlgorithmSettings.georouterUrl;
if (this._strict) {
this.restrict = frequency;
}
}
}

View File

@ -1,4 +1,13 @@
import { IRequestRequirement } from '../interfaces/requirement-request.interface';
export class Requirement { export class Requirement {
_requirementRequest: IRequestRequirement;
seatsDriver: number; seatsDriver: number;
seatsPassenger: number; seatsPassenger: number;
constructor(requirementRequest: IRequestRequirement, defaultSeats: number) {
this._requirementRequest = requirementRequest;
this.seatsDriver = requirementRequest.seatsDriver ?? defaultSeats;
this.seatsPassenger = requirementRequest.seatsPassenger ?? 1;
}
} }

View File

@ -1,14 +1,13 @@
import { Georouter } from '../interfaces/georouter.interface'; import { Algorithm } from '../dtos/algorithm.enum';
export class Settings { export interface IRequestAlgorithmSettings {
algorithm: Algorithm; algorithm: Algorithm;
restrict: boolean; strict: boolean;
remoteness: number; remoteness: number;
useProportion: boolean; useProportion: boolean;
proportion: number; proportion: number;
useAzimuth: boolean; useAzimuth: boolean;
azimuthMargin: number; azimuthMargin: number;
maxDetourDurationRatio: number;
maxDetourDistanceRatio: number; maxDetourDistanceRatio: number;
georouter: Georouter; maxDetourDurationRatio: number;
} }

View File

@ -0,0 +1,15 @@
import { Algorithm } from '../dtos/algorithm.enum';
export type DefaultAlgorithmSettings = {
algorithm: Algorithm;
strict: boolean;
remoteness: number;
useProportion: boolean;
proportion: number;
useAzimuth: boolean;
azimuthMargin: number;
maxDetourDistanceRatio: number;
maxDetourDurationRatio: number;
georouterType: string;
georouterUrl: string;
};

View File

@ -1,6 +1,10 @@
import { DefaultAlgorithmSettings } from './default-algorithm-settings.type';
export type IDefaultParams = { export type IDefaultParams = {
DEFAULT_IDENTIFIER: number; DEFAULT_IDENTIFIER: number;
MARGIN_DURATION: number; MARGIN_DURATION: number;
VALIDITY_DURATION: number; VALIDITY_DURATION: number;
DEFAULT_TIMEZONE: string; DEFAULT_TIMEZONE: string;
DEFAULT_SEATS: number;
DEFAULT_ALGORITHM_SETTINGS: DefaultAlgorithmSettings;
}; };

View File

@ -0,0 +1,4 @@
export interface IRequestRequirement {
seatsDriver?: number;
seatsPassenger?: number;
}

View File

@ -3,7 +3,7 @@ import { Geography } from '../domain/entities/geography';
import { Person } from '../domain/entities/person'; import { Person } from '../domain/entities/person';
import { Requirement } from '../domain/entities/requirement'; import { Requirement } from '../domain/entities/requirement';
import { Role } from '../domain/entities/role.enum'; import { Role } from '../domain/entities/role.enum';
import { Settings } from '../domain/entities/settings'; import { AlgorithmSettings } from '../domain/entities/algorithm-settings';
import { Time } from '../domain/entities/time'; import { Time } from '../domain/entities/time';
import { IDefaultParams } from '../domain/interfaces/default-params.type'; import { IDefaultParams } from '../domain/interfaces/default-params.type';
@ -16,7 +16,7 @@ export class MatchQuery {
geography: Geography; geography: Geography;
exclusions: Array<number>; exclusions: Array<number>;
requirement: Requirement; requirement: Requirement;
settings: Settings; algorithmSettings: AlgorithmSettings;
constructor(matchRequest: MatchRequest, defaultParams: IDefaultParams) { constructor(matchRequest: MatchRequest, defaultParams: IDefaultParams) {
this._matchRequest = matchRequest; this._matchRequest = matchRequest;
@ -25,15 +25,11 @@ export class MatchQuery {
this._setRoles(); this._setRoles();
this._setTime(); this._setTime();
this._setGeography(); this._setGeography();
this._initialize(); this._setRequirement();
this._setAlgorithmSettings();
this._setExclusions(); this._setExclusions();
} }
_initialize() {
this.requirement = new Requirement();
this.settings = new Settings();
}
_setPerson() { _setPerson() {
this.person = new Person( this.person = new Person(
this._matchRequest, this._matchRequest,
@ -67,6 +63,21 @@ export class MatchQuery {
this.geography.init(); this.geography.init();
} }
_setRequirement() {
this.requirement = new Requirement(
this._matchRequest,
this._defaultParams.DEFAULT_SEATS,
);
}
_setAlgorithmSettings() {
this.algorithmSettings = new AlgorithmSettings(
this._matchRequest,
this._defaultParams.DEFAULT_ALGORITHM_SETTINGS,
this.time.frequency,
);
}
_setExclusions() { _setExclusions() {
this.exclusions = []; this.exclusions = [];
if (this._matchRequest.identifier) if (this._matchRequest.identifier)

View File

@ -1,5 +1,7 @@
import { Algorithm } from '../../domain/dtos/algorithm.enum';
import { MatchRequest } from '../../domain/dtos/match.request'; import { MatchRequest } from '../../domain/dtos/match.request';
import { Role } from '../../domain/entities/role.enum'; import { Role } from '../../domain/entities/role.enum';
import { TimingFrequency } from '../../domain/entities/timing';
import { IDefaultParams } from '../../domain/interfaces/default-params.type'; import { IDefaultParams } from '../../domain/interfaces/default-params.type';
import { MatchQuery } from '../../queries/match.query'; import { MatchQuery } from '../../queries/match.query';
@ -8,6 +10,20 @@ const defaultParams: IDefaultParams = {
MARGIN_DURATION: 900, MARGIN_DURATION: 900,
VALIDITY_DURATION: 365, VALIDITY_DURATION: 365,
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: {
algorithm: Algorithm.CLASSIC,
strict: false,
remoteness: 15000,
useProportion: true,
proportion: 0.3,
useAzimuth: true,
azimuthMargin: 10,
maxDetourDistanceRatio: 0.3,
maxDetourDurationRatio: 0.3,
georouterType: 'graphhopper',
georouterUrl: 'http://localhost',
},
}; };
describe('Match query', () => { describe('Match query', () => {
@ -28,28 +44,23 @@ describe('Match query', () => {
expect(matchQuery).toBeDefined(); expect(matchQuery).toBeDefined();
}); });
describe('init', () => { it('should create a query with excluded identifiers', () => {
it('should create a query with excluded identifiers', () => { const matchRequest: MatchRequest = new MatchRequest();
const matchRequest: MatchRequest = new MatchRequest(); matchRequest.departure = '2023-04-01 12:00';
matchRequest.departure = '2023-04-01 12:00'; matchRequest.waypoints = [
matchRequest.waypoints = [ {
{ lat: 49.440041,
lat: 49.440041, lon: 1.093912,
lon: 1.093912, },
}, {
{ lat: 50.630992,
lat: 50.630992, lon: 3.045432,
lon: 3.045432, },
}, ];
]; matchRequest.identifier = 125;
matchRequest.identifier = 125; matchRequest.exclusions = [126, 127, 128];
matchRequest.exclusions = [126, 127, 128]; const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams);
const matchQuery: MatchQuery = new MatchQuery( expect(matchQuery.exclusions.length).toBe(4);
matchRequest,
defaultParams,
);
expect(matchQuery.exclusions.length).toBe(4);
});
}); });
it('should create a query with driver role only', () => { it('should create a query with driver role only', () => {
@ -108,4 +119,60 @@ describe('Match query', () => {
expect(matchQuery.roles).toContain(Role.PASSENGER); expect(matchQuery.roles).toContain(Role.PASSENGER);
expect(matchQuery.roles).toContain(Role.DRIVER); expect(matchQuery.roles).toContain(Role.DRIVER);
}); });
it('should create a query with number of seats modified', () => {
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
matchRequest.seatsDriver = 1;
matchRequest.seatsPassenger = 2;
const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams);
expect(matchQuery.requirement.seatsDriver).toBe(1);
expect(matchQuery.requirement.seatsPassenger).toBe(2);
});
it('should create a query with modified algorithm settings', () => {
const matchRequest: MatchRequest = new MatchRequest();
matchRequest.departure = '2023-04-01 12:00';
matchRequest.waypoints = [
{
lat: 49.440041,
lon: 1.093912,
},
{
lat: 50.630992,
lon: 3.045432,
},
];
matchRequest.algorithm = Algorithm.CLASSIC;
matchRequest.strict = true;
matchRequest.useProportion = true;
matchRequest.proportion = 0.45;
matchRequest.useAzimuth = true;
matchRequest.azimuthMargin = 15;
matchRequest.remoteness = 20000;
matchRequest.maxDetourDistanceRatio = 0.41;
matchRequest.maxDetourDurationRatio = 0.42;
const matchQuery: MatchQuery = new MatchQuery(matchRequest, defaultParams);
expect(matchQuery.algorithmSettings.algorithm).toBe(Algorithm.CLASSIC);
expect(matchQuery.algorithmSettings.restrict).toBe(
TimingFrequency.FREQUENCY_PUNCTUAL,
);
expect(matchQuery.algorithmSettings.useProportion).toBeTruthy();
expect(matchQuery.algorithmSettings.proportion).toBe(0.45);
expect(matchQuery.algorithmSettings.useAzimuth).toBeTruthy();
expect(matchQuery.algorithmSettings.azimuthMargin).toBe(15);
expect(matchQuery.algorithmSettings.remoteness).toBe(20000);
expect(matchQuery.algorithmSettings.maxDetourDistanceRatio).toBe(0.41);
expect(matchQuery.algorithmSettings.maxDetourDurationRatio).toBe(0.42);
});
}); });

View File

@ -7,6 +7,7 @@ import { AdRepository } from '../../adapters/secondaries/ad.repository';
import { AutomapperModule } from '@automapper/nestjs'; import { AutomapperModule } from '@automapper/nestjs';
import { classes } from '@automapper/classes'; import { classes } from '@automapper/classes';
import { IDefaultParams } from '../../domain/interfaces/default-params.type'; import { IDefaultParams } from '../../domain/interfaces/default-params.type';
import { Algorithm } from '../../domain/dtos/algorithm.enum';
const mockAdRepository = {}; const mockAdRepository = {};
@ -19,6 +20,20 @@ const defaultParams: IDefaultParams = {
MARGIN_DURATION: 900, MARGIN_DURATION: 900,
VALIDITY_DURATION: 365, VALIDITY_DURATION: 365,
DEFAULT_TIMEZONE: 'Europe/Paris', DEFAULT_TIMEZONE: 'Europe/Paris',
DEFAULT_SEATS: 3,
DEFAULT_ALGORITHM_SETTINGS: {
algorithm: Algorithm.CLASSIC,
strict: false,
remoteness: 15000,
useProportion: true,
proportion: 0.3,
useAzimuth: true,
azimuthMargin: 10,
maxDetourDistanceRatio: 0.3,
maxDetourDurationRatio: 0.3,
georouterType: 'graphhopper',
georouterUrl: 'http://localhost',
},
}; };
describe('MatchUseCase', () => { describe('MatchUseCase', () => {