Skip to content

Commit

Permalink
Refonte des segments - ajout du critère de maillage et implémentation…
Browse files Browse the repository at this point in the history
… selon méthodologie EFCA (#3920)

## Linked issues

- [x] Resolve #3912
- [x] Resolve #3913
- [x] Resolve #3918
- [x] Resolve #3919
- [x] Resolve #3980
- [ ] Ajouter le maillage dans le formulaire de PNO manuel
- [x] Ajouter le type `scip_species_type` dans la table `species`
(migration/pipeline)
- [ ] Cas limite : espèce à 0 kg pour les cas où on sait qu'un navire
cible une espèce mais l'espère n'est pas à bord au moment du contrôle
- [x] back :
https://github.com/MTES-MCT/monitorfish/pull/3920/files#diff-3a06becd8bf849c4d6f39e570c5846bd989138c508d6406cf0540bd5f099b878R72
  - [ ] pipeline
  • Loading branch information
louptheron authored Jan 15, 2025
2 parents a18cfd2 + 0f11aa4 commit 5b2cc6b
Show file tree
Hide file tree
Showing 117 changed files with 17,503 additions and 15,479 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ data class FleetSegment(
val segment: String,
// TODO Rename that to `name`.
val segmentName: String,
val dirm: List<String> = listOf(),
val mainScipSpeciesType: ScipSpeciesType?,
val maxMesh: Double?,
val minMesh: Double?,
val minShareOfTargetSpecies: Double?,
val priority: Double,
val vesselTypes: List<String>,
val gears: List<String>,
val faoAreas: List<String>,
val targetSpecies: List<String>,
val bycatchSpecies: List<String> = listOf(),
val impactRiskFactor: Double,
val year: Int,
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package fr.gouv.cnsp.monitorfish.domain.entities.fleet_segment

enum class ScipSpeciesType {
PELAGIC,
DEMERSAL,
TUNA,
OTHER,
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ data class Gear(
val code: String,
val name: String,
val category: String? = null,
var groupId: Int? = null,
val groupId: Int? = null,
val isMeshRequiredForSegment: Boolean? = null,
)
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
package fr.gouv.cnsp.monitorfish.domain.entities.species

data class Species(val code: String, val name: String)
import fr.gouv.cnsp.monitorfish.domain.entities.fleet_segment.ScipSpeciesType

data class Species(
val code: String,
val name: String,
val scipSpeciesType: ScipSpeciesType?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ interface FleetSegmentRepository {
year: Int,
): List<FleetSegment>

fun create(segment: FleetSegment): FleetSegment
fun save(segment: FleetSegment): FleetSegment

fun findYearEntries(): List<Int>

fun addYear(
currentYear: Int,
nextYear: Int,
)

fun findAllSegmentsGearsWithRequiredMesh(year: Int): List<String>
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package fr.gouv.cnsp.monitorfish.domain.use_cases.dtos

import fr.gouv.cnsp.monitorfish.domain.entities.fleet_segment.ScipSpeciesType

data class CreateOrUpdateFleetSegmentFields(
var segment: String? = null,
var segmentName: String? = null,
var gears: List<String>? = null,
var faoAreas: List<String>? = null,
var targetSpecies: List<String>? = null,
var bycatchSpecies: List<String>? = null,
var impactRiskFactor: Double? = null,
var year: Int? = null,
val segment: String? = null,
val segmentName: String? = null,
val gears: List<String>? = null,
val faoAreas: List<String>? = null,
val targetSpecies: List<String>? = null,
val mainScipSpeciesType: ScipSpeciesType? = null,
val maxMesh: Double? = null,
val minMesh: Double? = null,
val minShareOfTargetSpecies: Double? = null,
val priority: Double? = null,
val vesselTypes: List<String>? = null,
val impactRiskFactor: Double? = null,
val year: Int? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,126 @@ package fr.gouv.cnsp.monitorfish.domain.use_cases.fleet_segment
import fr.gouv.cnsp.monitorfish.config.UseCase
import fr.gouv.cnsp.monitorfish.domain.entities.fao_area.FaoArea
import fr.gouv.cnsp.monitorfish.domain.entities.fleet_segment.FleetSegment
import fr.gouv.cnsp.monitorfish.domain.entities.fleet_segment.ScipSpeciesType
import fr.gouv.cnsp.monitorfish.domain.repositories.FleetSegmentRepository
import fr.gouv.cnsp.monitorfish.domain.repositories.VesselRepository
import fr.gouv.cnsp.monitorfish.domain.use_cases.fleet_segment.dtos.SpeciesCatchForSegmentCalculation
import org.slf4j.LoggerFactory
import java.time.Clock
import java.time.ZonedDateTime

/**
* Return the computed fleet segments from the faoAreas, gears and species parameters.
* Return the computed fleet segments from the species catches.
*/
@UseCase
class ComputeFleetSegments(
private val fleetSegmentRepository: FleetSegmentRepository,
private val vesselRepository: VesselRepository,
private val clock: Clock,
) {
private val logger = LoggerFactory.getLogger(ComputeFleetSegments::class.java)

fun execute(
faoAreas: List<String>,
gearCodes: List<String>,
specyCodes: List<String>,
vesselId: Int,
speciesCatches: List<SpeciesCatchForSegmentCalculation>,
): List<FleetSegment> {
logger.info("Got ${speciesCatches.size} catches to assign fleet segments")

val currentYear = ZonedDateTime.now(clock).year
val vesselType = vesselRepository.findVesselById(vesselId)?.vesselType
val fleetSegments = fleetSegmentRepository.findAllByYear(currentYear)

val computedSegments =
fleetSegments.filter { fleetSegment ->
val isContainingGearFromList =
fleetSegment.gears.isEmpty() || fleetSegment.gears.any { gearCodes.contains(it) }
val isContainingSpecyFromList =
(fleetSegment.targetSpecies.isEmpty() && fleetSegment.bycatchSpecies.isEmpty()) ||
fleetSegment.targetSpecies.any { specyCodes.contains(it) } ||
fleetSegment.bycatchSpecies.any { specyCodes.contains(it) }
val isContainingFaoAreaFromList =
fleetSegment.faoAreas.isEmpty() ||
fleetSegment.faoAreas.any { faoArea ->
faoAreas.map { FaoArea(it) }.any { it.hasFaoCodeIncludedIn(faoArea) }
}

return@filter isContainingGearFromList && isContainingSpecyFromList && isContainingFaoAreaFromList
val controlledPelagicSpeciesWeight =
speciesCatches
.filter { speciesCatch ->
speciesCatch.scipSpeciesType == ScipSpeciesType.PELAGIC
}.sumOf { it.weight }
val controlledDemersalSpeciesWeight =
speciesCatches
.filter { speciesCatch ->
speciesCatch.scipSpeciesType == ScipSpeciesType.DEMERSAL
}.sumOf { it.weight }
val totalSumOfSpeciesWeight = speciesCatches.sumOf { it.weight }

val mainScipSpeciesType =
if (controlledPelagicSpeciesWeight > controlledDemersalSpeciesWeight) {
ScipSpeciesType.PELAGIC
} else {
ScipSpeciesType.DEMERSAL
}

val speciesToSegments =
speciesCatches.map { specyCatch ->
val computedSegment =
fleetSegments.filter { fleetSegment ->
/**
* minShareOfTargetSpecies
*/
val containsTargetSpecies =
speciesCatches.any { summedSpecyCatch ->
fleetSegment.targetSpecies.any { it == summedSpecyCatch.species }
}
val sumOfTargetSpeciesWeight =
speciesCatches
.filter { summedSpecyCatch ->
fleetSegment.targetSpecies.any { it == summedSpecyCatch.species }
}.sumOf { it.weight }
val shareOfTargetSpecies = sumOfTargetSpeciesWeight / totalSumOfSpeciesWeight

// This condition is used to add "by hand" a fleet segment to a PNO or a control,
// by adding a species with a 0.0 weight
val hasZeroWeightTargetSpecies =
containsTargetSpecies &&
sumOfTargetSpeciesWeight == 0.0 &&
fleetSegment.minShareOfTargetSpecies == 0.0

val hasMinShareOfTargetSpecies =
fleetSegment.minShareOfTargetSpecies == null ||
fleetSegment.targetSpecies.isEmpty() ||
shareOfTargetSpecies > fleetSegment.minShareOfTargetSpecies ||
hasZeroWeightTargetSpecies

/**
* gears
*/
val isContainingGearFromList =
fleetSegment.gears.isEmpty() || fleetSegment.gears.any { gear -> gear == specyCatch.gear }

/**
* faoAreas
*/
val isContainingFaoAreaFromList =
fleetSegment.faoAreas.isEmpty() ||
fleetSegment.faoAreas.any { faoArea ->
FaoArea(specyCatch.faoArea).hasFaoCodeIncludedIn(faoArea)
}

/**
* mesh
*/
val isMeshAboveMinMesh =
fleetSegment.minMesh == null || (specyCatch.mesh != null && specyCatch.mesh >= fleetSegment.minMesh)
val isMeshBelowMaxMesh =
fleetSegment.maxMesh == null || (specyCatch.mesh != null && specyCatch.mesh < fleetSegment.maxMesh)
val hasRightVesselType =
fleetSegment.vesselTypes.isEmpty() || fleetSegment.vesselTypes.any { it == vesselType }

val hasMainScipSpeciesType =
fleetSegment.mainScipSpeciesType == null ||
fleetSegment.mainScipSpeciesType == mainScipSpeciesType

return@filter isContainingGearFromList &&
isContainingFaoAreaFromList &&
isMeshAboveMinMesh &&
isMeshBelowMaxMesh &&
hasRightVesselType &&
hasMainScipSpeciesType &&
hasMinShareOfTargetSpecies
}.maxByOrNull { it.priority }

return@map Pair(specyCatch, computedSegment)
}

return computedSegments
return speciesToSegments.mapNotNull { it.second }.distinct()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package fr.gouv.cnsp.monitorfish.domain.use_cases.fleet_segment

import fr.gouv.cnsp.monitorfish.config.UseCase
import fr.gouv.cnsp.monitorfish.domain.entities.fleet_segment.FleetSegment
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.GearControl
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.SpeciesControl
import fr.gouv.cnsp.monitorfish.domain.repositories.SpeciesRepository
import org.slf4j.LoggerFactory

@UseCase
class ComputeFleetSegmentsFromControl(
private val computeFleetSegments: ComputeFleetSegments,
private val speciesRepository: SpeciesRepository,
) {
private val logger = LoggerFactory.getLogger(ComputeFleetSegmentsFromControl::class.java)

fun execute(
vesselId: Int,
faoAreas: List<String>,
gears: List<GearControl>,
species: List<SpeciesControl>,
): List<FleetSegment> {
val allSpecies = speciesRepository.findAll()

val speciesCatches = getSpeciesCatchesForSegmentCalculation(faoAreas, gears, species, allSpecies)

return computeFleetSegments.execute(vesselId, speciesCatches)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,40 @@ import fr.gouv.cnsp.monitorfish.domain.use_cases.dtos.CreateOrUpdateFleetSegment
class CreateFleetSegment(private val fleetSegmentRepository: FleetSegmentRepository) {
fun execute(fields: CreateOrUpdateFleetSegmentFields): FleetSegment {
require(fields.segment != null) {
"Segment must be provided"
"`segment` must be provided"
}

require(fields.year != null) {
"Year must be provided"
"`year` must be provided"
}

require(fields.priority != null) {
"`priority` must be provided"
}

require(fields.vesselTypes != null) {
"`vesselTypes` must be provided"
}

val newSegment =
fields.let {
FleetSegment(
segment = it.segment!!,
segmentName = it.segmentName ?: "",
dirm = listOf(),
gears = it.gears ?: listOf(),
faoAreas = it.faoAreas ?: listOf(),
targetSpecies = it.targetSpecies ?: listOf(),
bycatchSpecies = it.bycatchSpecies ?: listOf(),
impactRiskFactor = it.impactRiskFactor ?: 0.0,
year = it.year!!,
mainScipSpeciesType = fields.mainScipSpeciesType,
maxMesh = fields.maxMesh,
minMesh = fields.minMesh,
minShareOfTargetSpecies = fields.minShareOfTargetSpecies,
priority = fields.priority,
vesselTypes = fields.vesselTypes,
)
}

return fleetSegmentRepository.create(newSegment)
return fleetSegmentRepository.save(newSegment)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ class UpdateFleetSegment(private val fleetSegmentRepository: FleetSegmentReposit
): FleetSegment {
require(
fields.segment != null ||
fields.bycatchSpecies != null ||
fields.segmentName != null ||
fields.faoAreas != null ||
fields.gears != null ||
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package fr.gouv.cnsp.monitorfish.domain.use_cases.fleet_segment

import fr.gouv.cnsp.monitorfish.domain.entities.fao_area.FaoArea
import fr.gouv.cnsp.monitorfish.domain.entities.logbook.LogbookFishingCatch
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.GearControl
import fr.gouv.cnsp.monitorfish.domain.entities.mission.mission_actions.SpeciesControl
import fr.gouv.cnsp.monitorfish.domain.entities.species.Species
import fr.gouv.cnsp.monitorfish.domain.use_cases.fleet_segment.dtos.SpeciesCatchForSegmentCalculation

/**
* Filters the input sequence of FAO areas to keep only the smallest non overlapping areas.
Expand Down Expand Up @@ -49,3 +54,52 @@ fun FaoArea.hasFaoCodeIncludedIn(faoCode: String?): Boolean {

return this.faoCode.startsWith(faoCode)
}

fun getSpeciesCatchesForSegmentCalculation(
faoAreas: List<String>,
gears: List<GearControl>,
species: List<SpeciesControl>,
allSpecies: List<Species>,
): List<SpeciesCatchForSegmentCalculation> {
return faoAreas.flatMap { faoArea ->
gears.flatMap { gear ->
species.map { specy ->
val scipSpeciesType = allSpecies.find { it.code == specy.speciesCode }?.scipSpeciesType
val mesh = gear.controlledMesh ?: gear.declaredMesh
val weight = specy.controlledWeight ?: specy.declaredWeight ?: 0.0

SpeciesCatchForSegmentCalculation(
mesh = mesh,
weight = weight,
gear = gear.gearCode,
species = specy.speciesCode,
faoArea = faoArea,
scipSpeciesType = scipSpeciesType,
)
}
}
}
}

fun getSpeciesCatchesForSegmentCalculation(
gearCodes: List<String>,
catches: List<LogbookFishingCatch>,
allSpecies: List<Species>,
): List<SpeciesCatchForSegmentCalculation> {
return gearCodes.flatMap { gearCode ->
catches.map { specy ->
val scipSpeciesType = allSpecies.find { it.code == specy.species }?.scipSpeciesType
val weight = specy.weight ?: 0.0

SpeciesCatchForSegmentCalculation(
// TODO The mesh is not included in the manual PNO form
mesh = null,
weight = weight,
gear = gearCode,
species = specy.species,
faoArea = specy.faoZone!!, // A FAO area is always included
scipSpeciesType = scipSpeciesType,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package fr.gouv.cnsp.monitorfish.domain.use_cases.fleet_segment.dtos

import fr.gouv.cnsp.monitorfish.domain.entities.fleet_segment.ScipSpeciesType

data class SpeciesCatchForSegmentCalculation(
val mesh: Double?,
val weight: Double,
val gear: String?,
val species: String?,
val faoArea: String,
val scipSpeciesType: ScipSpeciesType?,
)
Loading

0 comments on commit 5b2cc6b

Please sign in to comment.