-
Notifications
You must be signed in to change notification settings - Fork 13
initial cli files, core and solvents #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mdmix3-dev
Are you sure you want to change the base?
Conversation
| # Confirm creation | ||
| if not typer.confirm("Create project with these settings?"): | ||
| raise typer.Abort() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be an issue if we want to use the app in a pipeline.
| # Confirm creation | |
| if not typer.confirm("Create project with these settings?"): | |
| raise typer.Abort() |
|
|
||
| except Exception as e: | ||
| DisplayUtils.show_error(f"Project creation failed: {e}") | ||
| raise typer.Exit(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm not 100% sure about this. maybe at least we could do
| raise typer.Exit(1) | |
| raise typer.Exit(1) from e |
but i'd need to take a look
| # Show summary | ||
| self._show_project_summary(request) | ||
|
|
||
| # Confirm | ||
| if not typer.confirm("Create project?"): | ||
| raise typer.Abort() | ||
|
|
||
| # Create project | ||
| with console.status("[bold green]Creating project..."): | ||
| response = self.service.setup_project(request) | ||
|
|
||
| self._show_creation_results(response) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This already happened in the previous command.
It feels like it should be a function taking a ProjectRequest object as a parameter
| def _show_project_summary(self, request: ProjectRequest): | ||
| """Display project configuration summary""" | ||
|
|
||
| summary_table = DisplayUtils.create_table("Project Configuration", | ||
| ["Setting", "Value"]) | ||
|
|
||
| summary_table.add_row("Project Name", request.name) | ||
| summary_table.add_row("System", request.system.name) | ||
| summary_table.add_row("Structure", str(request.system.structure_file)) | ||
| summary_table.add_row("Total Replicas", str(request.replicas.count)) | ||
| summary_table.add_row("Solvents", ", ".join(set(request.replicas.solvents))) | ||
| summary_table.add_row("Simulation Time", f"{request.simulation.total_time} ns") | ||
| summary_table.add_row("Temperature", f"{request.simulation.temperature} K") | ||
| summary_table.add_row("Ensemble", request.simulation.ensemble.value) | ||
| summary_table.add_row("Restraints", request.simulation.restraint_mode.value) | ||
|
|
||
| console.print(summary_table) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a free function, probably, as it doesn't depend directly on the cli plugin
| def _show_creation_results(self, response): | ||
| """Display project creation results""" | ||
|
|
||
| if response.status == "success": | ||
| DisplayUtils.show_success(f"Project created successfully!") | ||
| else: | ||
| DisplayUtils.show_warning(f"Project created with issues") | ||
|
|
||
| # Summary | ||
| console.print(f"\nProject Directory: [bold]{response.project_dir}[/bold]") | ||
| console.print(f"Total Replicas: {response.total_replicas}") | ||
| console.print(f"Successful: [green]{response.successful_replicas}[/green]") | ||
|
|
||
| if response.successful_replicas < response.total_replicas: | ||
| failed = response.total_replicas - response.successful_replicas | ||
| console.print(f"Failed: [red]{failed}[/red]") | ||
|
|
||
| # Show any errors | ||
| if response.system.errors: | ||
| DisplayUtils.show_error("System setup errors:") | ||
| for error in response.system.errors: | ||
| console.print(f" - {error}") | ||
|
|
||
| # Show failed replicas | ||
| failed_replicas = [r for r in response.replicas if r.status == "failed"] | ||
| if failed_replicas: | ||
| DisplayUtils.show_error("Failed replicas:") | ||
| for replica in failed_replicas: | ||
| console.print(f" - {replica.name}: {replica.error}") | ||
|
|
||
| # Next steps | ||
| console.print("\n[bold]Next Steps:[/bold]") | ||
| console.print("1. Change to project directory:") | ||
| console.print(f" cd {response.project_dir}") | ||
| console.print("2. Run system preparation:") | ||
| console.print(" ./scripts/setup_project.sh") | ||
| console.print("3. Submit simulations to queue or run locally") No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing.
if something, both functions would belong with the 'back-end' side
the cli should probably be just a dumb front-end.
if we're following hexagonal architecture / ddd, this would probably be at the service level
| name: str = Field(..., description="System name") | ||
| structure_file: Path = Field(..., description="Path to OFF or PDB file") | ||
| structure_type: str = Field("off", description="Structure file type (off/pdb)") | ||
| force_fields: List[str] = Field( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since some time ago, we don't need to import typing.List anymore,
we can go with just list (same thing with many other collections and types)
| force_fields: List[str] = Field( | |
| force_fields: list[str] = Field( |
| """Request model for system setup""" | ||
| name: str = Field(..., description="System name") | ||
| structure_file: Path = Field(..., description="Path to OFF or PDB file") | ||
| structure_type: str = Field("off", description="Structure file type (off/pdb)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could do with a str enum, but it might be overkill
another way would be to use the union of Literals
| structure_type: str = Field("off", description="Structure file type (off/pdb)") | |
| structure_type: Literal["off", "pdb"] = Field("off", description="Structure file type (off/pdb)") |
| @field_validator('structure_file') | ||
| @classmethod | ||
| def validate_structure_file(cls, v): | ||
| if not v.exists(): | ||
| raise ValueError(f"Structure file not found: {v}") | ||
| return v |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this validator is going to be a bit of a pain if we want to create the templates from the pydantic models, let's either comment it out, or use it not as a field_validator but as some sort of model function later.
| force_fields=force_fields | ||
| ), | ||
| replicas=ReplicaRequest( | ||
| count=len(solvents) * replicas, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| count=len(solvents) * replicas, | |
| count=replicas, |
| @field_validator('solvents') | ||
| @classmethod | ||
| def validate_solvents(cls, v, info): # Change parameter name | ||
| # Access other field values through info.data | ||
| if info.data and 'count' in info.data: | ||
| count = info.data['count'] | ||
| if len(v) != count: | ||
| # If single solvent provided, replicate for all replicas | ||
| if len(v) == 1: | ||
| return v * count | ||
| raise ValueError(f"Number of solvents ({len(v)}) must match replica count ({count})") | ||
| return v |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is right (?)
we could have just a single solvent with 3 replicas, and the validator would fail that (?)
| class ReplicaRequest(BaseModel): | ||
| """Request model for replica configuration""" | ||
| count: int = Field(3, ge=1, description="Number of replicas") | ||
| solvents: List[str] = Field(..., description="List of solvents for replicas") | ||
| naming_scheme: str = Field("{solvent}_{index}", description="Replica naming pattern") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it could be interesting to have something like this:
| class ReplicaRequest(BaseModel): | |
| """Request model for replica configuration""" | |
| count: int = Field(3, ge=1, description="Number of replicas") | |
| solvents: List[str] = Field(..., description="List of solvents for replicas") | |
| naming_scheme: str = Field("{solvent}_{index}", description="Replica naming pattern") | |
| class ReplicaRequest(BaseModel): | |
| """Request model for replica configuration""" | |
| solvents: dict[str, int] = Field(default_factory=lambda: {"WAT": 3}, description="dictionary of solvent name to number of replicas") |
maybe the default should be just an empty dict, but this is okay
| class ProjectRequest(BaseModel): | ||
| """Complete project request model""" | ||
| name: str = Field(..., description="Project name") | ||
| description: str = Field("", description="Project description") | ||
| system: SystemRequest | ||
| replicas: ReplicaRequest | ||
| simulation: SimulationRequest | ||
| output_dir: Optional[Path] = Field(None, description="Output directory") | ||
| queue_system: QueueSystem = Field(QueueSystem.LOCAL, description="Queue system") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it will be better for us if we make the structure of project request to match the yaml config, as it will make it trivial to both parse the config and generate the template
| } | ||
|
|
||
|
|
||
| class SolventInfo(BaseModel): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just for consistency
| class SolventInfo(BaseModel): | |
| class SolventInfoResponse(BaseModel): |
| """Save configuration to YAML file""" | ||
| import yaml | ||
| with open(path, 'w') as f: | ||
| yaml.dump(self.dict(), f, default_flow_style=False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think the current way of doing that would be
| yaml.dump(self.dict(), f, default_flow_style=False) | |
| yaml.dump(self.model_dump(), f, default_flow_style=False) |
Also, it is possible that we want to use pydantic-settings BaseSettings
https://docs.pydantic.dev/latest/concepts/pydantic_settings/
| @classmethod | ||
| def from_yaml(cls, path: Path): | ||
| """Load configuration from YAML file""" | ||
| import yaml | ||
| with open(path, 'r') as f: | ||
| data = yaml.safe_load(f) | ||
| return cls(**data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be done directly with the pydantic
No description provided.