File size: 3,484 Bytes
9e62f55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import numpy as np
from src.config import cfg

class ShiftPatterns:
    """
    Modellazione del fenotipo dei turni. 
    Mappa le regole contrattuali e i vincoli normativi (es. pause VDT) in tensori 1D 
    che verranno successivamente proiettati sulla matrice di coverage.
    """
    def __init__(self):
        # Parametri normativi (es. Pausa ogni 2 ore per i video-terminalisti)
        self.vdt_interval_min = cfg.system_settings.get('vdt_interval_minutes', 120)
        self.vdt_break_min = cfg.system_settings.get('vdt_break_minutes', 15)
        
        # Quantizzazione: trasformazione dai minuti agli slot di sistema
        self.max_work_slots = int(self.vdt_interval_min / cfg.system_slot_minutes)
        self.vdt_slots = max(1, int(self.vdt_break_min / cfg.system_slot_minutes))

    def _create_block_inside_vdt(self, duration_minutes, flip_strategy=False):
        """
        Genera un macro-blocco lavorativo iniettando le micro-pause VDT obbligatorie.
        """
        total_slots = int(round(duration_minutes / cfg.system_slot_minutes))
        mask = np.ones(total_slots, dtype=int)
        
        # Early exit: se il turno è più corto dell'intervallo VDT, niente pause
        if total_slots <= self.max_work_slots:
            return mask
            
        # Sliding window per "scavare" gli slot di pausa all'interno della maschera booleana
        cursor = 0
        while cursor < total_slots:
            break_start_idx = cursor + self.max_work_slots
            if break_start_idx >= total_slots:
                break
            break_end_idx = min(break_start_idx + self.vdt_slots, total_slots)
            mask[break_start_idx : break_end_idx] = 0
            cursor = break_end_idx

        # Il flip garantisce varianza fenotipica a parità di orario (es. pausa all'inizio vs alla fine)
        return np.flip(mask) if flip_strategy else mask

    def get_mask_dynamic(self, work_hours, lunch_minutes, variant_seed=0):
        """
        Costruisce la firma oraria completa (fenotipo) del dipendente.
        Gestisce dinamicamente split-shift e variazioni deterministiche tramite seed.
        """
        total_contract_min = work_hours * 60
        lunch_slots = int(round(lunch_minutes / cfg.system_slot_minutes))
        
        # Decodifica bit a bit del seed per alterare la struttura delle pause
        # mantenendo un output deterministico per lo stesso ID dipendente
        flip_p1 = (variant_seed % 2) != 0
        flip_p2 = ((variant_seed >> 1) % 2) != 0
        
        # Vincolo normativo: i turni lunghi richiedono uno spezzato (es. mattina / pomeriggio)
        if work_hours > 6:
            first_part_min = 4 * 60  # Blocco standard pre-pranzo
            second_part_min = total_contract_min - first_part_min
            has_lunch = (lunch_slots > 0)
        else:
            first_part_min = total_contract_min
            second_part_min = 0
            has_lunch = False

        mask_part1 = self._create_block_inside_vdt(first_part_min, flip_strategy=flip_p1)
        mask_part2 = self._create_block_inside_vdt(second_part_min, flip_strategy=flip_p2) if second_part_min > 0 else np.array([], dtype=int)

        # Assemblaggio vettoriale del turno completo
        components = [mask_part1]
        if has_lunch:
            components.append(np.zeros(lunch_slots, dtype=int))
        if len(mask_part2) > 0:
            components.append(mask_part2)
            
        return np.concatenate(components)