Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .cursorignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/.venv/
/data/
/img/
/demo/
/LICENSE
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ venv/
.env
.env.local
.env.*
!.env.example

# Caches
.ruff_cache/
Expand Down
326 changes: 241 additions & 85 deletions demo/demo.ipynb

Large diffs are not rendered by default.

132 changes: 109 additions & 23 deletions demo/generalized_backend_documentation.ipynb

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Documentation = "https://2phi.github.io/weac"
interactive = [
"jupyter",
"ipython>=8.37.0",
"ipykernel>=6.30.1",
"ipykernel>=7.1.0",
"jupyter_client>=8.6.3",
"jupyter_core>=5.8.1",
"matplotlib-inline>=0.1.7",
Expand All @@ -56,7 +56,7 @@ dev = [
# Jupyter stack for interactive development
"jupyter",
"ipython>=8.37.0",
"ipykernel>=6.30.1",
"ipykernel>=7.1.0",
"jupyter_client>=8.6.3",
"jupyter_core>=5.8.1",
"matplotlib-inline>=0.1.7",
Expand Down
4 changes: 2 additions & 2 deletions src/weac/analysis/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,8 +745,8 @@ def differential_ERR(
)
Gdif[1:, j] = np.concatenate(
(
self.sm.fq.Gi(z_ct, z_ub, phi=phi, theta=theta, unit=unit),
self.sm.fq.Gii(z_ct, z_ub, phi=phi, theta=theta, unit=unit),
self.sm.fq.Gi(z_ct, z_ub, phi, theta, unit=unit),
self.sm.fq.Gii(z_ct, z_ub, phi, theta, unit=unit),
self.sm.fq.Giii(z_ct, z_ub, phi, theta, unit=unit),
)
Comment thread
pillowbeast marked this conversation as resolved.
)
Expand Down
12 changes: 5 additions & 7 deletions src/weac/analysis/criteria_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1266,20 +1266,18 @@ def _calculate_maximal_stresses(
"""
analyzer = Analyzer(system, printing_enabled=print_call_stats)
_, Z, _ = analyzer.rasterize_solution(num=4000, mode="cracked")
Sxx_kPa = analyzer.Sxx(Z=Z, phi=system.scenario.phi, dz=5, unit="kPa")
Sxx_kPa = analyzer.Sxx(Z=Z, phi=system.scenario.phi, dz=1, unit="kPa")
principal_stress_kPa = analyzer.principal_stress_slab(
Z=Z, phi=system.scenario.phi, dz=5, unit="kPa"
)
Sxx_norm = analyzer.Sxx(
Z=Z, phi=system.scenario.phi, dz=5, unit="kPa", normalize=True
Z=Z, phi=system.scenario.phi, dz=1, unit="kPa"
)
Sxx_norm = analyzer.Sxx(Z=Z, phi=system.scenario.phi, dz=1, normalize=True)
principal_stress_norm = analyzer.principal_stress_slab(
Z=Z, phi=system.scenario.phi, dz=5, unit="kPa", normalize=True
Z=Z, phi=system.scenario.phi, dz=1, normalize=True
)
Comment thread
pillowbeast marked this conversation as resolved.
max_principal_stress_norm = np.max(principal_stress_norm)
max_Sxx_norm = np.max(Sxx_norm)
# evaluate for each height level if the slab is prone to fail under tensile stresses
height_level_prone_to_fail = np.max(Sxx_norm, axis=1)
height_level_prone_to_fail = np.max(Sxx_norm, axis=1) > 1
slab_tensile_criterion = np.mean(height_level_prone_to_fail)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if print_call_stats:
analyzer.print_call_stats(
Expand Down
22 changes: 17 additions & 5 deletions src/weac/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,29 @@
from .criteria_config import CriteriaConfig
from .layer import Layer, WeakLayer
from .model_input import ModelInput
from .segment import Segment
from .presets import (
LESS_WEAK_LAYER,
VERY_WEAK_LAYER,
WEAK_LAYER,
WEAK_LAYER_PRESETS,
weak_layer_from_preset,
)
from .scenario_config import ScenarioConfig, SystemType, TouchdownMode
from .segment import Segment

__all__ = [
"Config",
"WeakLayer",
"Layer",
"Segment",
"CriteriaConfig",
"ScenarioConfig",
"Layer",
"LESS_WEAK_LAYER",
"ModelInput",
"ScenarioConfig",
"Segment",
"SystemType",
"TouchdownMode",
"VERY_WEAK_LAYER",
"WEAK_LAYER",
"WEAK_LAYER_PRESETS",
"WeakLayer",
"weak_layer_from_preset",
]
Comment on lines 19 to 34

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Confirm WEAK_LAYER vs WeakLayer naming won't confuse public API consumers.

Both WEAK_LAYER (the frozen preset instance) and WeakLayer (the class) are now exported from the same package namespace. The names differ only by casing convention, which is correct Python, but from weac.components import WEAK_LAYER vs WeakLayer is a subtle distinction that is easy to misread in user code.

Consider whether a name like WEAK_LAYER_PRESET (or keeping the constant internal to the preset module and exposing it only through WEAK_LAYER_PRESETS["weak"]) would reduce the surface-area ambiguity — especially since WEAK_LAYER_PRESETS already provides dictionary-style access to all three instances.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/weac/components/__init__.py` around lines 19 - 34, The public API
currently exports both the class WeakLayer and the frozen instance constant
WEAK_LAYER which can be confusing; rename the exported constant (e.g.,
WEAK_LAYER -> WEAK_LAYER_PRESET) or remove the individual preset constants from
__all__ and only expose them via WEAK_LAYER_PRESETS to avoid casing-only
ambiguity. Update the __all__ list to reflect the chosen approach and adjust any
references to WEAK_LAYER, VERY_WEAK_LAYER, LESS_WEAK_LAYER, and
weak_layer_from_preset to use the new constant name or access via
WEAK_LAYER_PRESETS, keeping the class name WeakLayer unchanged. Ensure
documentation/comments and imports in other modules are updated to the new
constant name or to use WEAK_LAYER_PRESETS to prevent breaking changes.

35 changes: 34 additions & 1 deletion src/weac/components/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from typing import Literal

from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

from weac.components.scenario_config import TouchdownMode

Expand All @@ -33,6 +33,8 @@ class Config(BaseModel):
are recalculated with different scenario parameters.
"""

model_config = ConfigDict(validate_assignment=True)

touchdown: bool = Field(
default=False, description="Whether to include slab touchdown in the analysis"
)
Expand All @@ -49,6 +51,37 @@ class Config(BaseModel):
description="Force a specific touchdown mode instead of auto-calculating",
)

@field_validator("touchdown")
@classmethod
def validate_touchdown_with_backend(cls, v, info):
"""Validate touchdown compatibility when touchdown is assigned."""
if v and info.data.get("backend") == "generalized":
raise ValueError(
"Slab touchdown is only available for the classic backend. "
"Set backend='classic' or disable touchdown."
)
return v

@field_validator("backend")
@classmethod
def validate_backend_with_touchdown(cls, v, info):
"""Validate backend compatibility when backend is assigned."""
if v == "generalized" and info.data.get("touchdown"):
raise ValueError(
"Slab touchdown is only available for the classic backend. "
"Set backend='classic' or disable touchdown."
)
return v

@model_validator(mode="after")
def validate_touchdown_backend_compatibility(self):
if self.touchdown and self.backend == "generalized":
raise ValueError(
"Slab touchdown is only available for the classic backend. "
"Set backend='classic' or disable touchdown."
)
return self
Comment thread
pillowbeast marked this conversation as resolved.
Comment thread
pillowbeast marked this conversation as resolved.


if __name__ == "__main__":
config = Config()
Expand Down
53 changes: 53 additions & 0 deletions src/weac/components/presets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Named presets for WEAC components."""

from weac.components.layer import WeakLayer

_WEAK_LAYER_PARAMS: dict[str, dict] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Tighten the inner dict type annotation.

dict[str, dict] leaves the value dict untyped.

♻️ Suggested type tightening
-_WEAK_LAYER_PARAMS: dict[str, dict] = {
+_WEAK_LAYER_PARAMS: dict[str, dict[str, float | int]] = {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_WEAK_LAYER_PARAMS: dict[str, dict] = {
_WEAK_LAYER_PARAMS: dict[str, dict[str, float | int]] = {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/weac/components/presets.py` at line 5, The _WEAK_LAYER_PARAMS annotation
is too loose—change it from dict[str, dict] to a tighter type such as dict[str,
dict[str, Any]] or dict[str, Mapping[str, Any]] (import Any/Mapping from typing)
so the inner mapping's key/value types are explicit; update the import and the
annotation on _WEAK_LAYER_PARAMS accordingly (referencing the symbol
_WEAK_LAYER_PARAMS to locate the declaration).

"very_weak": {
"rho": 125,
"h": 10,
"sigma_c": 5.16,
"tau_c": 4.09,
"E": 2.0,
},
"weak": {
"rho": 125,
"h": 10,
"sigma_c": 6.16,
"tau_c": 5.09,
"E": 2.0,
},
"less_weak": {
"rho": 125,
"h": 10,
"sigma_c": 7.16,
"tau_c": 6.09,
"E": 2.0,
},
}

VERY_WEAK_LAYER = WeakLayer(**_WEAK_LAYER_PARAMS["very_weak"])
WEAK_LAYER = WeakLayer(**_WEAK_LAYER_PARAMS["weak"])
LESS_WEAK_LAYER = WeakLayer(**_WEAK_LAYER_PARAMS["less_weak"])

WEAK_LAYER_PRESETS: dict[str, WeakLayer] = {
"very_weak": VERY_WEAK_LAYER,
"weak": WEAK_LAYER,
"less_weak": LESS_WEAK_LAYER,
}


def weak_layer_from_preset(name: str, **overrides) -> WeakLayer:
"""Create a WeakLayer from a named preset, with optional overrides.

Without overrides, returns the shared frozen instance.
With overrides, returns a new instance.
"""
if name not in _WEAK_LAYER_PARAMS:
raise ValueError(
f"Unknown preset '{name}'. Available: {list(_WEAK_LAYER_PARAMS)}"
)
if not overrides:
return WEAK_LAYER_PRESETS[name]
params = {**_WEAK_LAYER_PARAMS[name], **overrides}
Comment on lines +40 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Invalid override keys raise ValidationError, not ValueError — document or normalise.

When overrides contains a key that is not a valid WeakLayer field, WeakLayer(**params) raises pydantic.ValidationError (because the model uses extra = "forbid"). The function only raises ValueError for an unknown preset name, so callers handling ValueError alone will miss this path.

Either document the behaviour, or catch and re-raise for a uniform error surface:

🛡️ Proposed fix to normalise exceptions
+from pydantic import ValidationError
+
 def weak_layer_from_preset(name: str, **overrides) -> WeakLayer:
     """Create a WeakLayer from a named preset, with optional overrides.

     Without overrides, returns the shared frozen instance.
     With overrides, returns a new instance.
+
+    Raises:
+        ValueError: If *name* is not a known preset or an override key is
+            not a valid WeakLayer field.
     """
     if name not in _WEAK_LAYER_PARAMS:
         raise ValueError(
             f"Unknown preset '{name}'. Available: {list(_WEAK_LAYER_PARAMS)}"
         )
     if not overrides:
         return WEAK_LAYER_PRESETS[name]
     params = {**_WEAK_LAYER_PARAMS[name], **overrides}
-    return WeakLayer(**params)
+    try:
+        return WeakLayer(**params)
+    except ValidationError as exc:
+        raise ValueError(
+            f"Invalid override for preset '{name}': {exc}"
+        ) from exc
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/weac/components/presets.py` around lines 40 - 52, weak_layer_from_preset
currently raises ValueError for unknown preset names but allows
pydantic.ValidationError to bubble up when overrides contain invalid fields
(because WeakLayer uses extra="forbid"); make the error surface consistent by
catching pydantic.ValidationError around the WeakLayer(...) construction in
weak_layer_from_preset and re-raising a ValueError with a clear message (include
the invalid keys or the original error text) so callers only need to handle
ValueError; reference the WeakLayer type and the
_WEAK_LAYER_PARAMS/WEAK_LAYER_PRESETS usage to locate where to add the
try/except and raise.

return WeakLayer(**params)
Loading