ParameterPanel.tsx 13,5 ko
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,
    timeUnit,
    setTimeUnit,
  const [simTimeUnit, setSimTimeUnit] = useState<SimulationTimeUnit>('ms');
      <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 });
  };

  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>
        {/* 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)'}`}
            endAdornment: <Typography variant="caption" sx={{ ml: 1 }}>{timeUnit === 'ms' ? 'req/ms' : 'req/s'}</Typography>
        {/* 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' }}>
          </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>
        <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}
            >
            {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}>
                  {config.servers.length > 1 && (
                      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>
        <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 />