diff --git a/net_maestro/core/admin/__init__.py b/net_maestro/core/admin/__init__.py index 65c5eb3..38138ba 100644 --- a/net_maestro/core/admin/__init__.py +++ b/net_maestro/core/admin/__init__.py @@ -4,6 +4,7 @@ from .event_record import EventRecordAdmin from .model_file import ModelFileAdmin from .model_record import ModelRecordAdmin +from .phold_simulation_config import PHOLDSimulationConfigAdmin from .phold_simulation_lp_record import PHOLDSimulationLpRecordAdmin from .run import RunAdmin from .simulation_file import SimulationFileAdmin @@ -16,6 +17,7 @@ "EventRecordAdmin", "ModelFileAdmin", "ModelRecordAdmin", + "PHOLDSimulationConfigAdmin", "PHOLDSimulationLpRecordAdmin", "RunAdmin", "SimulationFileAdmin", diff --git a/net_maestro/core/admin/phold_simulation_config.py b/net_maestro/core/admin/phold_simulation_config.py new file mode 100644 index 0000000..c5bea5f --- /dev/null +++ b/net_maestro/core/admin/phold_simulation_config.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from django.contrib import admin + +from net_maestro.core.models import PHOLDSimulationConfig + + +@admin.register(PHOLDSimulationConfig) +class PHOLDSimulationConfigAdmin(admin.ModelAdmin): + list_select_related = ["run"] + list_display = ["id", "run", "synch", "nlp", "remote", "mean", "lookahead"] + list_filter = ["synch", "stagger"] + search_fields = ["run__name"] diff --git a/net_maestro/core/constants.py b/net_maestro/core/constants.py index 11070cd..8b263ce 100644 --- a/net_maestro/core/constants.py +++ b/net_maestro/core/constants.py @@ -4,6 +4,7 @@ class RunStatus(models.TextChoices): + SAVED = "saved" PENDING = "pending" RUNNING = "running" COMPLETED = "completed" @@ -24,3 +25,12 @@ class ModelType(models.TextChoices): class TrafficType(models.TextChoices): UNIFORM = "uniform" + + +class SynchProtocol(models.IntegerChoices): + SEQUENTIAL = 1, "Sequential" + CONSERVATIVE = 2, "Conservative" + OPTIMISTIC = 3, "Optimistic" + OPTIMISTIC_DEBUG = 4, "Optimistic Debug" + OPTIMISTIC_REALTIME = 5, "Optimistic Realtime" + REVERSE_HANDLER_CHECK = 6, "Reverse Handler Check" diff --git a/net_maestro/core/forms.py b/net_maestro/core/forms.py index d5e0190..0d26132 100644 --- a/net_maestro/core/forms.py +++ b/net_maestro/core/forms.py @@ -2,14 +2,22 @@ from __future__ import annotations +from typing import Any + from django import forms -from django.core.validators import MaxValueValidator, MinValueValidator +from .models import PHOLDSimulationConfig + + +class PHOLDSimulationForm(forms.ModelForm): + """Form for PHOLD simulation parameters. -class PHOLDSimulationForm(forms.Form): - """Form for PHOLD simulation parameters.""" + A ModelForm bound to PHOLDSimulationConfig so field validators are not duplicated + between the model and a plain Form. + """ - # Simulation Model section + # run_identifier maps to Run.name, not a PHOLDSimulationConfig field, + # so it must be declared explicitly. run_identifier = forms.CharField( label="Run Identifier", max_length=200, @@ -17,81 +25,62 @@ class PHOLDSimulationForm(forms.Form): widget=forms.TextInput(attrs={"class": "input input-bordered w-full"}), ) - # Engine Parameters - synch = forms.ChoiceField( - label="Synchronization Protocol", - choices=[ - (1, "Sequential"), - (2, "Conservative"), - (3, "Optimistic"), - (4, "Optimistic Debug"), - (5, "Optimistic Realtime"), - (6, "Reverse Handler Check"), - ], - initial=3, - widget=forms.Select(attrs={"class": "select select-bordered w-full"}), - ) - - avl_size = forms.IntegerField( - label="AVL Tree Size", - initial=18, - validators=[MinValueValidator(10), MaxValueValidator(24)], - widget=forms.NumberInput(attrs={"class": "input input-bordered w-full"}), - ) - - # Model Parameters - nlp = forms.IntegerField( - label="LPs per Processor", - initial=8, - validators=[MinValueValidator(1)], - widget=forms.NumberInput(attrs={"class": "input input-bordered w-full"}), - ) - - remote = forms.FloatField( - label="Remote Event Rate", - initial=0.25, - validators=[MinValueValidator(0.0), MaxValueValidator(1.0)], - widget=forms.NumberInput(attrs={"class": "input input-bordered w-full", "step": "0.01"}), - ) - - mean = forms.FloatField( - label="Mean Timestamp", - initial=1.0, - validators=[MinValueValidator(0.1)], - widget=forms.NumberInput(attrs={"class": "input input-bordered w-full", "step": "0.1"}), - ) - - mult = forms.FloatField( - label="Memory Multiplier", - initial=1.4, - validators=[MinValueValidator(1.0)], - widget=forms.NumberInput(attrs={"class": "input input-bordered w-full", "step": "0.1"}), - ) - - lookahead = forms.FloatField( - label="Lookahead", - initial=1.0, - validators=[MinValueValidator(0.1)], - widget=forms.NumberInput(attrs={"class": "input input-bordered w-full", "step": "0.1"}), - ) - - start_events = forms.IntegerField( - label="Start Events per LP", - initial=1, - validators=[MinValueValidator(1)], - widget=forms.NumberInput(attrs={"class": "input input-bordered w-full"}), - ) - - memory = forms.IntegerField( - label="Additional Memory Buffers", - initial=100, - validators=[MinValueValidator(0)], - widget=forms.NumberInput(attrs={"class": "input input-bordered w-full"}), - ) - - stagger = forms.ChoiceField( + # Rendered as a Select (Yes/No) rather than the default checkbox widget. + stagger = forms.TypedChoiceField( label="Stagger Events", choices=[(0, "No"), (1, "Yes")], + coerce=lambda value: str(value) == "1", initial=0, widget=forms.Select(attrs={"class": "select select-bordered w-full"}), ) + + class Meta: + model = PHOLDSimulationConfig + fields = [ + "synch", + "avl_size", + "nlp", + "remote", + "mean", + "mult", + "lookahead", + "start_events", + "memory", + "stagger", + ] + labels = { + "synch": "Synchronization Protocol", + "avl_size": "AVL Tree Size", + "nlp": "LPs per Processor", + "remote": "Remote Event Rate", + "mean": "Mean Timestamp", + "mult": "Memory Multiplier", + "start_events": "Start Events per LP", + "memory": "Additional Memory Buffers", + } + widgets = { + "synch": forms.Select(attrs={"class": "select select-bordered w-full"}), + "avl_size": forms.NumberInput(attrs={"class": "input input-bordered w-full"}), + "nlp": forms.NumberInput(attrs={"class": "input input-bordered w-full"}), + "remote": forms.NumberInput( + attrs={"class": "input input-bordered w-full", "step": "0.01"} + ), + "mean": forms.NumberInput( + attrs={"class": "input input-bordered w-full", "step": "0.1"} + ), + "mult": forms.NumberInput( + attrs={"class": "input input-bordered w-full", "step": "0.1"} + ), + "lookahead": forms.NumberInput( + attrs={"class": "input input-bordered w-full", "step": "0.1"} + ), + "start_events": forms.NumberInput(attrs={"class": "input input-bordered w-full"}), + "memory": forms.NumberInput(attrs={"class": "input input-bordered w-full"}), + } + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + # model_to_dict() surfaces the model's boolean value for stagger, + # but the widget's choices are keyed by 0/1. + if "stagger" in self.initial: + self.initial["stagger"] = int(bool(self.initial["stagger"])) diff --git a/net_maestro/core/migrations/0010_alter_run_status_pholdsimulationconfig.py b/net_maestro/core/migrations/0010_alter_run_status_pholdsimulationconfig.py new file mode 100644 index 0000000..f3a4b95 --- /dev/null +++ b/net_maestro/core/migrations/0010_alter_run_status_pholdsimulationconfig.py @@ -0,0 +1,134 @@ +# Generated by Django 6.0.6 on 2026-07-07 14:07 +from __future__ import annotations + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0009_pholdsimulationlprecord"), + ] + + operations = [ + migrations.AlterField( + model_name="run", + name="status", + field=models.CharField( + choices=[ + ("saved", "Saved"), + ("pending", "Pending"), + ("running", "Running"), + ("completed", "Completed"), + ("failed", "Failed"), + ("cancelled", "Cancelled"), + ], + default="saved", + max_length=20, + ), + ), + migrations.CreateModel( + name="PHOLDSimulationConfig", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "synch", + models.IntegerField( + choices=[ + (1, "Sequential"), + (2, "Conservative"), + (3, "Optimistic"), + (4, "Optimistic Debug"), + (5, "Optimistic Realtime"), + (6, "Reverse Handler Check"), + ], + default=3, + verbose_name="Synchronization protocol", + ), + ), + ( + "avl_size", + models.IntegerField( + default=18, + validators=[ + django.core.validators.MinValueValidator(10), + django.core.validators.MaxValueValidator(24), + ], + verbose_name="AVL tree size", + ), + ), + ( + "nlp", + models.IntegerField( + default=8, + validators=[django.core.validators.MinValueValidator(1)], + verbose_name="LPs per processor", + ), + ), + ( + "remote", + models.FloatField( + default=0.25, + validators=[ + django.core.validators.MinValueValidator(0.0), + django.core.validators.MaxValueValidator(1.0), + ], + verbose_name="Remote event rate", + ), + ), + ( + "mean", + models.FloatField( + default=1.0, + validators=[django.core.validators.MinValueValidator(0.1)], + verbose_name="Mean timestamp", + ), + ), + ( + "mult", + models.FloatField( + default=1.4, + validators=[django.core.validators.MinValueValidator(1.0)], + verbose_name="Memory multiplier", + ), + ), + ( + "lookahead", + models.FloatField( + default=1.0, validators=[django.core.validators.MinValueValidator(0.1)] + ), + ), + ( + "start_events", + models.IntegerField( + default=1, + validators=[django.core.validators.MinValueValidator(1)], + verbose_name="Start events per LP", + ), + ), + ( + "memory", + models.IntegerField( + default=100, + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="Additional memory buffers", + ), + ), + ("stagger", models.BooleanField(default=False)), + ( + "run", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="phold_configs", + to="core.run", + ), + ), + ], + ), + ] diff --git a/net_maestro/core/models/__init__.py b/net_maestro/core/models/__init__.py index c58d1a7..4ad40b8 100644 --- a/net_maestro/core/models/__init__.py +++ b/net_maestro/core/models/__init__.py @@ -4,6 +4,7 @@ from .event_record import EventRecord from .model_file import ModelFile from .model_record import ModelRecord +from .phold_simulation_config import PHOLDSimulationConfig from .run import Run from .simulation_base_record import SimulationBaseRecord from .simulation_file import SimulationFile @@ -16,6 +17,7 @@ "EventRecord", "ModelFile", "ModelRecord", + "PHOLDSimulationConfig", "PHOLDSimulationLpRecord", "Run", "SimulationBaseRecord", diff --git a/net_maestro/core/models/phold_simulation_config.py b/net_maestro/core/models/phold_simulation_config.py new file mode 100644 index 0000000..c4af989 --- /dev/null +++ b/net_maestro/core/models/phold_simulation_config.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models + +from net_maestro.core.constants import SynchProtocol +from net_maestro.core.models.run import Run + + +class PHOLDSimulationConfig(models.Model): + """PHOLD simulation configuration parameters submitted for a Run. + + NOTE: This model currently handles the single PHOLD scenario. As we support more + simulation types, this should be generalized so we can store various parameter sets + without duplicating schema for each model. + """ + + run = models.ForeignKey(Run, on_delete=models.CASCADE, related_name="phold_configs") + + synch = models.IntegerField( + choices=SynchProtocol, + default=SynchProtocol.OPTIMISTIC, + verbose_name="Synchronization protocol", + ) + avl_size = models.IntegerField( + default=18, + validators=[MinValueValidator(10), MaxValueValidator(24)], + verbose_name="AVL tree size", + ) + nlp = models.IntegerField( + default=8, validators=[MinValueValidator(1)], verbose_name="LPs per processor" + ) + remote = models.FloatField( + default=0.25, + validators=[MinValueValidator(0.0), MaxValueValidator(1.0)], + verbose_name="Remote event rate", + ) + mean = models.FloatField( + default=1.0, validators=[MinValueValidator(0.1)], verbose_name="Mean timestamp" + ) + mult = models.FloatField( + default=1.4, validators=[MinValueValidator(1.0)], verbose_name="Memory multiplier" + ) + lookahead = models.FloatField(default=1.0, validators=[MinValueValidator(0.1)]) + start_events = models.IntegerField( + default=1, validators=[MinValueValidator(1)], verbose_name="Start events per LP" + ) + memory = models.IntegerField( + default=100, validators=[MinValueValidator(0)], verbose_name="Additional memory buffers" + ) + stagger = models.BooleanField(default=False) + + def __str__(self) -> str: + return f"PHOLD config for Run {self.run_id}" diff --git a/net_maestro/core/models/run.py b/net_maestro/core/models/run.py index df991af..861ec07 100644 --- a/net_maestro/core/models/run.py +++ b/net_maestro/core/models/run.py @@ -9,7 +9,9 @@ class Run(models.Model): created = models.DateTimeField(auto_now_add=True) name = models.CharField(max_length=200) description = models.TextField(blank=True, default="") - status = models.CharField(max_length=20, choices=RunStatus, default=RunStatus.PENDING) + # Defaults to SAVED because a Run only becomes PENDING once a simulation is actually + # queued to execute. Immediately queued simulations must pass an explicit status. + status = models.CharField(max_length=20, choices=RunStatus, default=RunStatus.SAVED) def __str__(self): return f"Run {self.id}: {self.name} ({self.status})" diff --git a/net_maestro/core/templates/net_maestro/index.html b/net_maestro/core/templates/net_maestro/index.html index 1fd4fe3..26e4a48 100644 --- a/net_maestro/core/templates/net_maestro/index.html +++ b/net_maestro/core/templates/net_maestro/index.html @@ -39,6 +39,15 @@ {% include 'net_maestro/shared/sidebar.html' %}