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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@ scratch/
temp*
old*

.weac-reference/
.weac-reference/

# Folder for development and local testing
dev/
Comment thread
pillowbeast marked this conversation as resolved.
77 changes: 53 additions & 24 deletions demo/demo.ipynb

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/weac/analysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
CoupledCriterionResult,
CriteriaEvaluator,
FindMinimumForceResult,
SSERRResult,
SteadyStateResult,
)
from .plotter import Plotter

Expand All @@ -18,6 +18,6 @@
"CoupledCriterionHistory",
"CoupledCriterionResult",
"FindMinimumForceResult",
"SSERRResult",
"SteadyStateResult",
"Plotter",
]
76 changes: 57 additions & 19 deletions src/weac/analysis/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def get_zmesh(self, dz=2):
], # Convert to t/mm^3
"tensile_strength": [
layer.tensile_strength for layer in self.sm.slab.layers
],
], # in kPa
}

# Repeat properties for each grid point in the layer
Expand All @@ -225,7 +225,7 @@ def get_zmesh(self, dz=2):
return si

@track_analyzer_call
def Sxx(self, Z, phi, dz=2, unit="kPa"):
def Sxx(self, Z, phi, dz=2, unit="kPa", normalize: bool = False):
"""
Compute axial normal stress in slab layers.

Expand All @@ -239,6 +239,10 @@ def Sxx(self, Z, phi, dz=2, unit="kPa"):
Element size along z-axis (mm). Default is 2 mm.
unit : {'kPa', 'MPa'}, optional
Desired output unit. Default is 'kPa'.
normalize : bool, optional
Toggle normalization. If True, normalize stress values to the tensile strength of each layer (dimensionless).
When normalized, the `unit` parameter is ignored and values are returned as ratios.
Default is False.

Returns
-------
Expand All @@ -258,13 +262,13 @@ def Sxx(self, Z, phi, dz=2, unit="kPa"):
m = Z.shape[1]

# Initialize axial normal stress Sxx
Sxx = np.zeros(shape=[n, m])
Sxx_MPa = np.zeros(shape=[n, m])

# Compute axial normal stress Sxx at grid points in MPa
for i, z in enumerate(zi):
E = zmesh["E"][i]
E_MPa = zmesh["E"][i]
nu = zmesh["nu"][i]
Sxx[i, :] = E / (1 - nu**2) * self.sm.fq.du_dx(Z, z)
Sxx_MPa[i, :] = E_MPa / (1 - nu**2) * self.sm.fq.du_dx(Z, z)

# Calculate weight load at grid points and superimpose on stress field
qt = -rho * G_MM_S2 * np.sin(np.deg2rad(phi))
Expand All @@ -274,14 +278,22 @@ def Sxx(self, Z, phi, dz=2, unit="kPa"):
# Sxx[-1, :] += qt[-1] * (zi[-1] - zi[-2])
# New Implementation: Changed for numerical stability
dz = np.diff(zi)
Sxx[:-1, :] += qt[:-1, np.newaxis] * dz[:, np.newaxis]
Sxx[-1, :] += qt[-1] * dz[-1]
Sxx_MPa[:-1, :] += qt[:-1, np.newaxis] * dz[:, np.newaxis]
Sxx_MPa[-1, :] += qt[-1] * dz[-1]

# Normalize tensile stresses to tensile strength
if normalize:
tensile_strength_kPa = zmesh["tensile_strength"]
tensile_strength_MPa = tensile_strength_kPa / 1e3
# Normalize axial normal stress to layers' tensile strength
normalized_Sxx = Sxx_MPa / tensile_strength_MPa[:, None]
return normalized_Sxx
Comment thread
pillowbeast marked this conversation as resolved.

# Return axial normal stress in specified unit
return convert[unit] * Sxx
return convert[unit] * Sxx_MPa

@track_analyzer_call
def Txz(self, Z, phi, dz=2, unit="kPa"):
def Txz(self, Z, phi, dz=2, unit="kPa", normalize: bool = False):
"""
Compute shear stress in slab layers.

Expand All @@ -295,6 +307,9 @@ def Txz(self, Z, phi, dz=2, unit="kPa"):
Element size along z-axis (mm). Default is 2 mm.
unit : {'kPa', 'MPa'}, optional
Desired output unit. Default is 'kPa'.
normalize : bool, optional
Toggle normalization. If True, normalize shear stress values to the tensile strength of each layer (dimensionless).
When normalized, the `unit` parameter is ignored and values are returned as ratios. Default is False.

Returns
-------
Expand Down Expand Up @@ -332,14 +347,22 @@ def Txz(self, Z, phi, dz=2, unit="kPa"):

# Integrate -dsxx_dx along z and add cumulative weight load
# to obtain shear stress Txz in MPa
Txz = cumulative_trapezoid(dsxx_dx, zi, axis=0, initial=0)
Txz += cumulative_trapezoid(qt, zi, initial=0)[:, None]
Txz_MPa = cumulative_trapezoid(dsxx_dx, zi, axis=0, initial=0)
Txz_MPa += cumulative_trapezoid(qt, zi, initial=0)[:, None]

# Normalize shear stresses to tensile strength
if normalize:
tensile_strength_kPa = zmesh["tensile_strength"]
tensile_strength_MPa = tensile_strength_kPa / 1e3
# Normalize shear stress to layers' tensile strength
normalized_Txz = Txz_MPa / tensile_strength_MPa[:, None]
return normalized_Txz
Comment on lines +353 to +359

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 | 🟠 Major

Add guard against division by zero and clarify normalization approach.

Two concerns:

  1. Division by zero: Same issue as Sxx - dividing by tensile_strength_MPa without checking for zero/negative values.

  2. Normalization approach: Normalizing shear stress by tensile strength (rather than shear strength) is unconventional. If this is intentional for a specific physical reason, please document why in the docstring or add a reference.

Add the same guard as suggested for Sxx:

         # Normalize shear stresses to tensile strength
         if normalize:
             tensile_strength_kPa = zmesh["tensile_strength"]
             tensile_strength_MPa = tensile_strength_kPa / 1e3
+            if np.any(tensile_strength_MPa <= 0):
+                raise ValueError("Cannot normalize: tensile_strength must be positive for all layers.")
             # Normalize shear stress to layers' tensile strength
             normalized_Txz = Txz_MPa / tensile_strength_MPa[:, None]
             return normalized_Txz
📝 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
# Normalize shear stresses to tensile strength
if normalize:
tensile_strength_kPa = zmesh["tensile_strength"]
tensile_strength_MPa = tensile_strength_kPa / 1e3
# Normalize shear stress to layers' tensile strength
normalized_Txz = Txz_MPa / tensile_strength_MPa[:, None]
return normalized_Txz
# Normalize shear stresses to tensile strength
if normalize:
tensile_strength_kPa = zmesh["tensile_strength"]
tensile_strength_MPa = tensile_strength_kPa / 1e3
if np.any(tensile_strength_MPa <= 0):
raise ValueError("Cannot normalize: tensile_strength must be positive for all layers.")
# Normalize shear stress to layers' tensile strength
normalized_Txz = Txz_MPa / tensile_strength_MPa[:, None]
return normalized_Txz
🤖 Prompt for AI Agents
In src/weac/analysis/analyzer.py around lines 353 to 359, the code normalizes
shear stresses by tensile_strength_MPa without guarding against zero or
non-positive values and uses tensile strength for shear normalization without
explanation; add a guard that identifies non-positive tensile strength entries
and handles them (e.g., raise a clear ValueError, skip/NaN those layers, or
substitute a small epsilon) to avoid division-by-zero, and either change the
normalization to use shear strength if that was intended or add a brief
docstring comment and a reference explaining why tensile strength is used for
normalizing shear stresses so the choice is explicit.


# Return shear stress Txz in specified unit
return convert[unit] * Txz
return convert[unit] * Txz_MPa

@track_analyzer_call
def Szz(self, Z, phi, dz=2, unit="kPa"):
def Szz(self, Z, phi, dz=2, unit="kPa", normalize: bool = False):
"""
Compute transverse normal stress in slab layers.

Expand All @@ -353,6 +376,10 @@ def Szz(self, Z, phi, dz=2, unit="kPa"):
Element size along z-axis (mm). Default is 2 mm.
unit : {'kPa', 'MPa'}, optional
Desired output unit. Default is 'kPa'.
normalize : bool, optional
Toggle normalization. If True, normalize stress values to the tensile strength of each layer (dimensionless).
When normalized, the `unit` parameter is ignored and values are returned as ratios.
Default is False.

Returns
-------
Expand Down Expand Up @@ -392,11 +419,19 @@ def Szz(self, Z, phi, dz=2, unit="kPa"):
# Integrate dsxx_dxdx twice along z to obtain transverse
# normal stress Szz in MPa
integrand = cumulative_trapezoid(dsxx_dxdx, zi, axis=0, initial=0)
Szz = cumulative_trapezoid(integrand, zi, axis=0, initial=0)
Szz += cumulative_trapezoid(-qn, zi, initial=0)[:, None]
Szz_MPa = cumulative_trapezoid(integrand, zi, axis=0, initial=0)
Szz_MPa += cumulative_trapezoid(-qn, zi, initial=0)[:, None]

# Return shear stress txz in specified unit
return convert[unit] * Szz
# Normalize tensile stresses to tensile strength
if normalize:
tensile_strength_kPa = zmesh["tensile_strength"]
tensile_strength_MPa = tensile_strength_kPa / 1e3
# Normalize transverse normal stress to layers' tensile strength
normalized_Szz = Szz_MPa / tensile_strength_MPa[:, None]
return normalized_Szz

# Return transverse normal stress Szz in specified unit
return convert[unit] * Szz_MPa

@track_analyzer_call
def principal_stress_slab(
Expand Down Expand Up @@ -438,6 +473,8 @@ def principal_stress_slab(
'min', or if normalization of compressive principal stress
is requested.
"""
convert = {"kPa": 1e3, "MPa": 1}

