Newer
Older
* Parameter panel with Material-UI - input fields for simulation configuration
import { useState } from 'react';
import {
Box,
TextField,
Typography,
Divider,
Button,
IconButton,
Paper,
Alert,
Stack,
ToggleButtonGroup,
ToggleButton,
Select,
MenuItem,
type SelectChangeEvent,
} from '@mui/material';
import {
Add as AddIcon,
Delete as DeleteIcon,
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
} from '@mui/icons-material';
import { useSimulationStore } from '../../store/simulationStore';
import TimeConverter from '../tools/TimeConverter';
type SimulationTimeUnit = 'ms' | 's' | 'min' | 'h';
export default function ParameterPanel() {
const {
config,
updateConfig,
addServer,
removeServer,
updateServer,
isRunning,
} = useSimulationStore();
const [simTimeUnit, setSimTimeUnit] = useState<SimulationTimeUnit>('ms');
if (!config) {
return (
<Typography variant="body2" color="text.secondary" textAlign="center" py={3}>
Aucune configuration chargée
);
}
// Conversion helpers
const convertToRate = (meanTime: number): number => {
if (meanTime === 0) return 0;
const timeInMs = timeUnit === 's' ? meanTime * 1000 : meanTime;
return 1 / timeInMs;
};
const convertFromRate = (rate: number): number => {
if (rate === 0) return 0;
const timeInMs = 1 / rate;
return timeUnit === 's' ? timeInMs / 1000 : timeInMs;
};
const handleTimeUnitChange = (_: React.MouseEvent<HTMLElement>, newUnit: 'ms' | 's' | null) => {
if (newUnit !== null) {
setTimeUnit(newUnit);
}
};
const handleArrivalRateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const rateInput = parseFloat(e.target.value);
if (isNaN(rateInput)) {
updateConfig({ arrival_rate: 0 });
return;
}
// Convert to req/ms (internal unit)
const rateInMs = timeUnit === 's' ? rateInput / 1000 : rateInput;
updateConfig({ arrival_rate: rateInMs });
};
const getDisplayedArrivalRate = (): string => {
// Convert from req/ms to selected unit
const displayValue = timeUnit === 's' ? config.arrival_rate * 1000 : config.arrival_rate;
return displayValue.toFixed(timeUnit === 's' ? 1 : 3);
};
const handleCoordinatorServiceRateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const meanTime = parseFloat(e.target.value) || 0;
updateConfig({ coordinator_service_rate: convertToRate(meanTime) });
};
const handleExitProbabilityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
updateConfig({ coordinator_exit_probability: parseFloat(e.target.value) || 0 });
};
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
const handleSimulationTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const timeInput = parseFloat(e.target.value) || 0;
// Convert to ms (internal unit)
let timeInMs = timeInput;
if (simTimeUnit === 's') timeInMs = timeInput * 1000;
else if (simTimeUnit === 'min') timeInMs = timeInput * 60 * 1000;
else if (simTimeUnit === 'h') timeInMs = timeInput * 60 * 60 * 1000;
updateConfig({ simulation_time: timeInMs });
};
const handleWarmupTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const timeInput = parseFloat(e.target.value) || 0;
// Convert to ms (internal unit)
let timeInMs = timeInput;
if (simTimeUnit === 's') timeInMs = timeInput * 1000;
else if (simTimeUnit === 'min') timeInMs = timeInput * 60 * 1000;
else if (simTimeUnit === 'h') timeInMs = timeInput * 60 * 60 * 1000;
// Ensure warmup < simulation time
const simTime = config.simulation_time || 100000;
if (timeInMs >= simTime) {
// Cap at 50% of simulation time
timeInMs = Math.floor(simTime * 0.5);
}
updateConfig({ warmup_time: timeInMs });
};
const handleSimTimeUnitChange = (e: SelectChangeEvent<SimulationTimeUnit>) => {
setSimTimeUnit(e.target.value as SimulationTimeUnit);
};
const getDisplayedSimulationTime = (): number => {
const simTime = config.simulation_time || 100000;
if (simTimeUnit === 's') return simTime / 1000;
if (simTimeUnit === 'min') return simTime / (60 * 1000);
if (simTimeUnit === 'h') return simTime / (60 * 60 * 1000);
return simTime; // ms
};
const getDisplayedWarmupTime = (): number => {
const warmup = config.warmup_time || 0;
if (simTimeUnit === 's') return warmup / 1000;
if (simTimeUnit === 'min') return warmup / (60 * 1000);
if (simTimeUnit === 'h') return warmup / (60 * 60 * 1000);
return warmup; // ms
};
const getSimTimeStep = (): number => {
if (simTimeUnit === 'ms') return 10000;
if (simTimeUnit === 's') return 10;
if (simTimeUnit === 'min') return 1;
return 0.1; // h
};
const getExpectedRequests = (): number => {
const simTime = config.simulation_time || 100000;
return Math.round(config.arrival_rate * simTime);
};
const handleServerServiceRateChange = (serverId: string, value: string) => {
const meanTime = parseFloat(value) || 0;
updateServer(serverId, { service_rate: convertToRate(meanTime) });
};
const handleServerRoutingProbChange = (serverId: string, value: string) => {
updateServer(serverId, { routing_probability: parseFloat(value) || 0 });
};
// Calculate total routing probability
const totalRoutingProb =
config.coordinator_exit_probability +
config.servers.reduce((sum, s) => sum + s.routing_probability, 0);
const isProbabilityValid = Math.abs(totalRoutingProb - 1.0) < 0.001;
return (
<Box>
<Divider sx={{ my: 2 }} />
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="subtitle2" fontWeight={600}>
Paramètres du réseau
</Typography>
<ToggleButtonGroup
value={timeUnit}
exclusive
onChange={handleTimeUnitChange}
size="small"
disabled={isRunning}
>
<ToggleButton value="ms">ms</ToggleButton>
<ToggleButton value="s">s</ToggleButton>
</ToggleButtonGroup>
</Box>
<Stack spacing={2}>
{/* External arrival rate */}
label="Taux d'arrivée externe (λ)"
inputProps={{ step: timeUnit === 's' ? 0.1 : 0.001, min: 0 }}
value={getDisplayedArrivalRate()}
onChange={handleArrivalRateChange}
disabled={isRunning}
fullWidth
size="small"
helperText={`Requêtes par ${timeUnit === 'ms' ? 'milliseconde (req/ms)' : 'seconde (req/s)'}`}
InputProps={{
endAdornment: <Typography variant="caption" sx={{ ml: 1 }}>{timeUnit === 'ms' ? 'req/ms' : 'req/s'}</Typography>
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
{/* Simulation time */}
<Box sx={{ display: 'flex', gap: 1 }}>
<TextField
label="Temps de simulation"
type="number"
inputProps={{ step: getSimTimeStep(), min: 0 }}
value={getDisplayedSimulationTime().toFixed(simTimeUnit === 'h' ? 2 : simTimeUnit === 'min' ? 1 : 0)}
onChange={handleSimulationTimeChange}
disabled={isRunning}
fullWidth
size="small"
helperText={`~${getExpectedRequests().toLocaleString()} requêtes attendues`}
/>
<Select
value={simTimeUnit}
onChange={handleSimTimeUnitChange}
disabled={isRunning}
size="small"
sx={{ minWidth: 80 }}
>
<MenuItem value="ms">ms</MenuItem>
<MenuItem value="s">s</MenuItem>
<MenuItem value="min">min</MenuItem>
<MenuItem value="h">h</MenuItem>
</Select>
</Box>
{/* Warmup time */}
<TextField
label="Temps de préchauffage (warmup)"
type="number"
inputProps={{ step: getSimTimeStep(), min: 0, max: getDisplayedSimulationTime() * 0.9 }}
value={getDisplayedWarmupTime().toFixed(simTimeUnit === 'h' ? 2 : simTimeUnit === 'min' ? 1 : 0)}
onChange={handleWarmupTimeChange}
disabled={isRunning}
fullWidth
size="small"
error={config.warmup_time >= config.simulation_time}
helperText={
config.warmup_time >= config.simulation_time
? `Le warmup doit être < ${getDisplayedSimulationTime()} ${simTimeUnit}`
: `Période initiale ignorée pour calcul des statistiques (max: ${(getDisplayedSimulationTime() * 0.9).toFixed(0)} ${simTimeUnit})`
}
/>
{/* Warning alert if warmup >= simulation time */}
{config.warmup_time >= config.simulation_time && (
<Alert severity="error" sx={{ mt: 1 }}>
Le temps de préchauffage ({getDisplayedWarmupTime().toFixed(0)} {simTimeUnit}) ne peut pas être supérieur ou égal au temps de simulation ({getDisplayedSimulationTime().toFixed(0)} {simTimeUnit}).
</Alert>
)}
{/* Coordinator parameters */}
<Box>
<Typography variant="caption" fontWeight={600} color="text.secondary" sx={{ textTransform: 'uppercase' }}>
Coordinateur
</Typography>
<Stack spacing={1.5} sx={{ mt: 1 }}>
<TextField
label="Temps moyen de service (1/μc)"
inputProps={{ step: timeUnit === 's' ? 0.001 : 1, min: 0 }}
value={convertFromRate(config.coordinator_service_rate).toFixed(timeUnit === 's' ? 3 : 1)}
onChange={handleCoordinatorServiceRateChange}
disabled={isRunning}
fullWidth
size="small"
helperText={`En ${timeUnit === 'ms' ? 'millisecondes' : 'secondes'}`}
InputProps={{
endAdornment: <Typography variant="caption" sx={{ ml: 1 }}>{timeUnit}</Typography>
}}
/>
<TextField
label="Probabilité de sortie (p)"
type="number"
inputProps={{ step: 0.01, min: 0, max: 1 }}
value={config.coordinator_exit_probability}
onChange={handleExitProbabilityChange}
disabled={isRunning}
fullWidth
size="small"
/>
</Stack>
</Box>
{/* Servers */}
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="caption" fontWeight={600} color="text.secondary" sx={{ textTransform: 'uppercase' }}>
</Typography>
<Button
size="small"
startIcon={<AddIcon />}
onClick={addServer}
disabled={isRunning}
>
Ajouter
</Button>
</Box>
<Stack spacing={1.5}>
{config.servers.map((server, index) => (
<Paper key={server.id} variant="outlined" sx={{ p: 1.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="caption" fontWeight={600}>
Serveur {index + 1}
{config.servers.length > 1 && (
<IconButton
size="small"
onClick={() => removeServer(server.id)}
disabled={isRunning}
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
<Stack spacing={1}>
<TextField
label={`Temps moyen de service (1/μ${index + 1})`}
inputProps={{ step: timeUnit === 's' ? 0.001 : 1, min: 0 }}
value={convertFromRate(server.service_rate).toFixed(timeUnit === 's' ? 3 : 1)}
onChange={(e) => handleServerServiceRateChange(server.id, e.target.value)}
disabled={isRunning}
fullWidth
size="small"
helperText={`En ${timeUnit === 'ms' ? 'millisecondes' : 'secondes'}`}
InputProps={{
endAdornment: <Typography variant="caption" sx={{ ml: 1 }}>{timeUnit}</Typography>
}}
/>
<TextField
label={`Probabilité de routage (q${index + 1})`}
type="number"
inputProps={{ step: 0.01, min: 0, max: 1 }}
value={server.routing_probability}
onChange={(e) => handleServerRoutingProbChange(server.id, e.target.value)}
disabled={isRunning}
fullWidth
size="small"
/>
</Stack>
</Paper>
</Stack>
</Box>
{/* Probability validation */}
<Alert
severity={isProbabilityValid ? 'success' : 'error'}
icon={isProbabilityValid ? <CheckCircleIcon /> : <ErrorIcon />}
<Typography variant="caption" fontWeight={600}>
Conservation des probabilités
</Typography>
<Typography variant="caption" component="div">
p + Σq = {totalRoutingProb.toFixed(3)}
{isProbabilityValid ? ' = 1.0 ✓' : ' ≠ 1.0 ✗'}
</Typography>
</Alert>
{/* Time unit converter */}
<TimeConverter />
</Stack>
</Box>