Add machine-aware maximum product length

This commit is contained in:
2026-09-26 12:25:04 +02:00
parent 3e13d24405
commit 90c28fb2f5
11 changed files with 765 additions and 5 deletions
+164
View File
@@ -4,8 +4,14 @@ from pathlib import Path
from machine_constraints import (
MACHINE_CONFIG_FILE,
MAX_LENGTH_TIE_ABS_TOLERANCE_M,
Machine,
MachineConfigurationError,
MachineConstraints,
MachineRepository,
NumericRange,
_governing_maximum,
calculate_machine_max_product_length,
check_production_feasibility,
get_machine,
)
@@ -24,6 +30,38 @@ def request(**changes):
return payload
def machine_repository(
*,
diameter_maximum=1100.0,
diameter_minimum=200.0,
weight_maximum=700.0,
width_limits=(4.0, 6.1),
allowed_cores=(133.0,),
):
diameter = (
NumericRange(minimum=diameter_minimum, maximum=diameter_maximum)
if diameter_maximum is not None else None
)
weight = NumericRange(maximum=weight_maximum) if weight_maximum is not None else None
width = (
NumericRange(minimum=width_limits[0], maximum=width_limits[1])
if width_limits is not None else None
)
return MachineRepository((Machine(
id="test_machine",
name="Test Machine",
aliases=(),
location="Test",
constraints=MachineConstraints(
roll_weight_kg=weight,
roll_diameter_mm=diameter,
allowed_core_diameters_mm=allowed_cores,
product_width_m=width,
line_speed_m_per_min=None,
),
),))
class MachineRepositoryTests(unittest.TestCase):
def setUp(self):
self.repository = MachineRepository.load()
@@ -163,3 +201,129 @@ class MachineFeasibilityTests(unittest.TestCase):
self.assertEqual(check_production_feasibility({"machine": "B1", "unknown": 1})["status"], "invalid_parameter")
self.assertEqual(check_production_feasibility({"machine": "B1", "core_weight_kg": 10})["status"], "invalid_parameter")
self.assertEqual(self.check(machine="not a machine")["status"], "machine_not_found")
class MachineMaximumProductLengthTests(unittest.TestCase):
def calculate(self, repository=None, **changes):
values = {
"machine": "test_machine",
"core_diameter_mm": 133,
"thickness_mm": 4.311611,
"thickness_stddev_mm": 0.249999,
"product_width_m": 6,
"area_weight_g_m2": 495.395833,
"repository": repository or machine_repository(),
}
values.update(changes)
return calculate_machine_max_product_length(**values)
def test_characterizes_article_218500_on_k7(self):
result = calculate_machine_max_product_length(
machine="K7",
core_diameter_mm=133,
thickness_mm=4.311611,
thickness_stddev_mm=0.249999,
product_width_m=6,
area_weight_g_m2=495.395833,
)
self.assertEqual(result["status"], "success")
self.assertAlmostEqual(result["limits"]["nominal_diameter_length_m"], 217.1900177448)
self.assertAlmostEqual(result["limits"]["conservative_diameter_length_m"], 194.6207328149)
self.assertAlmostEqual(result["limits"]["material_weight_length_m"], 235.5019136115)
self.assertEqual(result["production_maximums"]["nominal_governing_constraints"], ["roll_diameter_nominal"])
self.assertEqual(result["production_maximums"]["conservative_governing_constraints"], ["roll_diameter_conservative"])
def test_stddev_controls_nominal_and_conservative_diameter_limits(self):
zero = self.calculate(thickness_stddev_mm=0)
varied = self.calculate()
self.assertEqual(
zero["limits"]["nominal_diameter_length_m"],
zero["limits"]["conservative_diameter_length_m"],
)
self.assertLess(
varied["limits"]["conservative_diameter_length_m"],
varied["limits"]["nominal_diameter_length_m"],
)
invalid = self.calculate(thickness_stddev_mm=3)
self.assertEqual(invalid["status"], "invalid_parameter")
def test_weight_can_govern_one_or_both_production_maximums(self):
one = self.calculate(repository=machine_repository(weight_maximum=600))
both = self.calculate(repository=machine_repository(weight_maximum=500))
self.assertEqual(one["production_maximums"]["nominal_governing_constraints"], ["material_weight"])
self.assertEqual(one["production_maximums"]["conservative_governing_constraints"], ["roll_diameter_conservative"])
self.assertEqual(both["production_maximums"]["nominal_governing_constraints"], ["material_weight"])
self.assertEqual(both["production_maximums"]["conservative_governing_constraints"], ["material_weight"])
def test_ties_use_documented_tolerance_and_canonical_order(self):
value, constraints = _governing_maximum((
("roll_diameter_nominal", 100.0),
("material_weight", 100.0 + MAX_LENGTH_TIE_ABS_TOLERANCE_M / 2),
))
self.assertEqual(value, 100.0)
self.assertEqual(constraints, ["roll_diameter_nominal", "material_weight"])
_, constraints = _governing_maximum((
("roll_diameter_nominal", 100.0),
("material_weight", 100.0 + MAX_LENGTH_TIE_ABS_TOLERANCE_M * 2),
))
self.assertEqual(constraints, ["roll_diameter_nominal"])
def test_core_and_width_incompatibilities_never_claim_a_length(self):
core = self.calculate(core_diameter_mm=130)
width = self.calculate(product_width_m=3.9)
self.assertEqual(core["status"], "incompatible")
self.assertEqual(core["production_maximums"], None)
self.assertEqual(core["failed_compatibility_constraints"], ["allowed_core_diameters_mm"])
self.assertEqual(width["status"], "incompatible")
self.assertEqual(width["production_maximums"], None)
self.assertEqual(width["failed_compatibility_constraints"], ["product_width_m"])
def test_width_boundaries_and_missing_inputs_are_structured(self):
repository = machine_repository(width_limits=(4, 6))
self.assertEqual(self.calculate(repository=repository, product_width_m=4)["status"], "success")
self.assertEqual(self.calculate(repository=repository, product_width_m=6)["status"], "success")
missing = self.calculate(product_width_m=None, area_weight_g_m2=None)
self.assertEqual(missing["status"], "needs_clarification")
self.assertEqual(missing["missing_required_inputs"], ["product_width_m", "area_weight_g_m2"])
self.assertIsNotNone(missing["partial_limits"])
self.assertIsNone(missing["production_maximums"])
def test_exact_width_machine_is_checked_before_length_calculation(self):
values = {
"machine": "E1",
"core_diameter_mm": 150,
"thickness_mm": 4.311611,
"product_width_m": 7.5,
"area_weight_g_m2": 495.395833,
}
self.assertEqual(calculate_machine_max_product_length(**values)["status"], "success")
values["product_width_m"] = 7.5001
result = calculate_machine_max_product_length(**values)
self.assertEqual(result["status"], "incompatible")
self.assertIsNone(result["production_maximums"])
def test_unconfigured_limiters_and_minimum_diameter_verification(self):
no_weight = self.calculate(repository=machine_repository(weight_maximum=None))
self.assertEqual(no_weight["limits"]["material_weight_maximum_state"], "not_configured")
no_diameter = self.calculate(repository=machine_repository(diameter_maximum=None))
self.assertEqual(no_diameter["limits"]["roll_diameter_maximum_state"], "not_configured")
self.assertEqual(
no_diameter["production_maximums"]["nominal_governing_constraints"],
["material_weight"],
)
no_limits = self.calculate(repository=machine_repository(diameter_maximum=None, weight_maximum=None, width_limits=None))
self.assertEqual(no_limits["status"], "no_maximum_length_constraint")
no_limits_without_article_size = self.calculate(
repository=machine_repository(
diameter_maximum=None, weight_maximum=None, width_limits=None
),
product_width_m=None,
area_weight_g_m2=None,
)
self.assertEqual(
no_limits_without_article_size["status"], "no_maximum_length_constraint"
)
low_weight = self.calculate(repository=machine_repository(weight_maximum=10, diameter_minimum=800))
self.assertEqual(low_weight["status"], "incompatible")
self.assertIsNone(low_weight["production_maximums"])