Skip to content

Conversation

@pillowbeast
Copy link
Collaborator

@pillowbeast pillowbeast commented Jun 30, 2025

Complete Refactor of CodeBase

Reasons:

  • Readability & Separation of Concern

Draft:
In the draft the weac package is refactored in weac_2 and integration tests are provided in tests_2.
Additionally, a streamlit App is provided to test the implementation.

Next steps:

  • Rebase Code to two commits ago (either surface load or crack_length introduced a mistake)
  • Change integration tests to hard-coded tests
  • Extend Plotting Capabilities

Summary by CodeRabbit

  • New Features

    • End-to-end example script, new Analyzer and Plotter for rasterization, stress/ERR analysis and visualizations, and a coupled-criterion evaluator for fracture/stability workflows; SnowPilot CAAML parsing and sample fixtures added.
  • Bug Fixes

    • README/documentation spacing and link corrections; installation/dependency guidance updated for v4.0.
  • Refactor

    • Public API surface reorganized around validated configuration models and a slimmer top-level export set.
  • Tests

    • Large expansion of unit, integration and regression tests with new fixtures and reference comparisons.
  • Chores

    • CI workflows added, Python requirement bumped to 3.12, dependency updates, updated ignore files, and data submodule included.

@coderabbitai
Copy link

coderabbitai bot commented Jun 30, 2025

Warning

Rate limit exceeded

@zacczakk has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 18 minutes and 25 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 08378f4 and a72b6d9.

📒 Files selected for processing (9)
  • .github/workflows/pylint.yml (2 hunks)
  • .gitignore (1 hunks)
  • README.md (5 hunks)
  • docs/sphinx/Makefile (1 hunks)
  • docs/sphinx/conf.py (1 hunks)
  • docs/sphinx/index.rst (1 hunks)
  • pyproject.toml (4 hunks)
  • tests/run_tests.py (1 hunks)
  • weac/__init__.py (1 hunks)

Walkthrough

Replace legacy procedural/mixin code with a new object-oriented core (Slab, Scenario, Eigensystem, SlabTouchdown, SystemModel, UnknownConstantsSolver, FieldQuantities), add Pydantic component models and analysis layer (Analyzer, CriteriaEvaluator, Plotter), introduce constants/utilities, CI workflows, many tests/fixtures, demo scripts, a data submodule, and remove numerous legacy modules and tests.

Changes

