Skip to content

Commit

Permalink
massive rework & cleanup; implement pydantic validation and extend ch…
Browse files Browse the repository at this point in the history
…ecks, simplify tests with matrixes, remove cumbersome test fixtures, use simple typing, create and update SolverSettings, rework n_max checks to properly respect calculation times, major performance upgrade to bruteforce solver (closes #66), fix and include gapfill solver, prepare structure for #68 metrics
  • Loading branch information
ModischFabrications committed Mar 30, 2024
1 parent 2697094 commit 156cb9f
Show file tree
Hide file tree
Showing 18 changed files with 312 additions and 332 deletions.
22 changes: 18 additions & 4 deletions app/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
version = "v1.0.0"
from pydantic import BaseModel

# solver parameter
n_max_precise = 9 # 10 takes 30s on a beefy desktop, 9 only 1.2s
n_max = 500 # around 1 million with n^2
# used for git tags
version = "v1.0.1"


class SolverSettings(BaseModel):
bruteforce_max_combinations: int
n_max: int


# TODO should be startup parameter
solverSettings = SolverSettings(
# Desktop with Ryzen 2700X:
# (4, 3, 2)=1260 => 0.1s, (4, 3, 3)=4200 => 0.8s, (5, 3, 3)=9240 => 8s
bruteforce_max_combinations=5000,
# that is already unusable x100, but the solver takes it easily
n_max=2000
)
15 changes: 4 additions & 11 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
from starlette.requests import Request
from starlette.responses import HTMLResponse, PlainTextResponse

from app.constants import version, n_max_precise, n_max

from app.constants import version, solverSettings
# don't mark /app as a sources root or pycharm will delete the "app." prefix
# that's needed for pytest to work correctly
from app.solver.data.Job import Job
Expand Down Expand Up @@ -84,15 +83,9 @@ def get_debug():


@app.get("/constants", response_class=HTMLResponse)
def get_debug():
static_answer = (
"Constants:"
"<ul>"
f"<li>Max Entries for perfect results: {n_max_precise}</li>"
f"<li>Max Entries for any result: {n_max}</li>"
)

return static_answer
@app.get("/settings", response_class=HTMLResponse)
def get_settings():
return solverSettings


# content_type results in browser pretty printing
Expand Down
74 changes: 31 additions & 43 deletions app/solver/data/Job.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from typing import Iterator, List, Optional, Tuple
from math import factorial, prod
from typing import Iterator, Optional

from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict, PositiveInt, NonNegativeInt, model_validator


class TargetSize(BaseModel):
length: int
quantity: int
# frozen might be nice, but that would make reuse in solvers worse
model_config = ConfigDict(validate_assignment=True)

length: PositiveInt
quantity: PositiveInt
name: Optional[str] = ""

def __lt__(self, other):
Expand All @@ -19,67 +23,51 @@ def __str__(self):


class Job(BaseModel):
max_length: int
cut_width: int = 0
target_sizes: List[TargetSize]
model_config = ConfigDict(frozen=True, validate_assignment=True)

# utility
max_length: PositiveInt
cut_width: NonNegativeInt = 0
target_sizes: tuple[TargetSize, ...]

def iterate_sizes(self) -> Iterator[Tuple[int, str | None]]:
def iterate_sizes(self) -> Iterator[tuple[int, str | None]]:
"""
yields all lengths, sorted descending
"""

# sort descending to favor combining larger sizes first
for target in sorted(self.target_sizes, key=lambda x: x.length, reverse=True):
for _ in range(target.quantity):
yield (target.length, target.name)

# NOTE: Not used, so not really refactored at the moment
def sizes_from_list(self, sizes_list: List[TargetSize]):
# known_sizes = {}
#
# # list to dict to make them unique
# for size in sizes_list:
# if size.length in known_sizes:
# known_sizes[size.length] += size.quantity
# else:
# known_sizes[size.length] = size.quantity

self.target_sizes = sizes_list

# NOTE: Can eventually be removed as it does nothing anymore
def sizes_as_list(self) -> List[TargetSize]:
yield target.length, target.name

def n_targets(self) -> int:
"""
Number of possible combinations of target sizes
"""
return sum([target.quantity for target in self.target_sizes])

def n_combinations(self) -> int:
"""
Compatibility function
Number of possible combinations of target sizes
"""
# back to list again for compatibility
return self.target_sizes
return int(factorial(self.n_targets()) / prod([factorial(n.quantity) for n in self.target_sizes]))

def assert_valid(self):
@model_validator(mode='after')
def assert_valid(self) -> 'Job':
if self.max_length <= 0:
raise ValueError(f"Job has invalid max_length {self.max_length}")
if self.cut_width < 0:
raise ValueError(f"Job has invalid cut_width {self.cut_width}")
if len(self.target_sizes) <= 0:
raise ValueError("Job is missing target_sizes")
if any(
target.length > (self.max_length - self.cut_width)
for target in self.target_sizes
):
if any(target.length > self.max_length for target in self.target_sizes):
raise ValueError("Job has target sizes longer than the stock")

def __len__(self) -> int:
"""
Number of target sizes in job
"""
return sum([target.quantity for target in self.target_sizes])
return self

def __eq__(self, other):
return (
self.max_length == other.max_length
and self.cut_width == other.cut_width
and self.target_sizes == other.target_sizes
self.max_length == other.max_length
and self.cut_width == other.cut_width
and self.target_sizes == other.target_sizes
)

def __hash__(self) -> int:
Expand Down
17 changes: 10 additions & 7 deletions app/solver/data/Result.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from enum import unique, Enum
from typing import List, Tuple, Optional
from typing import Optional, TypeAlias

from pydantic import BaseModel
from pydantic import BaseModel, PositiveInt, model_validator

from app.solver.data.Job import Job

Expand All @@ -13,12 +13,15 @@ class SolverType(str, Enum): # str as base enables Pydantic-Schemas
FFD = "FFD"


ResultLength: TypeAlias = tuple[tuple[PositiveInt, str | None], ...]
ResultLengths: TypeAlias = tuple[ResultLength, ...]


class Result(BaseModel):
# allow IDs to skip redundant transmission for future versions
job: Job
solver_type: SolverType
time_us: Optional[int] = -1
lengths: List[List[Tuple[int, str]]]
time_us: Optional[int] = None
lengths: ResultLengths

# no trimmings as they can be inferred from difference to job

Expand All @@ -38,11 +41,11 @@ def exactly(self, other):
and self.lengths == other.lengths
)

@model_validator(mode='after')
def assert_valid(self):
self.job.assert_valid()
if self.solver_type not in SolverType:
raise ValueError(f"Result has invalid solver_type {self.solver_type}")
if self.time_us < 0:
raise ValueError(f"Result has invalid time_us {self.time_us}")
if len(self.lengths) <= 0:
raise ValueError("Result is missing lengths")
return self
Loading

0 comments on commit 156cb9f

Please sign in to comment.