# Raise error if specified component is not available
if val not in ["min", "max"]:
raise ValueError(f"Component {val} not defined.")
Expand All @@ -460,9 +497,10 @@ def principal_stress_slab(
# Normalize tensile stresses to tensile strength
if normalize and val == "max":
zmesh = self.get_zmesh(dz=dz)
tensile_strength = zmesh["tensile_strength"]
tensile_strength_kPa = zmesh["tensile_strength"]
tensile_strength_converted = tensile_strength_kPa / 1e3 * convert[unit]
# Normalize maximum principal stress to layers' tensile strength
normalized_Ps = Ps / tensile_strength[:, None]
normalized_Ps = Ps / tensile_strength_converted[:, None]
return normalized_Ps
Comment on lines 498 to 504

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 | 🟠 Major

Add division by zero guard and consider simplifying normalization.

Two issues:

  1. Division by zero: Missing guard for tensile_strength_converted <= 0.

  2. Inconsistent approach: This method computes stress in the requested unit first (lines 486-491), then normalizes. In contrast, Sxx, Txz, and Szz compute in MPa, normalize if requested, then convert units. Consider aligning the approaches for consistency.

Add guard:

         # Normalize tensile stresses to tensile strength
         if normalize and val == "max":
             zmesh = self.get_zmesh(dz=dz)
             tensile_strength_kPa = zmesh["tensile_strength"]
             tensile_strength_converted = tensile_strength_kPa / 1e3 * convert[unit]
+            if np.any(tensile_strength_converted <= 0):
+                raise ValueError("Cannot normalize: tensile_strength must be positive for all layers.")
             # Normalize maximum principal stress to layers' tensile strength
             normalized_Ps = Ps / tensile_strength_converted[:, None]
             return normalized_Ps
🤖 Prompt for AI Agents
In src/weac/analysis/analyzer.py around lines 498 to 504, the code normalizes Ps
by tensile_strength_converted but lacks a division-by-zero guard and uses a
different unit-conversion order than Sxx/Txz/Szz; change the logic to compute
tensile strength in MPa first (matching the other methods), apply normalization
against tensile_strength_MPa with a safe guard (e.g., mask or clamp values <= 0
to raise or set to np.inf/np.nan) to avoid division by zero, and then convert
the normalized result to the requested unit—or alternatively clamp
tensile_strength_converted to a small positive epsilon (and log/raise on
non-positive values) before dividing so the normalization is consistent and
safe.


# Return absolute principal stresses
Expand Down
114 changes: 102 additions & 12 deletions src/weac/analysis/criteria_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,38 @@ class CoupledCriterionResult:


@dataclass
class SSERRResult:
class MaximalStressResult:
"""
Holds the results of the SSERR evaluation.
Holds the results of the maximal stress evaluation.

Attributes:
-----------
principal_stress_kPa: np.ndarray
The principal stress in kPa.
Sxx_kPa: np.ndarray
The axial normal stress in kPa.
principal_stress_norm: np.ndarray
The normalized principal stress to the tensile strength of the layers.
Sxx_norm: np.ndarray
The normalized axial normal stress to the tensile strength of the layers.
max_principal_stress_norm: float
The normalized maximum principal stress to the tensile strength of the layers.
max_Sxx_norm: float
The normalized maximum axial normal stress to the tensile strength of the layers.
"""

principal_stress_kPa: np.ndarray
Sxx_kPa: np.ndarray
principal_stress_norm: np.ndarray
Sxx_norm: np.ndarray
max_principal_stress_norm: float
max_Sxx_norm: float


@dataclass
class SteadyStateResult:
"""
Holds the results of the Steady State evaluation.

Attributes:
-----------
Expand All @@ -107,15 +136,21 @@ class SSERRResult:
The message of the evaluation.
touchdown_distance : float
The touchdown distance.
SSERR : float
The Steady-State Energy Release Rate calculated with the
touchdown distance from G_I and G_II.
energy_release_rate : float
The steady-state energy release rate calculated with the
touchdown distance from the differential energy release rate.
maximal_stress_result: MaximalStressResult
The maximal stresses in the system at the touchdown distance.
system: SystemModel
The modified system model used for the steady state evaluation.
"""

converged: bool
message: str
touchdown_distance: float
SSERR: float
energy_release_rate: float
maximal_stress_result: MaximalStressResult
system: SystemModel


@dataclass
Expand Down Expand Up @@ -641,12 +676,12 @@ def evaluate_coupled_criterion(
_recursion_depth=_recursion_depth + 1,
)

def evaluate_SSERR(
def evaluate_SteadyState(
self,
system: SystemModel,
vertical: bool = False,
print_call_stats: bool = False,
) -> SSERRResult:
) -> SteadyStateResult:
"""
Comment thread
pillowbeast marked this conversation as resolved.
Evaluates the Touchdown Distance in the Steady State and the Steady State
Energy Release Rate.
Expand Down Expand Up @@ -688,12 +723,19 @@ def evaluate_SSERR(
system_copy.update_scenario(segments=segments, scenario_config=scenario_config)
touchdown_distance = system_copy.slab_touchdown.touchdown_distance
analyzer = Analyzer(system_copy, printing_enabled=print_call_stats)
G, _, _ = analyzer.differential_ERR(unit="J/m^2")
return SSERRResult(
energy_release_rate, _, _ = analyzer.differential_ERR(unit="J/m^2")
maximal_stress_result = self._calculate_maximal_stresses(
system_copy, print_call_stats=print_call_stats
)
if print_call_stats:
analyzer.print_call_stats(message="evaluate_SteadyState Call Statistics")
return SteadyStateResult(
converged=True,
message="SSERR evaluation successful.",
message="Steady State evaluation successful.",
touchdown_distance=touchdown_distance,
SSERR=G,
energy_release_rate=energy_release_rate,
maximal_stress_result=maximal_stress_result,
system=system_copy,
)

def find_minimum_force(
Expand Down Expand Up @@ -1170,3 +1212,51 @@ def _fracture_toughness_exceedance(

# Return the difference from the target
return g_delta_diff - target

def _calculate_maximal_stresses(
self,
system: SystemModel,
print_call_stats: bool = False,
) -> MaximalStressResult:
"""
Calculate the maximal stresses in the system.

Parameters
----------
system : SystemModel
The system model to analyze.
print_call_stats : bool, optional
Whether to print analyzer call statistics. Default is False.

Returns
-------
MaximalStressResult
Object containing both absolute (in kPa) and normalized stress fields,
along with maximum normalized stress values.
"""
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")
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
)
principal_stress_norm = analyzer.principal_stress_slab(
Z=Z, phi=system.scenario.phi, dz=5, unit="kPa", 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)
if print_call_stats:
analyzer.print_call_stats(
message="_calculate_maximal_stresses Call Statistics"
)
return MaximalStressResult(
principal_stress_kPa=principal_stress_kPa,
Sxx_kPa=Sxx_kPa,
principal_stress_norm=principal_stress_norm,
Sxx_norm=Sxx_norm,
max_principal_stress_norm=max_principal_stress_norm,
max_Sxx_norm=max_Sxx_norm,
)
Comment thread
pillowbeast marked this conversation as resolved.
Loading