Cohort / File(s) Summary
Repo metadata & submodule
*.cursorignore, *.gitignore, .gitmodules, data
Add .cursorignore; extend .gitignore with env/caches/secrets patterns; add data git submodule and update its pointer.
Packaging & tooling
pyproject.toml, .github/workflows/*
Raise Python requirement to >=3.12; add dependencies (pydantic, snowpylot); reformat tool configs; add bumpversion entries; add CI workflows for formatting (ruff), pylint, and tests.
Docs & top-level docs
README.md, TODO.md, docs/sphinx/conf.py, CITATION.cff
Rewrite README usage for new API, fix CITATION link/spacing, add TODO.md, make Sphinx release dynamic and minor formatting tweaks.
Top-level demos & scripts
main.py, validation_cc.py
Add end-to-end demo scripts showing SystemModel, Analyzer, CriteriaEvaluator, and Plotter usage and saved plot outputs.
Core engine & API additions
weac/core/*
weac/core/__init__.py, eigensystem.py, field_quantities.py, scenario.py, slab.py, slab_touchdown.py, system_model.py, unknown_constants_solver.py
New object-oriented core modules implementing slab/scene/eigen/unknown-constants solver, touchdown logic, field-quantity helpers, and SystemModel orchestration; re-export core symbols under weac.core.
Components (Pydantic models)
weac/components/*
weac/components/__init__.py, config.py, criteria_config.py, layer.py, model_input.py, scenario_config.py, segment.py
Add Pydantic models and package exports: Config, CriteriaConfig, Layer, WeakLayer, ModelInput, ScenarioConfig, Segment, SystemType with validation and defaults.
Analysis layer
weac/analysis/*
weac/analysis/__init__.py, analyzer.py, criteria_evaluator.py, plotter.py
Add Analyzer (rasterization, stress/ERR/potentials), CriteriaEvaluator (fracture & coupled workflows + result dataclasses), and Plotter plotting API; expose via weac.analysis.
Utilities & constants
weac/constants.py, weac/utils/*
weac/utils/misc.py, weac/utils/geldsetzer.py, weac/utils/snow_types.py
Add simulation constants and utility helpers: decompose_to_normal_tangential, get_skier_point_load, isnotebook, load_dummy_profile, Geldsetzer density estimator, and snow-type Literal aliases.
Logging helper
weac/logging_config.py
Add setup_logging(level: Optional[str]) using dictConfig and WEAC_LOG_LEVEL env var.
Top-level API surface change
weac/__init__.py
Remove legacy re-exports (Layered, Inverse, plot) and reduce public surface; keep __version__ updated formatting.
Legacy removals
weac/eigensystem.py, weac/inverse.py, weac/layered.py, weac/mixins.py, weac/plot.py, weac/requirements.txt, examples/criterion_check.py
Remove many legacy procedural/mixin-based modules, old plotting module, requirements file, and an example script.
Tests added
tests/** (many new files)
Add extensive new test suite covering core, components, analysis, regression, utilities, SnowPilot CAAML fixtures, reference-runner tooling, and JSON helpers.
Tests removed / runner updated
tests/__init__.py, updated tests/run_tests.py, deleted legacy tests
Remove several legacy test files; update test runner discovery and output semantics (run_tests now returns TestResult and prints summary).
Test tooling & fixtures
tests/utils/weac_reference_runner.py, tests/.materials/*.xml
Add reference-runner to provision pinned external weac venvs and CAAML XML fixtures for SnowPilot parser tests.
Test utilities
tests/utils/json_helpers.py, tests/utils/test_json_helpers.py
Add JSON helper for NumPy types and corresponding tests.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Components as Components (ModelInput, Config)
  participant System as SystemModel
  participant Core as Eigensystem/Scenario/Solver
  participant Analysis as Analyzer/CriteriaEvaluator
  participant Plot as Plotter

  User->>Components: create ModelInput + Config
  User->>System: instantiate SystemModel(model_input, config)
  System->>Core: build Slab and Scenario
  System->>Core: compute cached Eigensystem
  System->>Core: UnknownConstantsSolver.solve_for_unknown_constants(...)
  System-->>User: provide field quantities (fq) and z(...)
  User->>Analysis: Analyzer(SystemModel) / CriteriaEvaluator(...)
  Analysis->>System: request rasterize_solution / find_minimum_force / find_crack_length
  Analysis-->>User: raster, stresses, ERR, CoupledCriterionResult
  User->>Plot: Plotter.plot_*(Analyzer/SystemModel)
  Plot-->>User: saved figure files
Loading
sequenceDiagram
  participant System as SystemModel
  participant TD as SlabTouchdown
  participant Solver as UnknownConstantsSolver

  System->>TD: instantiate if config.touchdown True
  TD-->>System: touchdown_mode, touchdown_distance, collapsed_kR
  System->>Solver: solve_for_unknown_constants(..., touchdown params)
  Solver-->>System: unknown constants C
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~180 minutes

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/researcher-focus

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@zacczakk
Copy link
Member

zacczakk commented Jul 7, 2025

bugbot run

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Bug: Duplicate Methods Cause Redundant Computation

The methods _create_collapsed_eigensystem and _create_collapsed_system are duplicates with identical logic. Both create a collapsed weak layer with modified stiffness and its associated eigensystem, though _create_collapsed_eigensystem takes an unused qs parameter. This leads to redundant, expensive computation as both are called, with the second call overwriting the first's results, and introduces maintenance overhead.

weac_2/core/slab_touchdown.py#L203-L239

def _create_collapsed_eigensystem(self, qs: float):
"""
Create the collapsed weak layer and eigensystem with modified stiffness values.
This centralizes all collapsed-related logic within the SlabTouchdown class.
"""
# Create collapsed weak layer with increased stiffness
self.collapsed_weak_layer = self.scenario.weak_layer.model_copy(
update={
"kn": self.scenario.weak_layer.kn * STIFFNESS_COLLAPSE_FACTOR,
"kt": self.scenario.weak_layer.kt * STIFFNESS_COLLAPSE_FACTOR,
}
)
# Create eigensystem for the collapsed weak layer
self.collapsed_eigensystem = Eigensystem(
weak_layer=self.collapsed_weak_layer, slab=self.scenario.slab
)
def _create_collapsed_system(self):
"""
Create the collapsed weak layer and eigensystem with modified stiffness values.
This centralizes all collapsed-related logic within the SlabTouchdown class.
"""
# Create collapsed weak layer with increased stiffness
self.collapsed_weak_layer = self.scenario.weak_layer.model_copy(
update={
"kn": self.scenario.weak_layer.kn * STIFFNESS_COLLAPSE_FACTOR,
"kt": self.scenario.weak_layer.kt * STIFFNESS_COLLAPSE_FACTOR,
}
)
# Create eigensystem for the collapsed weak layer
self.collapsed_eigensystem = Eigensystem(
weak_layer=self.collapsed_weak_layer, slab=self.scenario.slab
)

Fix in CursorFix in Web


BugBot free trial expires on July 22, 2025
You have used $0.00 of your $50.00 spend limit so far. Manage your spend limit in the Cursor dashboard.

Was this report helpful? Give feedback by reacting with 👍 or 👎

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 23

🔭 Outside diff range comments (1)
.github/workflows/pylint.yml (1)

30-68: Upload pylint-report.txt as an artifact for reviewers

You already generate a useful report; persist it for PR reviewers and history.

       - name: Run pylint analysis
         # Using repository pylint config (pyproject.toml) with comprehensive settings for scientific code
         run: |
           exit_code=0
           TARGET_DIRS=""
           for d in weac weac_2 tests tests_2; do
             if [ -d "$d" ]; then TARGET_DIRS="$TARGET_DIRS $d"; fi
           done
           if [ -z "$TARGET_DIRS" ]; then
             echo "No target directories found for pylint (checked: weac, weac_2, tests, tests_2)."
             # Exit early without failing CI to avoid false negatives on empty repos.
             exit 0
           fi
           python -m pylint --rcfile=pyproject.toml --output-format=parseable --output=pylint-report.txt $TARGET_DIRS || exit_code=$?
           echo "Pylint finished with exit code $exit_code."
           echo
           echo "Pylint exit code meaning:"
           if [ $exit_code -eq 0 ]; then echo "-> No issues found"; fi
           if [ $((exit_code & 1)) -ne 0 ]; then echo "-> Fatal message issued"; fi
           if [ $((exit_code & 2)) -ne 0 ]; then echo "-> Error message issued"; fi
           if [ $((exit_code & 4)) -ne 0 ]; then echo "-> Warning message issued"; fi
           if [ $((exit_code & 8)) -ne 0 ]; then echo "-> Refactor message issued"; fi
           if [ $((exit_code & 16)) -ne 0 ]; then echo "-> Convention message issued"; fi
           if [ $((exit_code & 32)) -ne 0 ]; then echo "-> Usage error"; fi
           echo
           echo 'Error type counts:'
-          grep -oP '[A-Z]\d+\([a-z\-]+\)' pylint-report.txt | sort | uniq -c | sort -nr
+          grep -oE '[A-Z][0-9]+\([a-z-]+\)' pylint-report.txt | sort | uniq -c | sort -nr
           echo
           echo 'Errors per file:'
-          grep -oP '^[\w\-\/]+\.py' pylint-report.txt | sort | uniq -c | sort -nr
+          grep -oE '^[[:alnum:]_.\/-]+\.py' pylint-report.txt | sort | uniq -c | sort -nr
           echo
           echo 'Total errors:'
-          grep -oP '^[\w\-\/]+\.py' pylint-report.txt | wc -l
+          grep -oE '^[[:alnum:]_.\/-]+\.py' pylint-report.txt | wc -l
           echo
           grep 'Your code' pylint-report.txt || true
           # Fail on fatal, error, and usage error.
           # These are severe and should block PRs.
           # Warnings (4), refactors (8), and conventions (16) will not cause a failure.
           fail_on_codes=$((1 | 2 | 32))
           if [ $((exit_code & fail_on_codes)) -ne 0 ]; then
             echo "Failing CI due to fatal/error/usage messages from pylint."
             exit 1
           else
             echo "Pylint check passed. No fatal/error/usage messages."
           fi
+
+      - name: Upload pylint report
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: pylint-report
+          path: pylint-report.txt
+          if-no-files-found: ignore
+          retention-days: 7
♻️ Duplicate comments (27)
.github/workflows/pylint.yml (2)

3-10: Good fix: quoted “on” key resolves yamllint truthy warning

The top-level on key is now quoted. This aligns with yamllint rules and prior feedback.


47-54: Replace grep -P with portable -E patterns

GNU grep on ubuntu supports -P, but -P can be disabled elsewhere. Using -E is more portable with equivalent patterns.

-          grep -oP '[A-Z]\d+\([a-z\-]+\)' pylint-report.txt | sort | uniq -c | sort -nr
+          grep -oE '[A-Z][0-9]+\([a-z-]+\)' pylint-report.txt | sort | uniq -c | sort -nr
@@
-          grep -oP '^[\w\-\/]+\.py' pylint-report.txt | sort | uniq -c | sort -nr
+          grep -oE '^[[:alnum:]_.\/-]+\.py' pylint-report.txt | sort | uniq -c | sort -nr
@@
-          grep -oP '^[\w\-\/]+\.py' pylint-report.txt | wc -l
+          grep -oE '^[[:alnum:]_.\/-]+\.py' pylint-report.txt | wc -l
tests/utils/test_misc.py (1)

109-112: Clarify tangential comment for negative angles (match implementation/sign convention)

The code computes expected_tan = -f * sin(phi). For negative φ, this is positive; the comment currently suggests “magnitude f*sin(phi)” which can confuse since sin(phi) is negative. Tighten the wording.

-        # Normal component should still be positive and equal to f*cos(phi)
-        # Tangential should be positive (upslope for negative angle) with magnitude f*sin(phi)
+        # Normal component equals f*cos(phi) (cos is even ⇒ cos(-φ) = cos(φ))
+        # Tangential equals -f*sin(phi); for negative φ this becomes positive with magnitude f*sin(|φ|)
README.md (5)

182-187: Fix typo in header: “Exampel” → “Example”

Minor but user-facing.

-# Exampel 2: PST
+# Example 2: PST

216-217: Grammar: “cached property” (not “cached_properties”)

Tighten the sentence and avoid the underscore.

-Unknown constants are cached_properties; calling `skier_system.unknown_constants` solves the system of linear equations and extracts the constants.
+Unknown constants are a cached property; calling `skier_system.unknown_constants` solves the system of linear equations and extracts the constants.

261-271: Replace undefined placeholders (top/mid/bot) with concrete values and fix “weaklayer” typo

As written, top, mid, bot are undefined; give runnable values or explain them. Also fix the comment wording.

-# Compute stresses in kPa in the weaklayer
+# Compute stresses in kPa in the weak layer
 tau = skier_system.fq.tau(Z=z_skier, unit='kPa')
 sig = skier_system.fq.sig(Z=z_skier, unit='kPa')

 w = skier_system.fq.w(Z=z_skier, unit='um')
-# Example evaluation vertical displacement at top/mid/bottom of the slab
-u_top = skier_system.fq.u(Z=z_skier, h0=top, unit='um')
-u_mid = skier_system.fq.u(Z=z_skier, h0=mid, unit='um')
-u_bot = skier_system.fq.u(Z=z_skier, h0=bot, unit='um')
+# Example: vertical displacement at top/mid/bottom of the weak layer (mm from top surface)
+u_top = skier_system.fq.u(Z=z_skier, h0=0.0, unit='um')
+u_mid = skier_system.fq.u(Z=z_skier, h0=10.0, unit='um')
+u_bot = skier_system.fq.u(Z=z_skier, h0=20.0, unit='um')
 psi = skier_system.fq.psi(Z=z_skier, unit='deg')

360-366: Indent fenced block correctly, remove trailing spaces, and surround with blank lines

Keeps ordered list numbering intact and satisfies markdownlint MD031/MD029.

-2. Initialize submodules
-
-    ```bash  
-    git submodule update --init --recursive
-    ```
+2. Initialize submodules
+
+    ```bash
+    git submodule update --init --recursive
+    ```
+

200-206: Use the defined layers variable; custom_layers is undefined

The snippet above defines layers. Using custom_layers will raise a NameError if readers copy/paste.

 model_input = ModelInput(
     weak_layer=weak_layer,
     scenario_config=skier_config,
-    layers=custom_layers,
+    layers=layers,
     segments=skier_segments,
 )
tests/run_tests.py (1)

29-37: Discover tests in both tests and tests_2 to match the refactor

The PR mentions tests migrating to tests_2. Discover both directories when present.

-    # Discover all tests in the tests directory (recursive by default)
-    test_suite = unittest.defaultTestLoader.discover(
-        test_dir, pattern="test_*.py", top_level_dir=parent_dir
-    )
-
-    # Count and display discovered tests
-    test_count = test_suite.countTestCases()
-    print(f"Found {test_count} test cases")
+    # Discover in tests and optionally tests_2 (recursive by default)
+    test_dirs = [test_dir]
+    alt_dir = os.path.join(parent_dir, "tests_2")
+    if os.path.isdir(alt_dir):
+        test_dirs.append(alt_dir)
+
+    suites = [
+        unittest.defaultTestLoader.discover(d, pattern="test_*.py", top_level_dir=parent_dir)
+        for d in test_dirs
+    ]
+    test_suite = unittest.TestSuite(suites)
+
+    # Count and display discovered tests
+    test_count = test_suite.countTestCases()
+    print(f"Found {test_count} test cases across: {', '.join(test_dirs)}")
.github/workflows/tests.yml (3)

4-11: LGTM: quoting “on” and corrected indentation fix YAML linting issues

Trigger configuration and indentation look correct.


20-24: Speed up installs by enabling pip caching

Caching pip downloads noticeably reduces CI time.

       - name: Set up Python 3.12
         uses: actions/setup-python@v5
         with:
           python-version: "3.12"
+          cache: "pip"

30-34: Optionally install dev extras if tests rely on them

If test tooling or linters are in the dev extra, install with extras.

       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          python -m pip install -e .
+          python -m pip install -e ".[dev]"
weac/components/scenario_config.py (4)

7-7: Import ConfigDict to support field aliases (for backward compatibility)

We’ll use an alias for legacy configs (system) and need ConfigDict.

-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, ConfigDict

41-52: Add alias for legacy system key and clarify allowed values in description

Keeps old configs working and avoids doc/field mismatches.

-    system_type: SystemType = Field(
-        default="skiers",
-        description="Type of system, '-pst', 'pst-', ....; \n"
-        "skier: single skier in-between two segments, \n"
-        "skiers: multiple skiers spread over the slope, \n"
-        "pst-: positive PST: down-slope + slab-normal cuts, \n"
-        "-pst: negative PST: up-slope + slab-normal cuts, \n"
-        "rot: rotation: rotation of the slab, \n"
-        "trans: translation: translation of the slab, \n"
-        "vpst-: positive VPST: down-slope + vertical cuts, \n"
-        "-vpst: negative VPST: up-slope + vertical cuts, \n",
-    )
+    # Accept legacy key `system` (Pydantic v2)
+    model_config = ConfigDict(populate_by_name=True)
+
+    system_type: SystemType = Field(
+        default="skiers",
+        alias="system",
+        description=(
+            "Type of system. Allowed: 'skier', 'skiers', 'pst-', '-pst', 'rot', 'trans', 'vpst-', '-vpst'."
+        ),
+    )

59-61: Alias cut_length to legacy crack_length for smoother migration

Examples and old configs may still use crack_length.

-    cut_length: float = Field(
-        default=0.0, ge=0, description="Cut length of performed PST or VPST [mm]"
-    )
+    cut_length: float = Field(
+        default=0.0,
+        ge=0,
+        alias="crack_length",
+        description="Cut length for PST/VPST [mm] (legacy alias: crack_length)",
+    )

67-72: Clarify no upper bound implied for surface_load; units are consistent

Text and non-negative constraint align with N/mm convention; consider stating “no upper bound enforced” if you’ve seen confusion in user configs.

tests/core/test_system_model.py (3)

325-328: Avoid in-place mutation of ScenarioConfig before update (copy first).

Mutating shared config instances can cause subtle aliasing/caching issues. Copy, modify, then update.

-        new_cfg = system.scenario.scenario_config
-        new_cfg.phi = 20.0
-        system.update_scenario(scenario_config=new_cfg)
+        new_cfg = system.scenario.scenario_config.model_copy()
+        new_cfg.phi = 20.0
+        system.update_scenario(scenario_config=new_cfg)

336-373: Add a follow-up test that toggling touchdown off resets solver args.

You cover the True path; add a quick test to ensure (None, None, None) after disabling to catch cache/param leakage.

@@
     def test_toggle_touchdown_switches_solver_arguments(self, mock_solve):
         """Test that toggle_touchdown switches the solver arguments."""
@@
         self.assertEqual(calls[1], (900.0, "A_free_hanging", None))
+
+    @patch("weac.core.system_model.UnknownConstantsSolver.solve_for_unknown_constants")
+    def test_toggle_touchdown_off_resets_solver_arguments(self, mock_solve):
+        calls = []
+        def solver_side_effect(scenario, eigensystem, system_type,
+                               touchdown_distance, touchdown_mode, collapsed_weak_layer_kR):
+            calls.append((touchdown_distance, touchdown_mode, collapsed_weak_layer_kR))
+            return np.zeros((6, len(scenario.segments)))
+        mock_solve.side_effect = solver_side_effect
+        system = self._build_model(touchdown=True, system_type="skiers")
+        with patch("weac.core.system_model.SlabTouchdown") as mock_td:
+            mock_inst = MagicMock()
+            mock_inst.touchdown_distance = 500.0
+            mock_inst.touchdown_mode = "B_point_contact"
+            mock_inst.collapsed_weak_layer_kR = 1.0
+            mock_td.return_value = mock_inst
+            _ = system.unknown_constants  # with TD
+        system.toggle_touchdown(False)
+        _ = system.unknown_constants  # recompute without TD
+        self.assertEqual(calls[0], (500.0, "B_point_contact", 1.0))
+        self.assertEqual(calls[1], (None, None, None))

267-293: Also verify touchdown param threading for uncracked branch.

Parity with the cracked path test will protect TD-arg propagation in the uncracked solver.

@@
     def test_uncracked_unknown_constants_sets_all_foundation(self, mock_solve):
         """Test that uncracked_unknown_constants sets all foundation."""
-        captured_scenarios = []
+        captured_scenarios = []
+        captured_args = []
@@
-            captured_scenarios.append(scenario)
+            captured_scenarios.append(scenario)
+            captured_args.append((touchdown_distance, touchdown_mode, collapsed_weak_layer_kR))
             n = len(scenario.segments)
             return np.zeros((6, n))
@@
         self.assertTrue(
             all(seg.has_foundation for seg in captured_scenarios[-1].segments)
         )
+        # Touchdown args should be None when touchdown is disabled
+        self.assertEqual(captured_args[-1], (None, None, None))
tests/utils/weac_reference_runner.py (2)

114-132: Avoid code injection in version check; pass expected version via argv.

Don’t embed untrusted version strings into executed code. Read from sys.argv instead.

-        code = (
+        code = (
             "import sys\n"
             "try:\n"
             "    from importlib.metadata import version, PackageNotFoundError\n"
             "except Exception:\n"
             "    from importlib_metadata import version, PackageNotFoundError\n"
             "try:\n"
-            f"    v = version('weac'); sys.exit(0 if v == '{version}' else 1)\n"
+            "    expected = sys.argv[1]\n"
+            "    v = version('weac'); sys.exit(0 if v == expected else 1)\n"
             "except PackageNotFoundError:\n"
             "    sys.exit(2)\n"
         )
         check_proc = subprocess.run(
-            [py_exe, "-c", code],
+            [py_exe, "-c", code, version],
             cwd=venv_dir,
             env=_clean_env(),
             check=False,
         )

49-53: Harden pip environment (disable version check) and always sanitize env.

Minor speed/noise improvement; use PIP_DISABLE_PIP_VERSION_CHECK.

 def _clean_env() -> Dict[str, str]:
     env = os.environ.copy()
     env.pop("PYTHONPATH", None)
     env["PYTHONNOUSERSITE"] = "1"
+    env.setdefault("PIP_DISABLE_PIP_VERSION_CHECK", "1")
     return env
weac/core/system_model.py (1)

399-407: Vectorize z(...) for sequence x to reduce Python overhead.

The current list-comprehension concatenation is allocation-heavy. Preallocate or batch-call if zh/zp can handle vector x.

-        if isinstance(x, (list, tuple, np.ndarray)):
-            z = np.concatenate(
-                [
-                    np.dot(self.eigensystem.zh(xi, length, has_foundation), C)
-                    + self.eigensystem.zp(xi, phi, has_foundation, qs)
-                    for xi in x
-                ],
-                axis=1,
-            )
+        if isinstance(x, (list, tuple, np.ndarray)):
+            x_arr = np.asarray(x)
+            cols = []
+            for xi in x_arr:
+                cols.append(
+                    self.eigensystem.zh(xi, length, has_foundation) @ C
+                    + self.eigensystem.zp(xi, phi, has_foundation, qs)
+                )
+            z = np.concatenate(cols, axis=1)
weac/core/slab.py (2)

100-103: Vectorize boundary coordinate computation (O(n) instead of O(n²)).

Replace repeated sum() with cumsum for clarity and performance.

-            z = np.array([-H / 2 + sum(hi[0:j]) for j in range(n + 1)])
+            z = -H / 2 + np.concatenate(([0.0], np.cumsum(hi)))

83-85: Clarify units of w (mass vs weight per unit width).

Ai is [mm²] and rho is [t/mm³], so w is [t/mm]. Current doc implies [t] and “weight” (force), which is misleading.

-        w : float
-            Weight of the slab segment that is cut off or added [t]
+        w : float
+            Mass per unit out-of-plane width of the triangular segment [t/mm]
+            (often interpreted as [t] assuming 1 mm width)
weac/core/slab_touchdown.py (3)

250-255: Good: polynomial evaluation logging is at debug-level

This avoids flooding logs during root-finding iterations.


329-335: Fix return docstring: it returns stiffness, not “has_foundation”

Rename the return description to “stiffness” and clarify meaning.

Apply this diff:

-        has_foundation : stiffness of substitute spring.
+        stiffness : substitute spring stiffness at x=0.
+            For dof='rot': kR = M/ψ. For dof='trans': kN = V/w.
+            UnknownConstantsSolver applies unit excitation, so numerically
+            kR=1/ψ and kN=1/w, but using the ratios keeps this robust.

350-363: Compute physical stiffness M/ψ and V/w; fix misleading comments

Current implementation returns 1/ψ and 1/w with comments that mention “moment of inertia” and “weight,” which are incorrect. Use the actual ratios M/ψ and V/w evaluated at x=0. This remains correct even if the unit excitation convention in UnknownConstantsSolver changes.

Apply this diff:

-        stiffness = None
-        if dof in ["rot"]:
-            # For rotational stiffness: has_foundation = M / psi
-            # Uses M = 1.0 for the moment of inertia.
-            psi_val = fq.psi(z_at_x0)[0]  # Extract scalar value from the result
-            stiffness = abs(1 / psi_val) if abs(psi_val) > 1e-12 else 1e12
-        elif dof in ["trans"]:
-            # For translational stiffness: has_foundation = V / w
-            # Uses w = 1.0 for the weight of the slab.
-            w_val = fq.w(z_at_x0)[0]  # Extract scalar value from the result
-            stiffness = abs(1 / w_val) if abs(w_val) > 1e-12 else 1e12
-        if stiffness is None:
-            raise ValueError(f"Stiffness for {dof} is None")
-        return stiffness
+        stiffness = None
+        eps = 1e-12
+        if dof in ["rot"]:
+            # Rotational substitute stiffness: kR = M / ψ at x=0
+            psi_val = float(fq.psi(z_at_x0)[0])
+            M_val = float(fq.M(z_at_x0)[0])
+            stiffness = abs(M_val / psi_val) if abs(psi_val) > eps else 1e12
+        elif dof in ["trans"]:
+            # Translational substitute stiffness: kN = V / w at x=0
+            w_val = float(fq.w(z_at_x0)[0])
+            V_val = float(fq.V(z_at_x0)[0])
+            stiffness = abs(V_val / w_val) if abs(w_val) > eps else 1e12
+        else:
+            raise ValueError(f"Invalid dof: {dof}. Expected 'rot' or 'trans'.")
+        return stiffness
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 883aa89 and 770af47.

📒 Files selected for processing (12)
  • .github/workflows/pylint.yml (1 hunks)
  • .github/workflows/tests.yml (1 hunks)
  • README.md (5 hunks)
  • tests/core/test_system_model.py (1 hunks)
  • tests/run_tests.py (1 hunks)
  • tests/test_comparison_results.py (1 hunks)
  • tests/utils/test_misc.py (1 hunks)
  • tests/utils/weac_reference_runner.py (1 hunks)
  • weac/components/scenario_config.py (1 hunks)
  • weac/core/slab.py (1 hunks)
  • weac/core/slab_touchdown.py (1 hunks)
  • weac/core/system_model.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (8)
tests/utils/test_misc.py (1)
weac/utils/misc.py (2)
  • decompose_to_normal_tangential (13-38)
  • get_skier_point_load (41-56)
weac/core/slab.py (1)
weac/components/layer.py (1)
  • Layer (97-171)
tests/run_tests.py (1)
weac/logging_config.py (1)
  • setup_logging (10-39)
tests/utils/weac_reference_runner.py (1)
weac/core/system_model.py (1)
  • z (366-413)
tests/core/test_system_model.py (5)
weac/components/layer.py (2)
  • Layer (97-171)
  • WeakLayer (174-276)
weac/components/model_input.py (1)
  • ModelInput (28-76)
weac/components/scenario_config.py (1)
  • ScenarioConfig (15-72)
weac/components/segment.py (1)
  • Segment (8-31)
weac/core/system_model.py (10)
  • SystemModel (38-413)
  • eigensystem (152-155)
  • unknown_constants (198-256)
  • update_layers (306-316)
  • update_weak_layer (294-303)
  • update_scenario (319-341)
  • slab_touchdown (158-195)
  • uncracked_unknown_constants (259-291)
  • toggle_touchdown (343-348)
  • z (366-413)
weac/core/system_model.py (9)
weac/components/config.py (1)
  • Config (16-28)
weac/components/layer.py (2)
  • Layer (97-171)
  • WeakLayer (174-276)
weac/components/model_input.py (1)
  • ModelInput (28-76)
weac/components/segment.py (1)
  • Segment (8-31)
weac/core/eigensystem.py (3)
  • Eigensystem (20-405)
  • zh (218-274)
  • zp (276-358)
weac/core/field_quantities.py (1)
  • FieldQuantities (35-273)
weac/core/scenario.py (1)
  • Scenario (17-200)
weac/core/slab.py (1)
  • Slab (13-149)
weac/core/unknown_constants_solver.py (2)
  • UnknownConstantsSolver (28-444)
  • solve_for_unknown_constants (34-256)
tests/test_comparison_results.py (9)
weac/analysis/analyzer.py (7)
  • Analyzer (50-790)
  • rasterize_solution (95-176)
  • get_zmesh (179-225)
  • Sxx (228-281)
  • Txz (284-339)
  • Szz (342-399)
  • principal_stress_slab (402-469)
weac/components/layer.py (2)
  • Layer (97-171)
  • WeakLayer (174-276)
weac/components/model_input.py (1)
  • ModelInput (28-76)
weac/components/scenario_config.py (1)
  • ScenarioConfig (15-72)
weac/components/segment.py (1)
  • Segment (8-31)
weac/components/config.py (1)
  • Config (16-28)
weac/core/system_model.py (4)
  • SystemModel (38-413)
  • unknown_constants (198-256)
  • z (366-413)
  • slab_touchdown (158-195)
tests/utils/weac_reference_runner.py (1)
  • compute_reference_model_results (287-379)
weac/core/field_quantities.py (1)
  • Gi (126-139)
weac/core/slab_touchdown.py (7)
weac/components/layer.py (1)
  • WeakLayer (174-276)
weac/components/scenario_config.py (1)
  • ScenarioConfig (15-72)
weac/components/segment.py (1)
  • Segment (8-31)
weac/core/eigensystem.py (3)
  • Eigensystem (20-405)
  • zh (218-274)
  • zp (276-358)
weac/core/field_quantities.py (3)
  • FieldQuantities (35-273)
  • psi (76-83)
  • w (68-70)
weac/core/scenario.py (1)
  • Scenario (17-200)
weac/core/unknown_constants_solver.py (2)
  • UnknownConstantsSolver (28-444)
  • solve_for_unknown_constants (34-256)
🪛 markdownlint-cli2 (0.17.2)
README.md

37-37: Inline HTML
Element: a

(MD033, no-inline-html)


123-123: Lists should be surrounded by blank lines

(MD032, blanks-around-lists)

🪛 LanguageTool
README.md

[grammar] ~122-~122: There might be a mistake here.
Context: ...om/2phi/weac/blob/main/pyproject.toml)): - [Python](https://www.python.org/downloads...

(QB_NEW_EN)


[grammar] ~123-~123: There might be a mistake here.
Context: ...ownloads/release/python-3100/) ≥ 3.10 - Numpy ≥ 2.0.1 - ...

(QB_NEW_EN)


[grammar] ~124-~124: There might be a mistake here.
Context: ...- Numpy ≥ 2.0.1 - Scipy ≥ 1.14...

(QB_NEW_EN)


[grammar] ~125-~125: There might be a mistake here.
Context: ...ipy](https://www.scipy.org/) ≥ 1.14.0 - Matplotlib &ge...

(QB_NEW_EN)


[grammar] ~126-~126: There might be a mistake here.
Context: ...lib](https://matplotlib.org/) ≥ 3.9.1 - [Pydantic](https://docs.pydantic.dev/late...

(QB_NEW_EN)


[grammar] ~127-~127: There might be a mistake here.
Context: ...//docs.pydantic.dev/latest/) ≥ 2.11.7 - [Snowpylot](https://github.com/connellymk...

(QB_NEW_EN)


[grammar] ~128-~128: Use correct spacing
Context: ...hub.com/connellymk/snowpylot) ≥ 1.1.3 ## Usage The following describes the basic...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~131-~131: Use correct spacing
Context: ... 1.1.3 ## Usage The following describes the basic usage ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~133-~133: Use correct spacing
Context: ...tps://2phi.github.io/weac/) for details. Load the module. ```python import weac ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~135-~135: Use correct spacing
Context: ...io/weac/) for details. Load the module. python import weac Choose a snow profile from the preconfig...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~141-~141: Use correct spacing
Context: ...w surface) to bottom (above weak layer). python from weac.components import Layer layers = [ Layer(rho=170, h=100), # (1) surface layer Layer(rho=190, h=40), # (2) Layer(rho=230, h=130), # : Layer(rho=250, h=20), Layer(rho=210, h=70), Layer(rho=380, h=20), # : Layer(rho=280, h=100) # (N) last slab layer above weak layer ] Create a WeakLayer instance that lies un...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~157-~157: Use correct spacing
Context: ... instance that lies underneath the slab. python from weac.components import WeakLayer weak_layer = WeakLayer(rho=125, h=20) Create a Scenario that defines the envir...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~165-~165: There might be a mistake here.
Context: ...environment and setup that the slab and weaklayer will be evaluated in. ```python from w...

(QB_NEW_EN_OTHER)


[grammar] ~165-~165: Use correct spacing
Context: ...slab and weaklayer will be evaluated in. python from weac.components import ScenarioConfig, Segment # Example 1: SKIER skier_config = ScenarioConfig( system_type='skier', phi=30, ) skier_segments = [ Segment(length=5000, has_foundation=True, m=0), Segment(length=0, has_foundation=False, m=80), Segment(length=0, has_foundation=False, m=0), Segment(length=5000, has_foundation=True, m=0), ] # Scenario is a skier of 80 kg standing on a 10 meter long slab at a 30 degree angle # Exampel 2: PST pst_config = ScenarioConfig( system_type='pst-', # Downslope cut phi=30, # (counterclockwise positive) cut_length=300, ) pst_segments = [ Segment(length=5000, has_foundation=True, m=0), Segment(length=300, has_foundation=False, m=0), # Crack Segment ] # Scenario is Downslope PST with a 300mm cut Create SystemModel instance that combine...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[style] ~193-~193: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... is Downslope PST with a 300mm cut ``` Create SystemModel instance that combines the ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[grammar] ~194-~194: Use correct spacing
Context: ...m solving and field quantity extraction. python from weac.components import Config, ModelInput from weac.core.system_model import SystemModel # Example: build a model for the skier scenario defined above model_input = ModelInput( weak_layer=weak_layer, scenario_config=skier_config, layers=custom_layers, segments=skier_segments, ) system_config = Config( touchdown=True ) skier_system = SystemModel( model_input=model_input, config=system_config, ) Unknown constants are cached_properties;...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~216-~216: There might be a mistake here.
Context: ...em_config, ) ``` Unknown constants are cached_properties; calling `skier_system.unknown_constants...

(QB_NEW_EN_OTHER)


[grammar] ~216-~216: There might be a problem here.
Context: ...ar equations and extracts the constants. python C = skier_system.unknown_constants Analyzer handles rasterization + computation of ...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~222-~222: There might be a mistake here.
Context: ...weak-layer properties Sxx, Sxz, etc. Prepare the output by rasterizing the so...

(QB_NEW_EN)


[grammar] ~223-~223: Use correct spacing
Context: ...ates that are supported by a foundation. python from weac.analysis.analyzer import Analyzer skier_analyzer = Analyzer(skier_system) xsl_skier, z_skier, xwl_skier = skier_analyzer.rasterize_solution(mode="cracked") Gdif, GdifI, GdifII = skier_analyzer.differential_ERR() Ginc, GincI, GincII = skier_analyzer.incremental_ERR() # and Sxx, Sxz, Tzz, principal stress, incremental_potential, ... Visualize the results. ```python from w...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~235-~235: Use correct spacing
Context: ...tential, ... Visualize the results. python from weac.analysis.plotter import Plotter plotter = Plotter() # Visualize slab profile fig = plotter.plot_slab_profile( weak_layers=weak_layer, slabs=skier_system.slab, ) # Visualize deformations as a contour plot fig = plotter.plot_deformed( xsl_skier, xwl_skier, z_skier, skier_analyzer, scale=200, window=200, aspect=2, field="Sxx" ) # Plot slab displacements (using x-coordinates of all segments, xsl) plotter.plot_displacements(skier_analyzer, x=xsl_skier, z=z_skier) # Plot weak-layer stresses (using only x-coordinates of bedded segments, xwl) plotter.plot_stresses(skier_analyzer, x=xwl_skier, z=z_skier) ``` Compute output/field quantities for expo...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~258-~258: Use correct spacing
Context: ...ld quantities for exporting or plotting. python # Compute stresses in kPa in the weaklayer tau = skier_system.fq.tau(Z=z_skier, unit='kPa') sig = skier_system.fq.sig(Z=z_skier, unit='kPa') w = skier_system.fq.w(Z=z_skier, unit='um') # Example evaluation vertical displacement at top/mid/bottom of the slab u_top = skier_system.fq.u(Z=z_skier, h0=top, unit='um') u_mid = skier_system.fq.u(Z=z_skier, h0=mid, unit='um') u_bot = skier_system.fq.u(Z=z_skier, h0=bot, unit='um') psi = skier_system.fq.psi(Z=z_skier, unit='deg') ## Roadmap See the [open issues](https://g...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~274-~274: Use correct spacing
Context: ...='deg') ``` ## Roadmap See the [open issues](https://github.com...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~276-~276: Use correct spacing
Context: ...t of proposed features and known issues. ### v4.0 - [] Change to scenario & scenario...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~278-~278: Use correct spacing
Context: ...sed features and known issues. ### v4.0 - [] Change to scenario & scenario_config:...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~280-~280: Use correct spacing
Context: ...enario_config: InfEnd/Cut/Segment/Weight ### v3.2 <!-- - [ ] New mathematical foundat...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~282-~282: Use correct spacing
Context: ...fig: InfEnd/Cut/Segment/Weight ### v3.2 - [ ] Complex terrain through the addition...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~284-~284: There might be a mistake here.
Context: ...hrough the addition of out-of-plane tilt - [ ] Up, down, and cross-slope cracks ##...

(QB_NEW_EN)


[grammar] ~285-~285: Use correct spacing
Context: ...t - [ ] Up, down, and cross-slope cracks ### v3.1 - [ ] Improved CriteriaEvaluator O...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~287-~287: Use correct spacing
Context: ..., down, and cross-slope cracks ### v3.1 - [ ] Improved CriteriaEvaluator Optimizat...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~289-~289: Use correct spacing
Context: ...aluator Optimization (x2 time reduction) ## Release history ### v3.0 - Code Refact...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~291-~291: Use correct spacing
Context: ... (x2 time reduction) ## Release history ### v3.0 - Code Refactor - Input Validation...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~293-~293: Use correct spacing
Context: ...reduction) ## Release history ### v3.0 - Code Refactor - Input Validation - Modul...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~295-~295: There might be a mistake here.
Context: ...lease history ### v3.0 - Code Refactor - Input Validation - Modular + Object-Orie...

(QB_NEW_EN)


[grammar] ~296-~296: There might be a mistake here.
Context: ...v3.0 - Code Refactor - Input Validation - Modular + Object-Oriented ### v2.6 - F...

(QB_NEW_EN)


[grammar] ~297-~297: Use correct spacing
Context: ...t Validation - Modular + Object-Oriented ### v2.6 - Finite fracture mechanics implem...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~299-~299: Use correct spacing
Context: ...on - Modular + Object-Oriented ### v2.6 - Finite fracture mechanics implementation...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~303-~303: Use correct spacing
Context: ...tropic weak layer (?) - Add demo gif (?) ### v2.5 - Analyze slab touchdown in PST ex...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~305-~305: Use correct spacing
Context: ...k layer (?) - Add demo gif (?) ### v2.5 - Analyze slab touchdown in PST experiment...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~308-~308: There might be a mistake here.
Context: ...significantly improved API documentation ### v2.4 - Choose between slope-normal (`'-...

(QB_NEW_EN_OTHER)


[grammar] ~310-~310: Use correct spacing
Context: ...tly improved API documentation ### v2.4 - Choose between slope-normal ('-pst', `...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~312-~312: There might be a mistake here.
Context: ...st', 'vpst-'`) PST boundary conditions ### v2.3 - Stress plots on deformed contour...

(QB_NEW_EN_OTHER)


[grammar] ~314-~314: Use correct spacing
Context: ...st-'`) PST boundary conditions ### v2.3 - Stress plots on deformed contours - PSTs...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~316-~316: There might be a mistake here.
Context: ...s ### v2.3 - Stress plots on deformed contours - PSTs now account for slab touchdown ###...

(QB_NEW_EN_OTHER)


[grammar] ~317-~317: There might be a mistake here.
Context: ...rs - PSTs now account for slab touchdown ### v2.2 - Sign of inclination phi consis...

(QB_NEW_EN_OTHER)


[grammar] ~319-~319: Use correct spacing
Context: ...now account for slab touchdown ### v2.2 - Sign of inclination phi consistent wit...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~331-~331: There might be a mistake here.
Context: ...t use of coordinate system with downward pointing z-axis - Consitent top-to-botto...

(QB_NEW_EN_OTHER)


[grammar] ~331-~331: Ensure spelling is correct
Context: ...ate system with downward pointing z-axis - Consitent top-to-bottom numbering of slab layers ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~332-~332: There might be a mistake here.
Context: ...t top-to-bottom numbering of slab layers - Implementation of PSTs cut from either l...

(QB_NEW_EN)


[grammar] ~333-~333: There might be a mistake here.
Context: ...ayers - Implementation of PSTs cut from either left or right side ### v2.0 - Complet...

(QB_NEW_EN)


[grammar] ~333-~333: Use correct spacing
Context: ... PSTs cut from either left or right side ### v2.0 - Completely rewritten in 🐍 Pytho...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~335-~335: Use correct spacing
Context: ...from either left or right side ### v2.0 - Completely rewritten in 🐍 Python - Coup...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~360-~360: There might be a mistake here.
Context: ...ork the project 2. Initialize submodules bash git submodule update --init --recursive 3. Create your feature branch (`git checkou...

(QB_NEW_EN_OTHER)


[grammar] ~366-~366: There might be a mistake here.
Context: ...git checkout -b feature/amazingfeature) 4. Commit your changes (git commit -m 'Add...

(QB_NEW_EN_OTHER)


[style] ~367-~367: Consider using a more formal and expressive alternative to ‘amazing’.
Context: ...mazingfeature) 4. Commit your changes (git commit -m 'Add some amazing feature') 5. Push to the branch (git ...

(AWESOME)


[grammar] ~367-~367: There might be a mistake here.
Context: ...t commit -m 'Add some amazing feature') 5. Push to the branch (git push origin fea...

(QB_NEW_EN_OTHER)


[grammar] ~369-~369: There might be a mistake here.
Context: .../amazingfeature`) 6. Open a pull request ## Workflows [![Publish Python 🐍 releases ...

(QB_NEW_EN_OTHER)

🔇 Additional comments (7)
.github/workflows/pylint.yml (1)

19-24: Nice: pip cache enabled on setup-python

This will reduce CI time by reusing downloads.

tests/utils/test_misc.py (1)

15-17: Solid, comprehensive coverage for decomposition and load utilities

Good breadth of cases (0°, 90°, 45°, ±φ symmetry, obtuse, array inputs, energy conservation) and clear assertions. This will catch sign and unit regressions early.

tests/run_tests.py (1)

50-59: Good: success-rate guard avoids division by zero

Thanks for guarding testsRun == 0; avoids ZeroDivisionError when discovery yields no tests.

weac/components/scenario_config.py (1)

53-58: Bounds for phi look good (±90°) and description is clear

Covers steep and near-vertical slopes and documents sign convention.

weac/core/slab_touchdown.py (3)

29-39: Docstring typo from earlier review is fixed; good

The “is strictly smaller than the cut length” correction is in place and reads clearly now.


87-95: Good: flat config cloned correctly with phi=0

You preserve system_type, cut_length, stiffness_ratio, and surface_load while forcing phi=0. This keeps substitute-stiffness problems on a flat configuration, which is appropriate.


337-339: Fix invalid segment position key in UnknownConstantsSolver

The first element of the position vector is being set to "length", which isn’t one of the accepted pos values ('l'|'left', 'm'|'mid', 'r'|'right'). This will skip all branches in _setup_conditions and cause a crash at runtime. Update the assignment to use a supported key:

• File: weac/core/unknown_constants_solver.py
Location (around line 79):

- pi[0], pi[-1] = "length", "r"
+ pi[0], pi[-1] = "l",      "r"

With this change, the first segment will correctly be treated as the left boundary ('l'), matching the checks in _setup_conditions.

Likely an incorrect or invalid review comment.

Comment on lines +237 to +249
# Unpack variables
bs = -(self.eigensystem.B11**2 / self.eigensystem.A11 - self.eigensystem.D11)
ss = self.eigensystem.kA55
L = self.scenario.L
cut_length = self.scenario.cut_length
crack_h = self.scenario.crack_h
qn = self.scenario.qn

# Spring stiffness of uncollapsed eigensystem of length L - cut_length
straight_scenario = self._generate_straight_scenario(L - cut_length)
kRl = self._substitute_stiffness(straight_scenario, self.eigensystem, "rot")
kNl = self._substitute_stiffness(straight_scenario, self.eigensystem, "trans")

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard for qn≈0 and robust bracketing for touchdown_distance in mode C

  • If qn≈0, coefficients collapse and brentq will fail.
  • The fixed bracket [cut/1000, 0.999*cut] can miss sign changes; add adaptive bracketing.
  • Also guard against cut_length ≤ 0.

Add early checks and bracket scanning like above.

Apply this diff:

         # Unpack variables
         bs = -(self.eigensystem.B11**2 / self.eigensystem.A11 - self.eigensystem.D11)
         ss = self.eigensystem.kA55
         L = self.scenario.L
         cut_length = self.scenario.cut_length
         crack_h = self.scenario.crack_h
         qn = self.scenario.qn
+        eps = 1e-12
+        if cut_length <= 0:
+            raise ValueError(f"cut_length must be > 0 for mode C, got {cut_length}")
+        if abs(qn) <= eps:
+            raise ValueError(f"Normal load qn≈0 (qn={qn:.3e}); touchdown_distance undefined.")
@@
-        # Find root
-        touchdown_distance = brentq(
-            polynomial, cut_length / 1000, 999 / 1000 * cut_length
-        )
+        # Find root with robust bracketing
+        a, b = cut_length / 1000.0, 0.999 * cut_length
+        fa, fb = polynomial(a), polynomial(b)
+        if fa == 0.0:
+            return a
+        if fb == 0.0:
+            return b
+        if fa * fb > 0:
+            N = 96
+            prev_x, prev_f = a, fa
+            found = False
+            for i in range(1, N + 1):
+                x = a + (b - a) * i / N
+                fval = polynomial(x)
+                if prev_f * fval <= 0:
+                    a, b = prev_x, x
+                    found = True
+                    break
+                prev_x, prev_f = x, fval
+            if not found:
+                raise ValueError("Failed to bracket a root for touchdown_distance in (0, cut_length).")
+        touchdown_distance = brentq(polynomial, a, b)

Also applies to: 286-290

🤖 Prompt for AI Agents
In weac/core/slab_touchdown.py around lines 237-249 (and similarly at 286-290),
add guards and robust bracketing: first validate scenario.cut_length > 0 and
raise/return early if not; then detect qn ≈ 0 using np.isclose(self.scenario.qn,
0) (or an absolute tolerance) and handle that branch explicitly to avoid forming
degenerate coefficients (e.g., skip root-finding or set touchdown_distance to a
safe default). Replace the fixed brentq bracket [cut/1000, 0.999*cut] with an
adaptive scan that samples a sequence of points from a small positive fraction
up to just below cut_length, searching for an interval where the function
changes sign and using that interval for brentq; if no sign change is found,
fall back to a safe behavior (raise a clear exception or set a fallback value)
instead of calling brentq with an invalid bracket.

Comment on lines +305 to +317
def _generate_straight_scenario(self, L: float) -> Scenario:
"""
Generate a straight scenario with a given length.
"""
segments = [Segment(length=L, has_foundation=True, m=0)]
straight_scenario = Scenario(
scenario_config=self.flat_config,
segments=segments,
weak_layer=self.scenario.weak_layer,
slab=self.scenario.slab,
)
return straight_scenario

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Minor: declared class attribute ‘straight_scenario’ is never set

You declare straight_scenario in the class attributes list but only use a local variable inside _generate_straight_scenario. Either set self.straight_scenario or drop the class attribute to avoid confusion.

Apply this diff to remove the unused class attribute:

-    straight_scenario: Scenario

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In weac/core/slab_touchdown.py around lines 305 to 317, the class declares a
straight_scenario attribute but the method _generate_straight_scenario only
creates a local straight_scenario variable, leaving the class attribute unused;
either assign the generated Scenario to self.straight_scenario
(self.straight_scenario = straight_scenario) so the instance attribute is
populated, or remove the unused class attribute declaration from the class
definition to avoid confusion—pick the option that matches intended design and
update references accordingly.

Comment on lines +337 to +346
unknown_constants = UnknownConstantsSolver.solve_for_unknown_constants(
scenario=scenario, eigensystem=eigensystem, system_type=dof
)

# Calculate field quantities at x=0 (left end)
Zh0 = eigensystem.zh(x=0, length=scenario.L, has_foundation=True)
zp0 = eigensystem.zp(x=0, phi=0, has_foundation=True, qs=0)
C_at_x0 = unknown_constants[:, 0].reshape(-1, 1) # Ensure column vector
z_at_x0 = Zh0 @ C_at_x0 + zp0

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Wrap UnknownConstantsSolver failures with context

If the linear system is singular or ill-conditioned, propagate a clear error from this context to aid callers (especially during root finding).

Apply this diff:

-        unknown_constants = UnknownConstantsSolver.solve_for_unknown_constants(
-            scenario=scenario, eigensystem=eigensystem, system_type=dof
-        )
+        try:
+            unknown_constants = UnknownConstantsSolver.solve_for_unknown_constants(
+                scenario=scenario, eigensystem=eigensystem, system_type=dof
+            )
+        except Exception as e:
+            raise ValueError(f"Failed to compute substitute stiffness (dof='{dof}', L={scenario.L:.3f}): {e}") from e
📝 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
unknown_constants = UnknownConstantsSolver.solve_for_unknown_constants(
scenario=scenario, eigensystem=eigensystem, system_type=dof
)
# Calculate field quantities at x=0 (left end)
Zh0 = eigensystem.zh(x=0, length=scenario.L, has_foundation=True)
zp0 = eigensystem.zp(x=0, phi=0, has_foundation=True, qs=0)
C_at_x0 = unknown_constants[:, 0].reshape(-1, 1) # Ensure column vector
z_at_x0 = Zh0 @ C_at_x0 + zp0
try:
unknown_constants = UnknownConstantsSolver.solve_for_unknown_constants(
scenario=scenario, eigensystem=eigensystem, system_type=dof
)
except Exception as e:
raise ValueError(
f"Failed to compute substitute stiffness (dof='{dof}', L={scenario.L:.3f}): {e}"
) from e
# Calculate field quantities at x=0 (left end)
Zh0 = eigensystem.zh(x=0, length=scenario.L, has_foundation=True)
zp0 = eigensystem.zp(x=0, phi=0, has_foundation=True, qs=0)
C_at_x0 = unknown_constants[:, 0].reshape(-1, 1) # Ensure column vector
z_at_x0 = Zh0 @ C_at_x0 + zp0
🤖 Prompt for AI Agents
In weac/core/slab_touchdown.py around lines 337 to 346, wrap the call to
UnknownConstantsSolver.solve_for_unknown_constants in a try/except that catches
linear-algebra errors (e.g. np.linalg.LinAlgError, numpy.linalg.LinAlgError, or
a broad Exception if the solver raises its own) and re-raise a new RuntimeError
(or a custom exception) with clear context including scenario identifier (e.g.
scenario.name or scenario.L), dof/system_type, and that the linear system is
singular or ill-conditioned, while preserving the original exception as the
__cause__ or by including its message; keep the later usage of unknown_constants
unchanged so callers receive a contextual error during root-finding.

Comment on lines +8 to +9
We utilize the pydantic library to define the system model.
"""
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Docstring nit: SystemModel itself is not a Pydantic model.

Pydantic is used for component models; SystemModel is a plain orchestrator class. Clarify to avoid confusion.

-We utilize the pydantic library to define the system model.
+Pydantic is used for component models (inputs/config). SystemModel orchestrates them.
📝 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
We utilize the pydantic library to define the system model.
"""
Pydantic is used for component models (inputs/config). SystemModel orchestrates them.
"""
🤖 Prompt for AI Agents
In weac/core/system_model.py around lines 8 to 9, the module docstring
incorrectly implies SystemModel is a Pydantic model; clarify that Pydantic is
used only for component models and that SystemModel is a plain
orchestrator/class that composes those Pydantic component models. Update the
docstring to state that Pydantic models define components (e.g., ComponentA,
ComponentB) while SystemModel orchestrates and validates/holds them but does not
inherit from pydantic.BaseModel.

Comment on lines +157 to +195
@cached_property
def slab_touchdown(self) -> Optional[SlabTouchdown]:
"""
Solve for the slab touchdown.
Modifies the scenario object in place by replacing the undercut segment
with a new segment of length equal to the touchdown distance if the system is
a PST or VPST.
"""
if self.config.touchdown:
logger.info("Solving for Slab Touchdown")
slab_touchdown = SlabTouchdown(
scenario=self.scenario, eigensystem=self.eigensystem
)
logger.info(
"Original cut_length: %s, touchdown_distance: %s",
self.scenario.cut_length,
slab_touchdown.touchdown_distance,
)

new_segments = copy.deepcopy(self.scenario.segments)
if self.scenario.system_type in ("pst-", "vpst-"):
new_segments[-1].length = slab_touchdown.touchdown_distance
elif self.scenario.system_type in ("-pst", "-vpst"):
new_segments[0].length = slab_touchdown.touchdown_distance

# Create new scenario with updated segments
self.scenario = Scenario(
scenario_config=self.scenario.scenario_config,
segments=new_segments,
weak_layer=self.weak_layer,
slab=self.slab,
)
logger.info(
"Updated scenario with new segment lengths: %s",
[seg.length for seg in new_segments],
)

return slab_touchdown
return None
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Cached property with side effects (mutating self.scenario).

Accessing slab_touchdown replaces self.scenario lengths. It’s documented, but still surprising for a cached property. Consider isolating mutation behind an explicit method (e.g., apply_touchdown) or computing an effective Scenario for solves.

Please confirm this side effect is acceptable across analyzers/plotters and any external consumers. If not, I can draft an “effective_scenario” pattern that keeps self.scenario immutable.

🤖 Prompt for AI Agents
In weac/core/system_model.py around lines 157 to 195, slab_touchdown is a
cached_property that mutates self.scenario by replacing segment lengths; change
this so the cached property does not have side effects: extract the mutation
logic into an explicit method apply_touchdown(scenario) that returns a new
Scenario (or implement a cached effective_scenario property that returns a
modified copy) and have slab_touchdown only compute and return the SlabTouchdown
without altering self.scenario; update all internal callers (analyzers/plotters)
to use the explicit apply_touchdown() or effective_scenario when they need the
modified segments, and remove in-place assignment to self.scenario to keep the
original scenario immutable.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
.github/workflows/tests.yml (1)

30-31: Verify tests fail the job on failures (non-zero exit)

Confirm tests/run_tests.py exits non-zero on failure. Earlier commits didn’t; your latest changes may have fixed it—let’s verify.

#!/bin/bash
set -euo pipefail
# Check that the test runner exits non-zero on failure
# 1) Ensure a sys.exit based on wasSuccessful() exists in __main__
rg -n -C2 --type=py 'if __name__ == .__main__.:|sys\.exit\(|wasSuccessful\(' tests/run_tests.py || true

# 2) Heuristic: look for unittest result used in sys.exit
rg -n -C2 --type=py 'result\s*=\s*run_tests\(\)' tests/run_tests.py || true

If absent, update main:

if __name__ == "__main__":
    import sys
    result = run_tests()
    sys.exit(0 if result.wasSuccessful() else 1)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 770af47 and b9d8ba0.

📒 Files selected for processing (1)
  • .github/workflows/tests.yml (1 hunks)
🔇 Additional comments (1)
.github/workflows/tests.yml (1)

1-11: Triggers and YAML formatting look good

Quoting "on", fixing indentation under steps, and ending with a newline are all addressed. Workflow triggers cover PRs, workflow_call, and manual dispatch appropriately.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (10)
README.md (5)

184-194: Fix header typo: “Exampel 2” → “Example 2”.

Keep example headers clean and searchable.

-# Exampel 2: PST
+# Example 2: PST

362-366: Remove trailing spaces in fenced code block language tag.

The two spaces after “```bash” violate markdownlint; keep indentation and surrounding blank lines as-is.

-    ```bash  
+    ```bash
     git submodule update --init --recursive
     ```

203-208: Fix undefined variable in ModelInput: use layers (defined above), not custom_layers.

As written, the snippet will error at runtime.

 model_input = ModelInput(
     weak_layer=weak_layer,
     scenario_config=skier_config,
-    layers=custom_layers,
+    layers=layers,
     segments=skier_segments,
 )

218-218: Wording: “cached_properties” → “cached properties”.

Also avoid backticks unless referencing a symbol.

-Unknown constants are cached_properties; calling `skier_system.unknown_constants` solves the system of linear equations and extracts the constants.
+Unknown constants are cached properties; calling `skier_system.unknown_constants` solves the system of linear equations and extracts the constants.

263-273: Fix terminology and undefined variables (top, mid, bot).

  • “weaklayer” → “weak layer” in the comment.
  • Replace top/mid/bot with concrete values or derive from weak_layer.h so the snippet runs.
-# Compute stresses in kPa in the weaklayer
+# Compute stresses in kPa in the weak layer
 tau = skier_system.fq.tau(Z=z_skier, unit='kPa')
 sig = skier_system.fq.sig(Z=z_skier, unit='kPa')
 
 w = skier_system.fq.w(Z=z_skier, unit='um')
-# Example evaluation vertical displacement at top/mid/bottom of the slab
-u_top = skier_system.fq.u(Z=z_skier, h0=top, unit='um')
-u_mid = skier_system.fq.u(Z=z_skier, h0=mid, unit='um')
-u_bot = skier_system.fq.u(Z=z_skier, h0=bot, unit='um')
+# Example: vertical displacement at top/mid/bottom of the weak layer (mm from top surface)
+u_top = skier_system.fq.u(Z=z_skier, h0=0.0, unit='um')
+u_mid = skier_system.fq.u(Z=z_skier, h0=weak_layer.h/2, unit='um')
+u_bot = skier_system.fq.u(Z=z_skier, h0=weak_layer.h, unit='um')
 psi = skier_system.fq.psi(Z=z_skier, unit='deg')
.github/workflows/tests.yml (5)

4-11: Good fix: quoted “on” and clean trigger section

Quoting the on key and structuring triggers looks correct now. Previous yamllint complaints about truthy “on” are resolved.


4-8: Optionally scope PR triggers to main/develop

If you want to limit CI runs to primary branches (as hinted earlier), add a branches filter.

Apply:

   pull_request:
     types: [opened, reopened, synchronize, ready_for_review]
+    branches: [main, develop]

11-13: Harden workflow: least-privilege token + cancel superseded runs

Reduce token scope and cancel in-flight runs on new pushes to the same PR.

Apply:

   workflow_dispatch:
 
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:

28-31: Install build tooling and test extras (if defined)

  • Ensure setuptools/wheel are present for PEP 517 builds.
  • If you define dev/test extras in pyproject, install them to pull test-only deps; otherwise keep -e . or use requirements.txt.

Option A (preferred if extras exist):

       - name: Install dependencies
         run: |
-          python -m pip install --upgrade pip
-          python -m pip install -e .
+          python -m pip install --upgrade pip setuptools wheel
+          python -m pip install -e ".[dev]"

Option B (fallback if no extras; use requirements.txt):

       - name: Install dependencies
         run: |
-          python -m pip install --upgrade pip
-          python -m pip install -e .
+          python -m pip install --upgrade pip setuptools wheel
+          python -m pip install -e .
+          python -m pip install -r requirements.txt

25-27: Include requirements.txt in cache key to improve cache hits

If you keep a requirements.txt (added in this PR), include it so cache invalidates when deps change there.

Apply:

           cache-dependency-path: |
             pyproject.toml
+            requirements.txt
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b9d8ba0 and 4f4b33a.

📒 Files selected for processing (2)
  • .github/workflows/tests.yml (1 hunks)
  • README.md (5 hunks)
🧰 Additional context used
🪛 markdownlint-cli2 (0.17.2)
README.md

37-37: Inline HTML
Element: a

(MD033, no-inline-html)


131-131: Multiple consecutive blank lines
Expected: 1; Actual: 2

(MD012, no-multiple-blanks)

🪛 LanguageTool
README.md

[grammar] ~122-~122: Use correct spacing
Context: ...om/2phi/weac/blob/main/pyproject.toml)): - [Python](https://www.python.org/downloads...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~124-~124: There might be a mistake here.
Context: ...g/downloads/release/python-3120/) ≥ 3.12 - Numpy ≥ 2.0.1 - [Sc...

(QB_NEW_EN)


[grammar] ~125-~125: There might be a mistake here.
Context: ...12 - Numpy ≥ 2.0.1 - Scipy ≥ 1.14.0 ...

(QB_NEW_EN)


[grammar] ~126-~126: There might be a mistake here.
Context: ...Scipy ≥ 1.14.0 - Matplotlib ≥ 3...

(QB_NEW_EN)


[grammar] ~127-~127: There might be a mistake here.
Context: ...lotlib](https://matplotlib.org/) ≥ 3.9.1 - [Pydantic](https://docs.pydantic.dev/late...

(QB_NEW_EN)


[grammar] ~128-~128: There might be a mistake here.
Context: ...ps://docs.pydantic.dev/latest/) ≥ 2.11.7 - [Snowpylot](https://github.com/connellymk...

(QB_NEW_EN)


[grammar] ~129-~129: Use correct spacing
Context: ...github.com/connellymk/snowpylot) ≥ 1.1.3 ## Usage The following describes the basic...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~133-~133: Use correct spacing
Context: ...1.1.3 ## Usage The following describes the basic usage ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~135-~135: Use correct spacing
Context: ...tps://2phi.github.io/weac/) for details. Load the module. ```python import weac ...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~137-~137: Use correct spacing
Context: ...io/weac/) for details. Load the module. python import weac Choose a snow profile from the preconfig...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~143-~143: Use correct spacing
Context: ...w surface) to bottom (above weak layer). python from weac.components import Layer layers = [ Layer(rho=170, h=100), # (1) surface layer Layer(rho=190, h=40), # (2) Layer(rho=230, h=130), # : Layer(rho=250, h=20), Layer(rho=210, h=70), Layer(rho=380, h=20), # : Layer(rho=280, h=100) # (N) last slab layer above weak layer ] Create a WeakLayer instance that lies un...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~159-~159: Use correct spacing
Context: ... instance that lies underneath the slab. python from weac.components import WeakLayer weak_layer = WeakLayer(rho=125, h=20) Create a Scenario that defines the envir...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~167-~167: There might be a mistake here.
Context: ...environment and setup that the slab and weaklayer will be evaluated in. ```python from w...

(QB_NEW_EN_OTHER)


[grammar] ~167-~167: Use correct spacing
Context: ...slab and weaklayer will be evaluated in. python from weac.components import ScenarioConfig, Segment # Example 1: SKIER skier_config = ScenarioConfig( system_type='skier', phi=30, ) skier_segments = [ Segment(length=5000, has_foundation=True, m=0), Segment(length=0, has_foundation=False, m=80), Segment(length=0, has_foundation=False, m=0), Segment(length=5000, has_foundation=True, m=0), ] # Scenario is a skier of 80 kg standing on a 10 meter long slab at a 30 degree angle # Exampel 2: PST pst_config = ScenarioConfig( system_type='pst-', # Downslope cut phi=30, # (counterclockwise positive) cut_length=300, ) pst_segments = [ Segment(length=5000, has_foundation=True, m=0), Segment(length=300, has_foundation=False, m=0), # Crack Segment ] # Scenario is Downslope PST with a 300mm cut Create SystemModel instance that combine...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[style] ~195-~195: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... is Downslope PST with a 300mm cut ``` Create SystemModel instance that combines the ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[grammar] ~196-~196: Use correct spacing
Context: ...m solving and field quantity extraction. python from weac.components import Config, ModelInput from weac.core.system_model import SystemModel # Example: build a model for the skier scenario defined above model_input = ModelInput( weak_layer=weak_layer, scenario_config=skier_config, layers=custom_layers, segments=skier_segments, ) system_config = Config( touchdown=True ) skier_system = SystemModel( model_input=model_input, config=system_config, ) Unknown constants are cached_properties;...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~218-~218: There might be a mistake here.
Context: ...em_config, ) ``` Unknown constants are cached_properties; calling `skier_system.unknown_constants...

(QB_NEW_EN_OTHER)


[grammar] ~218-~218: There might be a problem here.
Context: ...ar equations and extracts the constants. python C = skier_system.unknown_constants Analyzer handles rasterization + computation of ...

(QB_NEW_EN_MERGED_MATCH)


[grammar] ~224-~224: There might be a mistake here.
Context: ...weak-layer properties Sxx, Sxz, etc. Prepare the output by rasterizing the so...

(QB_NEW_EN)


[grammar] ~225-~225: Use correct spacing
Context: ...ates that are supported by a foundation. python from weac.analysis.analyzer import Analyzer skier_analyzer = Analyzer(skier_system) xsl_skier, z_skier, xwl_skier = skier_analyzer.rasterize_solution(mode="cracked") Gdif, GdifI, GdifII = skier_analyzer.differential_ERR() Ginc, GincI, GincII = skier_analyzer.incremental_ERR() # and Sxx, Sxz, Tzz, principal stress, incremental_potential, ... Visualize the results. ```python from w...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~237-~237: Use correct spacing
Context: ...tential, ... Visualize the results. python from weac.analysis.plotter import Plotter plotter = Plotter() # Visualize slab profile fig = plotter.plot_slab_profile( weak_layers=weak_layer, slabs=skier_system.slab, ) # Visualize deformations as a contour plot fig = plotter.plot_deformed( xsl_skier, xwl_skier, z_skier, skier_analyzer, scale=200, window=200, aspect=2, field="Sxx" ) # Plot slab displacements (using x-coordinates of all segments, xsl) plotter.plot_displacements(skier_analyzer, x=xsl_skier, z=z_skier) # Plot weak-layer stresses (using only x-coordinates of bedded segments, xwl) plotter.plot_stresses(skier_analyzer, x=xwl_skier, z=z_skier) ``` Compute output/field quantities for expo...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~260-~260: Use correct spacing
Context: ...ld quantities for exporting or plotting. python # Compute stresses in kPa in the weaklayer tau = skier_system.fq.tau(Z=z_skier, unit='kPa') sig = skier_system.fq.sig(Z=z_skier, unit='kPa') w = skier_system.fq.w(Z=z_skier, unit='um') # Example evaluation vertical displacement at top/mid/bottom of the slab u_top = skier_system.fq.u(Z=z_skier, h0=top, unit='um') u_mid = skier_system.fq.u(Z=z_skier, h0=mid, unit='um') u_bot = skier_system.fq.u(Z=z_skier, h0=bot, unit='um') psi = skier_system.fq.psi(Z=z_skier, unit='deg') ## Roadmap See the [open issues](https://g...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~276-~276: Use correct spacing
Context: ...='deg') ``` ## Roadmap See the [open issues](https://github.com...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~278-~278: Use correct spacing
Context: ...t of proposed features and known issues. ### v4.0 - [] Change to scenario & scenario...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~280-~280: Use correct spacing
Context: ...sed features and known issues. ### v4.0 - [] Change to scenario & scenario_config:...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~282-~282: Use correct spacing
Context: ...enario_config: InfEnd/Cut/Segment/Weight ### v3.2 <!-- - [ ] New mathematical foundat...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~284-~284: Use correct spacing
Context: ...fig: InfEnd/Cut/Segment/Weight ### v3.2 - [ ] Complex terrain through the addition...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~286-~286: There might be a mistake here.
Context: ...hrough the addition of out-of-plane tilt - [ ] Up, down, and cross-slope cracks ##...

(QB_NEW_EN)


[grammar] ~287-~287: Use correct spacing
Context: ...t - [ ] Up, down, and cross-slope cracks ### v3.1 - [ ] Improved CriteriaEvaluator O...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~289-~289: Use correct spacing
Context: ..., down, and cross-slope cracks ### v3.1 - [ ] Improved CriteriaEvaluator Optimizat...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~291-~291: Use correct spacing
Context: ...aluator Optimization (x2 time reduction) ## Release history ### v3.0 - Code Refact...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~293-~293: Use correct spacing
Context: ... (x2 time reduction) ## Release history ### v3.0 - Code Refactor - Input Validation...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~295-~295: Use correct spacing
Context: ...reduction) ## Release history ### v3.0 - Code Refactor - Input Validation - Modul...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~297-~297: There might be a mistake here.
Context: ...lease history ### v3.0 - Code Refactor - Input Validation - Modular + Object-Orie...

(QB_NEW_EN)


[grammar] ~298-~298: There might be a mistake here.
Context: ...v3.0 - Code Refactor - Input Validation - Modular + Object-Oriented ### v2.6 - F...

(QB_NEW_EN)


[grammar] ~299-~299: Use correct spacing
Context: ...t Validation - Modular + Object-Oriented ### v2.6 - Finite fracture mechanics implem...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~301-~301: Use correct spacing
Context: ...on - Modular + Object-Oriented ### v2.6 - Finite fracture mechanics implementation...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~305-~305: Use correct spacing
Context: ...tropic weak layer (?) - Add demo gif (?) ### v2.5 - Analyze slab touchdown in PST ex...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~307-~307: Use correct spacing
Context: ...k layer (?) - Add demo gif (?) ### v2.5 - Analyze slab touchdown in PST experiment...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~310-~310: There might be a mistake here.
Context: ...significantly improved API documentation ### v2.4 - Choose between slope-normal (`'-...

(QB_NEW_EN_OTHER)


[grammar] ~312-~312: Use correct spacing
Context: ...tly improved API documentation ### v2.4 - Choose between slope-normal ('-pst', `...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~314-~314: There might be a mistake here.
Context: ...st', 'vpst-'`) PST boundary conditions ### v2.3 - Stress plots on deformed contour...

(QB_NEW_EN_OTHER)


[grammar] ~316-~316: Use correct spacing
Context: ...st-'`) PST boundary conditions ### v2.3 - Stress plots on deformed contours - PSTs...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~318-~318: There might be a mistake here.
Context: ...s ### v2.3 - Stress plots on deformed contours - PSTs now account for slab touchdown ###...

(QB_NEW_EN_OTHER)


[grammar] ~319-~319: There might be a mistake here.
Context: ...rs - PSTs now account for slab touchdown ### v2.2 - Sign of inclination phi consis...

(QB_NEW_EN_OTHER)


[grammar] ~321-~321: Use correct spacing
Context: ...now account for slab touchdown ### v2.2 - Sign of inclination phi consistent wit...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~333-~333: There might be a mistake here.
Context: ...t use of coordinate system with downward pointing z-axis - Consitent top-to-botto...

(QB_NEW_EN_OTHER)


[grammar] ~333-~333: Ensure spelling is correct
Context: ...ate system with downward pointing z-axis - Consitent top-to-bottom numbering of slab layers ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~334-~334: There might be a mistake here.
Context: ...t top-to-bottom numbering of slab layers - Implementation of PSTs cut from either l...

(QB_NEW_EN)


[grammar] ~335-~335: There might be a mistake here.
Context: ...ayers - Implementation of PSTs cut from either left or right side ### v2.0 - Complet...

(QB_NEW_EN)


[grammar] ~335-~335: Use correct spacing
Context: ... PSTs cut from either left or right side ### v2.0 - Completely rewritten in 🐍 Pytho...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~337-~337: Use correct spacing
Context: ...from either left or right side ### v2.0 - Completely rewritten in 🐍 Python - Coup...

(QB_NEW_EN_OTHER_ERROR_IDS_5)


[grammar] ~362-~362: There might be a mistake here.
Context: ...ork the project 2. Initialize submodules bash git submodule update --init --recursive 3. Create your feature branch (`git checkou...

(QB_NEW_EN_OTHER)


[grammar] ~368-~368: There might be a mistake here.
Context: ...git checkout -b feature/amazingfeature) 4. Commit your changes (git commit -m 'Add...

(QB_NEW_EN_OTHER)


[style] ~369-~369: Consider using a more formal and expressive alternative to ‘amazing’.
Context: ...mazingfeature) 4. Commit your changes (git commit -m 'Add some amazing feature') 5. Push to the branch (git ...

(AWESOME)


[grammar] ~369-~369: There might be a mistake here.
Context: ...t commit -m 'Add some amazing feature') 5. Push to the branch (git push origin fea...

(QB_NEW_EN_OTHER)


[grammar] ~371-~371: There might be a mistake here.
Context: .../amazingfeature`) 6. Open a pull request ## Workflows [![Publish Python 🐍 releases ...

(QB_NEW_EN_OTHER)

🔇 Additional comments (7)
README.md (5)

122-130: Good: Runtime dependencies now align with pyproject/CI (Python ≥ 3.12).

The dependency block looks correct and matches the refactor direction. Thanks for adding the surrounding blank lines.


230-234: Good: Analyzer now receives the correct model variable and typo fixed.

This addresses prior feedback; the example should run as shown.


245-247: Good: Consistent variable names in Plotter usage.

weak_layers=weak_layer and slabs=skier_system.slab are now consistent with earlier definitions.


304-304: Good: “anisotropic” spelling corrected.

Resolved prior typo.


314-314: Good: “vertical” spelling corrected.

Resolved prior typo in PST boundary conditions section.

.github/workflows/tests.yml (2)

20-27: Nice use of setup-python with caching

Using cache: 'pip' and check-latest is solid. This will speed up runs and keep 3.12 fresh.


33-34: Tests correctly fail the CI on test failures

Confirmed that tests/run_tests.py invokes:

if __name__ == "__main__":
    unittest_result = run_tests()
    sys.exit(0 if unittest_result.wasSuccessful() else 1)

No changes required.

Comment on lines 13 to 16
jobs:
test:
name: Run unit tests 🤖
runs-on: ubuntu-latest
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Add a job timeout to prevent stuck runs consuming minutes

Guardrail to auto-cancel if tests hang.

Apply:

   test:
     name: Run unit tests 🤖
     runs-on: ubuntu-latest
+    timeout-minutes: 20
📝 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
jobs:
test:
name: Run unit tests 🤖
runs-on: ubuntu-latest
jobs:
test:
name: Run unit tests 🤖
runs-on: ubuntu-latest
timeout-minutes: 20
🤖 Prompt for AI Agents
In .github/workflows/tests.yml around lines 13 to 16, the test job lacks a
timeout and can hang indefinitely; add a job timeout by including a
timeout-minutes field under the job definition (for example timeout-minutes: 20
or another project-appropriate value) to ensure GitHub Actions cancels stuck
runs automatically.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
.github/workflows/pylint.yml (3)

3-10: Good fix: quoted “on” key; triggers look correct.
Addresses the previous yamllint truthy warning and keeps the intended triggers.


30-68: Fix: lint targets don’t match repo reorg (weac_2/tests_2) and grep can fail the step.

  • The PR moved code/tests to weac_2/tests_2. Current command still targets weac/ tests/ and will either do nothing useful or trigger a usage error (32).
  • Several grep lines can return non-zero when there are zero matches or when the report file is empty/missing, causing the step to fail under bash -e.

Refactor to dynamically select existing targets and guard summaries.

       - name: Run pylint analysis
         # Using repository pylint config (pyproject.toml) with comprehensive settings for scientific code
         run: |
-          exit_code=0
-          python -m pylint --rcfile=pyproject.toml --output-format=parseable --output=pylint-report.txt weac/ tests/ || exit_code=$?
+          set +e  # we'll manage exit codes manually
+          exit_code=0
+          : > pylint-report.txt  # ensure file exists even if pylint errors early
+
+          # Build target list compatible with repo reorg (prefer new dirs, fall back to legacy)
+          TARGET_DIRS=""
+          for d in weac_2 weac tests_2 tests; do
+            [ -d "$d" ] && TARGET_DIRS="$TARGET_DIRS $d"
+          done
+          if [ -z "$TARGET_DIRS" ]; then
+            echo "No target directories found for pylint (checked: weac_2, weac, tests_2, tests)."
+            echo "Skipping pylint run."
+            exit 0
+          fi
+
+          python -m pylint --output-format=parseable --output=pylint-report.txt $TARGET_DIRS || exit_code=$?
           echo "Pylint finished with exit code $exit_code."
           echo
           echo "Pylint exit code meaning:"
           if [ $exit_code -eq 0 ]; then echo "-> No issues found"; fi
           if [ $((exit_code & 1)) -ne 0 ]; then echo "-> Fatal message issued"; fi
           if [ $((exit_code & 2)) -ne 0 ]; then echo "-> Error message issued"; fi
           if [ $((exit_code & 4)) -ne 0 ]; then echo "-> Warning message issued"; fi
           if [ $((exit_code & 8)) -ne 0 ]; then echo "-> Refactor message issued"; fi
           if [ $((exit_code & 16)) -ne 0 ]; then echo "-> Convention message issued"; fi
           if [ $((exit_code & 32)) -ne 0 ]; then echo "-> Usage error"; fi
           echo
 
           echo 'Error type counts:'
-          grep -oP '[A-Z]\d+\([a-z\-]+\)' pylint-report.txt | sort | uniq -c | sort -nr
+          grep -oP '[A-Z]\d+\([a-z\-]+\)' pylint-report.txt | sort | uniq -c | sort -nr || true
           echo
           echo 'Errors per file:'
-          grep -oP '^[\w\-\/]+\.py' pylint-report.txt | sort | uniq -c | sort -nr
+          grep -oP '^[\w\-\/]+\.py' pylint-report.txt | sort | uniq -c | sort -nr || true
           echo
           echo 'Total errors:'
-          grep -oP '^[\w\-\/]+\.py' pylint-report.txt | wc -l
+          grep -oP '^[\w\-\/]+\.py' pylint-report.txt | wc -l || true
           echo
           grep 'Your code' pylint-report.txt || true
 
           # Fail on fatal, error, and usage error.
           # These are severe and should block PRs.
           # Warnings (4), refactors (8), and conventions (16) will not cause a failure.
           fail_on_codes=$((1 | 2 | 32))
           if [ $((exit_code & fail_on_codes)) -ne 0 ]; then
             echo "Failing CI due to fatal/error/usage messages from pylint."
             exit 1
           else
             echo "Pylint check passed. No fatal/error/usage messages."
           fi

Notes:

  • If your pylint config lives in pyproject.toml, you can drop the explicit --rcfile flag; pylint auto-detects pyproject. Keep it only if you’ve verified it’s needed.

12-15: Add least-privilege permissions to harden the workflow.
Explicitly scope the token to read-only. Top-level preferred so all jobs inherit it.

 "on":
   pull_request:
     types: [opened, reopened, synchronize, ready_for_review]
   workflow_call:
   workflow_dispatch:
 
+permissions:
+  contents: read
+
 jobs:
   pylint:
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4f4b33a and 08378f4.

📒 Files selected for processing (1)
  • .github/workflows/pylint.yml (1 hunks)

Comment on lines 19 to 24
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Optional: improve pip cache effectiveness with dependency path hints.
actions/setup-python can hash your requirements to invalidate cache correctly.

       - name: Set up Python 3.12
         uses: actions/setup-python@v5
         with:
           python-version: "3.12"
           cache: "pip"
+          cache-dependency-path: |
+            requirements.txt
+            pyproject.toml
+            setup.cfg
+            setup.py
+            environment.yml
📝 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
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: |
requirements.txt
pyproject.toml
setup.cfg
setup.py
environment.yml
🤖 Prompt for AI Agents
In .github/workflows/pylint.yml around lines 19 to 24, the workflow uses
actions/setup-python with cache: "pip" but does not provide
cache-dependency-path, so the cache won’t be invalidated when requirements
change; add the cache-dependency-path input pointing to your dependency files
(e.g. **/requirements.txt, **/pyproject.toml, **/setup.cfg or other files you
use) so the action will hash those files and invalidate the pip cache when they
change.

