-
Notifications
You must be signed in to change notification settings - Fork 18
Description
Preliminary Checks
- I have read the project documentation.
- I have searched the issue tracker for similar questions.
What Have You Tried?
Proposal: Universal Check Interface
I opened this issue to propose a minimal universal interface for structural checks in Blueprints.
My goal is to gather feedback from the community and reach consensus on the best approach before implementing it.
Sooooo @Blueprints-org/maintainers , let me know what you think!
Problem
At Blueprints we want to support structural checks that engineers can use for a broad variety of cases.
Think of checks like, shear capacity of a concrete beam, bending capacity of a steel section, or bearing capacity of a soil foundation.
Those are just examples, but the goal is to support any engineering check for any material (concrete, steel, soil, etc.) and any complexity level (from simple single checks to complex hierarchical checks composed of sub-checks).
So how are we going to achieve that in a clean, maintainable way and offer our users a consistent experience?
The short answer is you can't. Checks vary widely in their logic, inputs, outputs, and complexity.
However, we can define a minimal universal interface that all checks must implement. This interface will provide a consistent way to interact with any check, regardless of its internal workings.
In that way, users can rely on a common set of methods and properties to work with checks, while the specific implementation details can vary as needed.
Solution: Minimal Universal Interface
class StructuralCheckProtocol(Protocol):
"""Minimal interface for ANY engineering check."""
name: str # Check name (instance parameter)
source_docs: list[str] # Source references
def execute(self, **kwargs) -> Any: # Internal calculations
"""Flexible return type for check-specific logic."""
def check(self) -> CheckResult: # Public API
"""Execute check. Always returns CheckResult."""
def calculation_steps(self) -> dict[str, "StructuralCheckProtocol"]:
"""Return sub-check instances (not results)."""That's the complete interface. No more, no less.
Design Principles
-
Single Responsibility: Each element has one reason to exist
name: Identitysource_docs: Traceabilityexecute(): Internal logiccheck(): Verificationcalculation_steps(): Composition
-
Open/Closed: Add new checks without modifying existing code
-
Liskov Substitution: Any check works anywhere a check is expected
-
Interface Segregation: Only what's needed, nothing extra
-
Dependency Inversion: Depend on
StructuralCheckProtocol, not implementations
Simple Check Pattern
@dataclass(frozen=True)
class SimpleShearCheck:
"""Minimal implementation."""
name: str
source_docs: list[str]
v_ed: float
v_rd: float
def execute(self) -> float:
return self.v_rd
def check(self) -> CheckResult:
return CheckResult.from_comparison(self.v_ed, self.v_rd)
def calculation_steps(self) -> dict:
return {} # No sub-checks
# ... and any other relevant methods that are specific to this check
# Usage
check = SimpleShearCheck(
name="Shear check for Beam B-1",
source_docs=["EN 1992-1-1 art. 6.2.2"],
v_ed=150_000,
v_rd=200_000,
)
result = check.check()Composite Check Pattern
@dataclass(frozen=True)
class SteelBeamCheck:
"""Check composed of sub-checks."""
name: str
source_docs: list[str]
m_ed: float
m_rd: float
v_ed: float
v_rd: float
def execute(self) -> dict[str, BaseCheck]:
"""Create sub-check instances."""
bending = BendingCheck("Bending", ["EN 1993"], self.m_ed, self.m_rd)
shear = ShearCheck("Shear", ["EN 1993"], self.v_ed, self.v_rd)
return {"bending": bending, "shear": shear}
def check(self) -> CheckResult:
"""Aggregate sub-checks."""
checks = self.execute()
return aggregate_checks(checks, mode="all_must_pass")
def calculation_steps(self) -> dict[str, StructuralCheckProtocol]:
"""Return sub-check instances."""
return self.execute()
# ... and any other relevant methods that are specific to this check
# Usage
check = SteelBeamCheck(
name="Steel IPE 300",
source_docs=["EN 1993-1-1:2005"],
m_ed=100_000,
m_rd=150_000,
v_ed=50_000,
v_rd=75_000,
)
# Overall result
overall = check.check()
# Inspect sub-checks independently
for name, sub_check in check.calculation_steps().items():
result = sub_check.check()
print(f"{name}: {result.is_ok}")OPTIONAL: BaseCheck ABC
For the case where inheritance is preferred over duck typing:
@dataclass(frozen=True)
class BaseCheck(ABC):
"""Optional base class. Implements StructuralCheckProtocol."""
name: str
source_docs: list[str] = field(default_factory=list)
@abstractmethod
def execute(self, **kwargs) -> Any:
pass
@abstractmethod
def check(self) -> CheckResult:
pass
@abstractmethod
def calculation_steps(self) -> dict[str, "StructuralCheckProtocol"]:
return {} # Default: no sub-checks
@abstractmethod
def report(self) -> LatexReport:
"""Optional: Generate formatted reports."""
return LatexReport(...)Choice: Use Protocol (duck typing) OR BaseCheck (structure). Both work identically.
Summary
A minimal, universal interface for structural checks that:
- Scales from simple single-checks to complex hierarchies
- Works for any material (concrete, steel, timber, masonry, etc.)
- Follows SOLID principles
- Maintains backward compatibility
- Provides optional convenience features (BaseCheck, report(), aggregation)
The interface requires only 5 elements:
name- Check identitysource_docs- Documentation referencesexecute()- Internal calculationscheck()- Public verification APIcalculation_steps()- Sub-check composition
So what do you think? Is this the right approach for Blueprints?
- Should
calculation_steps()use a different name? (sub_checks(),components(),children()?) - Any material-specific patterns needed?
- Other suggestions or improvements?
- ABC or Protocol / Maybe both?
Question Description
I would love to have some feedback on this before starting on the implementation.
Relevant Code or Steps
No response
Confirmation
- I have checked that this question does not already exist in the issue tracker or documentation.
- I have read and understood the contribution guidelines.