diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..102ae9e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug Report +about: Report a bug to help us improve +title: "[BUG] " +labels: bug +assignees: "" +--- + +## Description + +A clear and concise description of the bug. + +## Steps to Reproduce + +1. ... +2. ... +3. ... + +## Expected Behavior + +What you expected to happen. + +## Actual Behavior + +What actually happened. Include error messages and tracebacks if applicable. + +## Environment + +- OS: [e.g., macOS 14, Ubuntu 22.04] +- Python version: [e.g., 3.11.5] +- steer-materials version: [e.g., 0.2.11] + +## Additional Context + +Any other context about the problem. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..792fe22 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature Request +about: Suggest an idea for this project +title: "[FEATURE] " +labels: enhancement +assignees: "" +--- + +## Problem + +A clear description of the problem or use case this feature would address. + +## Proposed Solution + +A clear description of what you'd like to happen. + +## Alternatives Considered + +Any alternative solutions or features you've considered. + +## Additional Context + +Any other context, references, or screenshots about the feature request. diff --git a/.gitignore b/.gitignore index 38f04dd..a27efb4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ scripts/ dist/ Archive/ app_builds/ +.vscode/ +.venv/ +.pytest_cache/ diff --git a/CITATION.cff b/CITATION.cff index cb8ab79..8f2288b 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -12,9 +12,9 @@ authors: orcid: https://orcid.org/0000-0002-0755-3981 repository-code: https://github.com/stanford-developers/steer-materials license: AGPL-3.0-or-later -version: 0.1.39 +version: "0.1.44" doi: -date-released: '2026-03-17' +date-released: '2026-06-10' keywords: - battery - materials diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a86cd72..55fc7bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,4 +88,4 @@ This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT. ## License -By contributing, you agree that your contributions will be licensed under the [AGPL-3.0](LICENCE.txt) license and, per the CLA, may also be distributed under the project's commercial license. +By contributing, you agree that your contributions will be licensed under the [AGPL-3.0](LICENSE) license and, per the CLA, may also be distributed under the project's commercial license. diff --git a/LICENCE.txt b/LICENSE similarity index 99% rename from LICENCE.txt rename to LICENSE index be745ec..a52c571 100644 --- a/LICENCE.txt +++ b/LICENSE @@ -1,6 +1,6 @@ STEER Materials - Dual Licensed -Copyright (c) 2024-2026 Nicholas Siemons +Copyright (c) 2024-2026 Stanford University 1. Open-source license: GNU Affero General Public License v3.0 (AGPL-3.0) See the full AGPL-3.0 license text below. diff --git a/README.md b/README.md index 4151c2d..52f46a2 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ pytest --cov=steer_materials ## Documentation -Full documentation for the STEER ecosystem is available at the [STEER documentation site](https://github.com/stanford-developers). +Full documentation for this package is available at [stanford-developers.github.io/steer-materials](https://stanford-developers.github.io/steer-materials/). ## Contributing @@ -69,4 +69,4 @@ This project is dual-licensed: 1. **Open source** — [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0) (AGPL-3.0-or-later) 2. **Commercial** — A separate commercial license is available for use without AGPL-3.0 copyleft requirements. Contact [nsiemons@stanford.edu](mailto:nsiemons@stanford.edu) for details. -See [LICENCE.txt](LICENCE.txt) for full terms. \ No newline at end of file +See [LICENSE](LICENSE) for full terms. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5f45117 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security Policy + +## Supported Versions + +Only the latest released version of steer-materials receives security updates. + +## Reporting a Vulnerability + +Please do **not** open a public GitHub issue for security vulnerabilities. + +Instead, report them privately by emailing [nsiemons@stanford.edu](mailto:nsiemons@stanford.edu) +with a description of the issue, steps to reproduce, and any relevant details. + +You should receive an acknowledgement within a few business days. Once the issue +is confirmed and a fix is available, we will coordinate disclosure and credit +you in the release notes unless you prefer to remain anonymous. diff --git a/docs/developer.md b/docs/developer.md index 3ba012d..8245068 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -51,7 +51,7 @@ steer-materials/ ├── docs/ # MkDocs documentation (this site) ├── .github/workflows/ # CI: tests + linting ├── pyproject.toml # Build config, dependencies, tool settings -├── LICENCE.txt # Dual license (AGPL-3.0 + commercial) +├── LICENSE # Dual license (AGPL-3.0 + commercial) ├── CITATION.cff # Citation metadata ├── CONTRIBUTING.md # Contribution guidelines └── CODE_OF_CONDUCT.md # Contributor Covenant v2.1 diff --git a/pyproject.toml b/pyproject.toml index 1c56e67..dc798fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "steer-materials" dynamic = ["version"] -description = "Modelling energy storage from cell to site - STEER OpenCell Design" +description = "Base material classes for the STEER OpenCell platform." readme = "README.md" requires-python = ">=3.10" license = {text = "AGPL-3.0-or-later"} @@ -31,7 +31,7 @@ classifiers = [ ] dependencies = [ - "steer-core==0.2.12", + "steer-core==0.2.18", ] [project.optional-dependencies] @@ -58,7 +58,7 @@ docs = [ Homepage = "https://github.com/stanford-developers/steer-materials/" Repository = "https://github.com/stanford-developers/steer-materials/" "Bug Tracker" = "https://github.com/stanford-developers/steer-materials/issues" -Documentation = "https://github.com/stanford-developers" +Documentation = "https://stanford-developers.github.io/steer-materials/" [tool.setuptools] package-dir = {"" = "."} diff --git a/steer_materials/Base.py b/steer_materials/Base.py index 3929dd6..8efc5bd 100644 --- a/steer_materials/Base.py +++ b/steer_materials/Base.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2024-2026 Nicholas Siemons +# SPDX-FileCopyrightText: 2024-2026 Stanford University # SPDX-License-Identifier: AGPL-3.0-or-later from steer_core.Constants.Units import * @@ -9,8 +9,6 @@ from datetime import datetime as dt -import numpy as np - class _Material( ValidationMixin, @@ -64,14 +62,14 @@ def _update_ranges(self): @property def density(self) -> float: """Density of the material in g/cm³.""" - return np.round(self._density * (KG_TO_G / M_TO_CM**3), 2) + return self._density * (KG_TO_G / M_TO_CM**3) @property def density_range(self) -> tuple[float, float]: """Soft range for density (±10% of current value) in g/cm³.""" return ( - np.round(self._density_range[0] * (KG_TO_G / M_TO_CM**3), 2), - np.round(self._density_range[1] * (KG_TO_G / M_TO_CM**3), 2), + self._density_range[0] * (KG_TO_G / M_TO_CM**3), + self._density_range[1] * (KG_TO_G / M_TO_CM**3), ) @property @@ -82,14 +80,14 @@ def density_hard_range(self): @property def specific_cost(self): """Cost per kilogram in $/kg.""" - return np.round(self._specific_cost, 2) + return self._specific_cost @property def specific_cost_range(self) -> tuple[float, float]: """Soft range for specific cost (0.5x–2x current value) in $/kg.""" return ( - np.round(self._specific_cost_range[0], 2), - np.round(self._specific_cost_range[1], 2), + self._specific_cost_range[0], + self._specific_cost_range[1], ) @property @@ -273,7 +271,7 @@ def __init__( def volume(self) -> float | None: """Volume in cm³, or `None` if not set.""" if hasattr(self, '_volume') and self._volume is not None: - return np.round(self._volume * (M_TO_CM**3), 2) + return self._volume * (M_TO_CM**3) else: return None @@ -281,7 +279,7 @@ def volume(self) -> float | None: def mass(self) -> float | None: """Mass in grams, or `None` if not set.""" if hasattr(self, '_mass') and self._mass is not None: - return np.round(self._mass * KG_TO_G, 2) + return self._mass * KG_TO_G else: return None @@ -289,7 +287,7 @@ def mass(self) -> float | None: def cost(self) -> float | None: """Cost in dollars, or `None` if not set.""" if hasattr(self, '_cost') and self._cost is not None: - return np.round(self._cost, 2) + return self._cost else: return None diff --git a/steer_materials/__init__.py b/steer_materials/__init__.py index 8c77233..f59e3ef 100644 --- a/steer_materials/__init__.py +++ b/steer_materials/__init__.py @@ -1,5 +1,5 @@ -# SPDX-FileCopyrightText: 2024-2026 Nicholas Siemons +# SPDX-FileCopyrightText: 2024-2026 Stanford University # SPDX-License-Identifier: AGPL-3.0-or-later -__version__ = "0.1.39" +__version__ = "0.1.44" diff --git a/test/test_material.py b/test/test_material.py index 370704c..8d7a330 100644 --- a/test/test_material.py +++ b/test/test_material.py @@ -1,5 +1,4 @@ import pytest -import numpy as np from steer_materials.Base import _Material, Metal, Solvent @@ -8,14 +7,14 @@ class TestMaterialConstruction: def test_metal_valid_construction(self, aluminum): assert aluminum.name == "Aluminum" - assert aluminum.density == 2.7 - assert aluminum.specific_cost == 2.50 + assert aluminum.density == pytest.approx(2.7, rel=1e-10) + assert aluminum.specific_cost == pytest.approx(2.50, rel=1e-10) assert aluminum.color == "silver" def test_solvent_valid_construction(self, water): assert water.name == "Water" - assert water.density == 1.0 - assert water.specific_cost == 0.01 + assert water.density == pytest.approx(1.0, rel=1e-10) + assert water.specific_cost == pytest.approx(0.01, rel=1e-10) assert water.color == "clear" def test_negative_density_raises(self): @@ -54,11 +53,11 @@ class TestMaterialProperties: def test_density_returns_g_per_cm3(self): # Input 2.7 g/cm³ should come back as 2.7 m = Metal(name="Al", density=2.7, specific_cost=1.0, color="grey") - assert m.density == 2.7 + assert m.density == pytest.approx(2.7, rel=1e-10) def test_specific_cost_returns_per_kg(self): m = Metal(name="Al", density=2.7, specific_cost=3.45, color="grey") - assert m.specific_cost == 3.45 + assert m.specific_cost == pytest.approx(3.45, rel=1e-10) def test_density_round_trip(self): # Verify internal storage → getter round-trips correctly @@ -78,11 +77,11 @@ class TestMaterialSetters: def test_set_density(self, aluminum): aluminum.density = 5.0 - assert aluminum.density == 5.0 + assert aluminum.density == pytest.approx(5.0, rel=1e-10) def test_set_specific_cost(self, aluminum): aluminum.specific_cost = 10.0 - assert aluminum.specific_cost == 10.0 + assert aluminum.specific_cost == pytest.approx(10.0, rel=1e-10) def test_set_name(self, aluminum): aluminum.name = "Steel" diff --git a/test/test_volumed_material.py b/test/test_volumed_material.py index 0ba35f4..8d8ade8 100644 --- a/test/test_volumed_material.py +++ b/test/test_volumed_material.py @@ -34,14 +34,14 @@ class TestVolumedMaterialInit: """Tests for _VolumedMaterialMixin initialization.""" def test_volume_init_derives_mass_and_cost(self, volumed_al): - assert volumed_al.volume == 100.0 - assert volumed_al.mass == 270.0 - assert volumed_al.cost == 0.68 + assert volumed_al.volume == pytest.approx(100.0, rel=1e-10) + assert volumed_al.mass == pytest.approx(270.0, rel=1e-10) + assert volumed_al.cost == pytest.approx(0.675, abs=1e-5) def test_mass_init_derives_volume_and_cost(self, mass_al): - assert mass_al.volume == 100.0 - assert mass_al.mass == 270.0 - assert mass_al.cost == 0.68 + assert mass_al.volume == pytest.approx(100.0, rel=1e-10) + assert mass_al.mass == pytest.approx(270.0, rel=1e-10) + assert mass_al.cost == pytest.approx(0.675, abs=1e-5) def test_no_volume_or_mass_gives_none(self, bare_al): assert bare_al.volume is None @@ -65,15 +65,15 @@ class TestVolumedMaterialSetters: def test_set_volume(self, bare_al): bare_al.volume = 100.0 - assert bare_al.volume == 100.0 - assert bare_al.mass == 270.0 - assert bare_al.cost == 0.68 + assert bare_al.volume == pytest.approx(100.0, rel=1e-10) + assert bare_al.mass == pytest.approx(270.0, rel=1e-10) + assert bare_al.cost == pytest.approx(0.675, abs=1e-5) def test_set_mass(self, bare_al): bare_al.mass = 270.0 - assert bare_al.volume == 100.0 - assert bare_al.mass == 270.0 - assert bare_al.cost == 0.68 + assert bare_al.volume == pytest.approx(100.0, rel=1e-10) + assert bare_al.mass == pytest.approx(270.0, rel=1e-10) + assert bare_al.cost == pytest.approx(0.675, abs=1e-5) def test_set_volume_to_none_clears_all(self, volumed_al): volumed_al.volume = None @@ -110,16 +110,16 @@ class TestVolumedMaterialPropagation: def test_density_change_updates_mass_and_cost(self, volumed_al): # volume=100 cm³, density 2.7→5.0 g/cm³ volumed_al.density = 5.0 - assert volumed_al.volume == 100.0 - assert volumed_al.mass == 500.0 - assert volumed_al.cost == 1.25 + assert volumed_al.volume == pytest.approx(100.0, rel=1e-10) + assert volumed_al.mass == pytest.approx(500.0, rel=1e-10) + assert volumed_al.cost == pytest.approx(1.25, rel=1e-10) def test_specific_cost_change_updates_cost(self, volumed_al): # mass=270g, specific_cost 2.50→5.0 $/kg volumed_al.specific_cost = 5.0 - assert volumed_al.volume == 100.0 - assert volumed_al.mass == 270.0 - assert volumed_al.cost == 1.35 + assert volumed_al.volume == pytest.approx(100.0, rel=1e-10) + assert volumed_al.mass == pytest.approx(270.0, rel=1e-10) + assert volumed_al.cost == pytest.approx(1.35, rel=1e-10) def test_density_change_no_effect_without_volume(self, bare_al): bare_al.density = 5.0