Comment on lines +47 to +56
echo 'Error type counts:'
grep -oP '[A-Z]\d+\([a-z\-]+\)' pylint-report.txt | sort | uniq -c | sort -nr
echo
echo 'Errors per file:'
grep -oP '^[\w\-\/]+\.py' pylint-report.txt | sort | uniq -c | sort -nr
echo
echo 'Total errors:'
grep -oP '^[\w\-\/]+\.py' pylint-report.txt | wc -l
echo
grep 'Your code' pylint-report.txt || true
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Optional: drop grep -P for portability.
GNU grep -P is fine on ubuntu-latest, but can be disabled elsewhere. Consider awk/sed for portability if you reuse locally.

Example replacement for the three grep -oP pipelines:

  • Error types:
    awk '{ if (match($0, /[A-Z][0-9]+([a-z-]+)/, a)) print a[0] }' pylint-report.txt | sort | uniq -c | sort -nr || true
  • Per-file:
    awk -F: 'NF>1 && $1 ~ /.py$/ {print $1}' pylint-report.txt | sort | uniq -c | sort -nr || true
  • Total errors:
    awk -F: 'NF>1 && $1 ~ /.py$/' pylint-report.txt | wc -l || true
🤖 Prompt for AI Agents
In .github/workflows/pylint.yml around lines 47 to 56 the workflow uses grep -P
(Perl regex) which is not portable; replace the three grep -oP pipelines with
POSIX-safe awk (or sed) equivalents that extract the same fields (error type
tokens, filenames, and count of errors), preserve the same sorting/uniq logic
and ensure the pipeline exits zero on no matches (append || true where needed),
and keep the surrounding echo lines and output ordering unchanged.

@cursor
Copy link

cursor bot commented Aug 19, 2025

🚨 Bugbot Trial Expired

Your Bugbot trial has expired. Please purchase a license in the Cursor dashboard to continue using Bugbot.

@zacczakk zacczakk merged commit 1bb9acd into main Aug 19, 2025
6 checks passed
@zacczakk zacczakk deleted the feat/researcher-focus branch August 19, 2025 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants