Skip to content

Commit

Permalink
very broken WIP of #52, only test_multi and utils is currently running
Browse files Browse the repository at this point in the history
  • Loading branch information
ModischFabrications committed Apr 7, 2024
1 parent a73bf1e commit 721e8c7
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 109 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ You might need to replace `#!/bin/sh` with `#!/usr/bin/env sh` in the resulting
All obvious errors should be checked and or fixed by pre-commit, execute `pre-commit run --all-files --hook-stage push`
to run manually.

use `git push --no-verify` if you really need to skip these tests, but you better have a good explanation.

Change version number in main.py:version for newer releases, git tags will be created automatically.

### Testing
Expand Down
6 changes: 3 additions & 3 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from contextlib import asynccontextmanager

from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from fastapi.middleware.cors import CORSMiddleware
from starlette.requests import Request
from starlette.responses import HTMLResponse, PlainTextResponse

Expand Down Expand Up @@ -55,8 +55,8 @@ async def catch_exceptions_middleware(request: Request, call_next):
)


# response model ensures correct documentation
@app.post("/solve", response_model=Result)
# response model ensures correct documentation, exclude skips optional
@app.post("/solve", response_model=Result, response_model_exclude_defaults=True)
def post_solve(job: Job):
# pydantic guarantees type safety, no need to check manually
solved: Result = solve(job)
Expand Down
75 changes: 53 additions & 22 deletions app/solver/data/Job.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,73 +5,104 @@
from pydantic import BaseModel, ConfigDict, PositiveInt, NonNegativeInt, model_validator


class TargetSize(BaseModel):
class NamedSize(BaseModel):
# 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] = ""
name: Optional[str] = None

def __lt__(self, other):
"""
compares lengths
"""
return self.length < other.length

def __hash__(self) -> int:
return hash((self.length, self.name))

def __str__(self):
return f"{self.name}: l={self.length}"


class TargetStock(NamedSize):
pass


class TargetSize(NamedSize):
quantity: PositiveInt

def __str__(self):
return f"l:{self.length}, n:{self.quantity}"
return f"{self.name}: l={self.length}, n={self.quantity}"

def as_base(self) -> NamedSize:
return NamedSize(length=self.length, name=self.name)


class StockSize(TargetStock):
quantity: PositiveInt = 999 # more or less equal to infinite

def iterate_sizes(self) -> Iterator[TargetStock]:
"""
yields all lengths times amount, sorted descending
"""

for _ in range(self.quantity):
yield TargetStock(length=self.length, name=self.name)

def as_base(self) -> TargetStock:
return TargetStock(length=self.length, name=self.name)


class Job(BaseModel):
model_config = ConfigDict(frozen=True, validate_assignment=True)

max_length: PositiveInt
cut_width: NonNegativeInt = 0
target_sizes: tuple[TargetSize, ...]
stocks: tuple[StockSize, ...]
required: tuple[TargetSize, ...]

def iterate_sizes(self) -> Iterator[tuple[int, str | None]]:
def iterate_sizes(self) -> Iterator[NamedSize]:
"""
yields all lengths, sorted descending
yields all lengths times amount, 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 target in sorted(self.required, key=lambda x: x.length, reverse=True):
for _ in range(target.quantity):
yield target.length, target.name
yield target.as_base()

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

def n_combinations(self) -> float | int:
"""
Number of possible combinations of target sizes; returns infinite if too large
"""
if self.n_targets() > 100:
return math.inf
return int(factorial(self.n_targets()) / prod([factorial(n.quantity) for n in self.target_sizes]))
return int(factorial(self.n_targets()) / prod([factorial(n.quantity) for n in self.required]))

@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 for target in self.target_sizes):
# basic assertion are done at field level
if len(self.stocks) <= 0:
raise ValueError(f"Job is missing stocks")
if len(self.required) <= 0:
raise ValueError("Job is missing required")

if any(all(target.length > stock.length for stock in self.stocks) for target in self.required):
raise ValueError("Job has target sizes longer than the stock")
return self

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

def __hash__(self) -> int:
return hash((self.max_length, self.cut_width, str(sorted(self.target_sizes))))
return hash((str(sorted(self.stocks)), self.cut_width, str(sorted(self.required))))
36 changes: 24 additions & 12 deletions app/solver/data/Result.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from enum import unique, Enum
from typing import Optional, TypeAlias
from typing import Optional

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

from app.solver.data.Job import Job
from app.solver.data.Job import Job, NamedSize, TargetStock


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


ResultLength: TypeAlias = tuple[tuple[PositiveInt, str | None], ...]
ResultLengths: TypeAlias = tuple[ResultLength, ...]
class ResultEntry(BaseModel):
model_config = ConfigDict(frozen=True, validate_assignment=True)

stock: TargetStock
cuts: tuple[NamedSize, ...]
trimming: NonNegativeInt

def __lt__(self, other):
# this could also sort by trimmings, not sure if that is better
return len(self.cuts)


class Result(BaseModel):
Expand All @@ -23,30 +31,34 @@ class Result(BaseModel):
job: Job
solver_type: SolverType
time_us: Optional[PositiveInt] = None
lengths: ResultLengths
# keep most cuts at the top, getting simpler towards the end
layout: tuple[ResultEntry, ...]

# no trimmings as they can be inferred from difference to job
def trimmings(self):
"""
trimmings are summed from entries on demand
"""
return sum([t.trimming for t in self.layout])

# these could sort but there is no need with pre-sorted solvers
def __eq__(self, other):
return (
self.job == other.job
and self.solver_type == other.solver_type
and self.lengths == other.lengths
and self.layout == other.layout
)

def exactly(self, other):
return (
self.job == other.job
and self.solver_type == other.solver_type
and self.time_us == other.time_us
and self.lengths == other.lengths
and self.layout == other.layout
)

@model_validator(mode='after')
def assert_valid(self):
if self.solver_type not in SolverType:
raise ValueError(f"Result has invalid solver_type {self.solver_type}")
if len(self.lengths) <= 0:
# basic assertion are done at field level
if len(self.layout) <= 0:
raise ValueError("Result is missing lengths")
return self
Loading

0 comments on commit 721e8c7

Please sign in to comment.