timoshenko-engine 2.0.1__tar.gz → 2.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. timoshenko_engine-2.0.3/PKG-INFO +257 -0
  2. timoshenko_engine-2.0.3/README.md +219 -0
  3. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/pyproject.toml +29 -3
  4. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/__init__.py +1 -1
  5. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/adapters.py +2 -2
  6. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/modal.py +3 -4
  7. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/multichannel.py +3 -3
  8. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/oma.py +1 -1
  9. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/project.py +1 -0
  10. timoshenko_engine-2.0.3/src/timoshenko/py.typed +0 -0
  11. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/sections.py +0 -1
  12. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/sensors.py +1 -1
  13. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/session.py +7 -5
  14. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/storage.py +4 -3
  15. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/uncertainty.py +4 -4
  16. timoshenko_engine-2.0.3/src/timoshenko_engine.egg-info/PKG-INFO +257 -0
  17. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko_engine.egg-info/SOURCES.txt +1 -0
  18. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko_engine.egg-info/requires.txt +5 -0
  19. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/tests/test_package.py +20 -0
  20. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/tests/test_project_report.py +16 -1
  21. timoshenko_engine-2.0.1/PKG-INFO +0 -237
  22. timoshenko_engine-2.0.1/README.md +0 -205
  23. timoshenko_engine-2.0.1/src/timoshenko_engine.egg-info/PKG-INFO +0 -237
  24. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/LICENSE +0 -0
  25. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/setup.cfg +0 -0
  26. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/_validation.py +0 -0
  27. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/assets.py +0 -0
  28. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/beams.py +0 -0
  29. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/csv_source.py +0 -0
  30. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/health.py +0 -0
  31. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/mechanics.py +0 -0
  32. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/monitor.py +0 -0
  33. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/mqtt.py +0 -0
  34. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/observations.py +0 -0
  35. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/plugins.py +0 -0
  36. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/polygon.py +0 -0
  37. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/pressure.py +0 -0
  38. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/report.py +0 -0
  39. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/sensorthings.py +0 -0
  40. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/shafts.py +0 -0
  41. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/stability.py +0 -0
  42. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/strength.py +0 -0
  43. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/structure.py +0 -0
  44. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/update.py +0 -0
  45. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko/vibration.py +0 -0
  46. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko_engine.egg-info/dependency_links.txt +0 -0
  47. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko_engine.egg-info/entry_points.txt +0 -0
  48. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/src/timoshenko_engine.egg-info/top_level.txt +0 -0
  49. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/tests/test_adapters.py +0 -0
  50. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/tests/test_formulas.py +0 -0
  51. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/tests/test_modal.py +0 -0
  52. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/tests/test_polygon.py +0 -0
  53. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/tests/test_storage_session.py +0 -0
  54. {timoshenko_engine-2.0.1 → timoshenko_engine-2.0.3}/tests/test_uncertainty.py +0 -0
@@ -0,0 +1,257 @@
1
+ Metadata-Version: 2.4
2
+ Name: timoshenko-engine
3
+ Version: 2.0.3
4
+ Summary: Composable structural engineering calculations and analysis primitives for Python applications.
5
+ Author: Ayberk Korkmaz
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/Ayberkrk/timoshenko
8
+ Project-URL: Repository, https://github.com/Ayberkrk/timoshenko
9
+ Project-URL: Issues, https://github.com/Ayberkrk/timoshenko/issues
10
+ Project-URL: Documentation, https://timoshenko.readthedocs.io/
11
+ Project-URL: Changelog, https://github.com/Ayberkrk/timoshenko/blob/main/docs/changelog.md
12
+ Keywords: civil engineering,structural engineering,structural health monitoring,modal analysis,digital twin
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: numpy>=1.24
27
+ Provides-Extra: mqtt
28
+ Requires-Dist: paho-mqtt<3,>=2.1; extra == "mqtt"
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest>=7; extra == "test"
31
+ Requires-Dist: pytest-cov>=5; extra == "test"
32
+ Provides-Extra: lint
33
+ Requires-Dist: ruff>=0.6; extra == "lint"
34
+ Requires-Dist: mypy>=1.10; extra == "lint"
35
+ Provides-Extra: docs
36
+ Requires-Dist: mkdocs-material<10,>=9.5; extra == "docs"
37
+ Dynamic: license-file
38
+
39
+ <p align="center">
40
+ <img src="https://raw.githubusercontent.com/Ayberkrk/timoshenko/main/assets/timoshenko-logo.png" alt="Timoshenko Engine logo" width="920">
41
+ </p>
42
+
43
+ <h1 align="center">Timoshenko Engine</h1>
44
+
45
+ <p align="center">
46
+ Reusable structural engineering building blocks for Python applications.
47
+ </p>
48
+
49
+ <p align="center">
50
+ <a href="https://pypi.org/project/timoshenko-engine/"><img alt="PyPI" src="https://img.shields.io/pypi/v/timoshenko-engine?style=for-the-badge"></a>
51
+ <img alt="Python versions" src="https://img.shields.io/pypi/pyversions/timoshenko-engine?style=for-the-badge">
52
+ <img alt="Alpha" src="https://img.shields.io/badge/stage-alpha-orange?style=for-the-badge">
53
+ <img alt="Apache 2.0 license" src="https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge">
54
+ <a href="https://doi.org/10.5281/zenodo.22968739"><img alt="DOI" src="https://img.shields.io/badge/DOI-10.5281%2Fzenodo.22968739-blue?style=for-the-badge"></a>
55
+ <a href="https://github.com/Ayberkrk/timoshenko/actions/workflows/tests.yml"><img alt="Tests" src="https://github.com/Ayberkrk/timoshenko/actions/workflows/tests.yml/badge.svg"></a>
56
+ <a href="https://github.com/Ayberkrk/timoshenko/tree/python-coverage-comment-action-data"><img alt="Coverage" src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FAyberkrk%2Ftimoshenko%2Fpython-coverage-comment-action-data%2Fendpoint.json&style=for-the-badge"></a>
57
+ <a href="https://timoshenko.readthedocs.io/en/latest/"><img alt="Documentation" src="https://img.shields.io/readthedocs/timoshenko?style=for-the-badge"></a>
58
+ </p>
59
+
60
+ Timoshenko packages common structural calculations, modal analysis, sensor
61
+ workflows, and monitoring components so applications can reuse them instead
62
+ of rebuilding the same foundations for every project. It is an embeddable
63
+ Python library, not a hosted monitoring service or a general finite-element
64
+ solver.
65
+
66
+ > **Alpha:** the API may still change between releases. Every behavior change
67
+ > is listed in the [changelog](https://timoshenko.readthedocs.io/en/latest/changelog/).
68
+
69
+ ## Install
70
+
71
+ ```bash
72
+ python -m pip install timoshenko-engine
73
+ ```
74
+
75
+ The distribution is named `timoshenko-engine`; the import package is
76
+ `timoshenko`. Optional MQTT support is an extra:
77
+
78
+ ```bash
79
+ python -m pip install "timoshenko-engine[mqtt]"
80
+ ```
81
+
82
+ ## Quick start
83
+
84
+ Compare measured vibration with a reference model. This example simulates a
85
+ record in which both modes are 5% below the model:
86
+
87
+ ```python
88
+ import numpy as np
89
+ import timoshenko as tm
90
+
91
+ structure = tm.Structure(
92
+ structure_id="building-01",
93
+ story_masses_kg=[120_000.0, 110_000.0],
94
+ story_stiffness_n_m=[85_000_000.0, 70_000_000.0],
95
+ )
96
+ print(structure.natural_frequencies_hz) # (2.626, 6.476)
97
+
98
+ fs = 100.0
99
+ t = np.arange(60_000) / fs
100
+ f1, f2 = (0.95 * f for f in structure.natural_frequencies_hz)
101
+ signal = np.sin(2 * np.pi * f1 * t) + 0.4 * np.sin(2 * np.pi * f2 * t)
102
+ sensors = tm.SensorData(signal, sampling_hz=fs, unit="m/s^2")
103
+
104
+ result = tm.monitor(structure, sensors, review_threshold_pct=3.0)
105
+ print(result.structure.update_scale_factor) # 0.903, since stiffness scales with frequency squared
106
+ for change in result.health.mode_changes:
107
+ print(change.mode_number, change.change_pct) # 1 -5.0, then 2 -5.0
108
+ print(result.health.review_recommended) # True
109
+ ```
110
+
111
+ <p align="center">
112
+ <img src="https://raw.githubusercontent.com/Ayberkrk/timoshenko/main/assets/modal-comparison.svg" alt="Both identified modes plot 5% below the reference model, past the 3% review threshold" width="800">
113
+ </p>
114
+
115
+ Real records load from CSV or JSON with
116
+ `tm.load_sensors("acceleration.csv", sampling_hz=100.0, column="acc")`. The
117
+ steps inside `tm.monitor` are also available separately as `tm.modal.identify`,
118
+ `tm.update`, and `tm.health.assess`. The review flag is raised only when you
119
+ pass a threshold: a meaningful value depends on the structure and its
120
+ environmental variability, so the engine does not choose one.
121
+
122
+ ## Engineering calculations
123
+
124
+ Closed-form functions take and return SI units and validate their inputs:
125
+
126
+ ```python
127
+ import timoshenko as tm
128
+
129
+ section = tm.rectangle_section(width_m=0.3, height_m=0.6)
130
+ beam = tm.simply_supported_uniform_load(
131
+ 20_000.0, 6.0, 30e9, section.second_moment_y_m4,
132
+ shear_modulus_pa=12.5e9, area_m2=section.area_m2,
133
+ )
134
+ print(beam.bending_m, beam.shear_m) # 2.083 mm bending, 0.048 mm shear
135
+
136
+ frequency = tm.propagate_uncertainty(
137
+ tm.natural_frequency_hz,
138
+ {"mass_kg": 250.0, "stiffness_n_m": 4e5},
139
+ standard_uncertainties={"mass_kg": 5.0, "stiffness_n_m": 8e3},
140
+ )
141
+ print(frequency.estimate, frequency.standard_uncertainty) # 6.366 Hz ± 0.090 Hz
142
+ ```
143
+
144
+ ## What it provides
145
+
146
+ | Area | Components |
147
+ |---|---|
148
+ | Engineering calculations | Section properties (including polygons with holes), beam deflection with shear, torsion, Euler buckling, plane stress, thin-wall pressure, SDOF vibration, Rayleigh damping, and GUM / Monte Carlo uncertainty |
149
+ | Structural models | Lumped-mass shear-building models and their natural frequencies |
150
+ | Modal analysis | Single-channel peak picking with resolution-checked damping, and multi-channel FDD with complex mode shapes |
151
+ | Model comparison | Nearest-frequency mode pairing, global stiffness updating, and evidence-oriented health assessment |
152
+ | Monitoring | Bounded rolling-window sessions, restart from local history, and source adapters for CSV, MQTT, and OGC SensorThings |
153
+ | Data and reporting | Project manifests with SHA-256 provenance, local SQLite history, and standalone HTML reports |
154
+
155
+ Sensor collection, alarm policy, and engineering interpretation remain with the
156
+ application that embeds Timoshenko.
157
+
158
+ ## Multi-channel modal analysis
159
+
160
+ Frequency domain decomposition needs synchronized channels with a common
161
+ sample rate and unit:
162
+
163
+ ```python
164
+ signals = tm.load_multichannel_csv(
165
+ "aligned_accelerometers.csv",
166
+ columns=["deck_left", "deck_center", "deck_right"],
167
+ sampling_hz=100.0,
168
+ units=["m/s^2"] * 3,
169
+ )
170
+ fdd = tm.identify_fdd(signals, nperseg=1024, max_modes=5)
171
+ for mode in fdd.modes:
172
+ print(mode.frequency_hz, mode.shape_real)
173
+ ```
174
+
175
+ ## Monitoring sessions
176
+
177
+ A host application feeds timestamped observation batches. The session aligns
178
+ samples to the sample grid, waits for a fresh contiguous window after gaps, and
179
+ analyzes every configured hop:
180
+
181
+ ```python
182
+ session = tm.MonitoringSession(
183
+ structure,
184
+ sensor_ids=["deck-left", "deck-right"],
185
+ units=["m/s^2", "m/s^2"],
186
+ sampling_hz=100.0,
187
+ window_samples=2048,
188
+ hop_samples=512,
189
+ analysis_options={"nperseg": 512, "max_modes": 4},
190
+ review_threshold_pct=5.0,
191
+ )
192
+ result = session.ingest(batch) # batch: tm.ObservationBatch from your gateway or a source adapter
193
+ for report in result.reports:
194
+ tm.report.save_html(report, "reports/latest.html")
195
+ ```
196
+
197
+ The session does not open network connections or run in the background. Use
198
+ `tm.SessionRunner` with a source such as `tm.CSVObservationSource` or
199
+ `tm.MqttObservationSource` to drive it.
200
+
201
+ ## Documentation
202
+
203
+ The full documentation is at
204
+ [timoshenko.readthedocs.io](https://timoshenko.readthedocs.io/en/latest/). Good places to start:
205
+
206
+ - [API reference](https://timoshenko.readthedocs.io/en/latest/api/)
207
+ - [Numerical methods and limits](https://timoshenko.readthedocs.io/en/latest/numerical-methods/)
208
+ - [Architecture](https://timoshenko.readthedocs.io/en/latest/architecture/)
209
+ - [Monitoring sessions](https://timoshenko.readthedocs.io/en/latest/live-sessions/) and [source adapters](https://timoshenko.readthedocs.io/en/latest/adapters/)
210
+ - [Runnable examples](https://github.com/Ayberkrk/timoshenko/tree/main/examples), including a [cross-check of PyNite shear-deformable beams](https://timoshenko.readthedocs.io/en/latest/pynite-verification/)
211
+ - [Changelog](https://timoshenko.readthedocs.io/en/latest/changelog/)
212
+
213
+ ## Limits and engineering posture
214
+
215
+ - Timoshenko is alpha software and is not a structural safety certification tool.
216
+ - The shear-building model is a small lumped-mass reference model, not a full FEM solver.
217
+ - Modal identification is a screening estimate. A single sensor may miss a mode near a modal node; unpaired peaks are reported rather than compared with the wrong mode.
218
+ - Damping is a coarse screening estimate, reported only when the averaged spectrum resolves the half-power bandwidth. FDD does not estimate damping.
219
+ - The model update applies a single stiffness scale. It cannot locate or size local damage.
220
+ - A frequency shift is evidence for human review, not a damage verdict. Temperature, sensor placement, boundary conditions, and other effects also shift measured frequencies.
221
+ - Closed-form mechanics and section functions rely on their documented ideal assumptions. They are not code-compliance checks.
222
+
223
+ ## Development
224
+
225
+ ```bash
226
+ python -m pip install -e ".[test,lint]"
227
+ python -m pytest --cov
228
+ python -m ruff check .
229
+ python -m mypy
230
+ ```
231
+
232
+ To preview the documentation site:
233
+
234
+ ```bash
235
+ python -m pip install -e ".[docs]"
236
+ python -m mkdocs serve
237
+ ```
238
+
239
+ Bug reports and pull requests are welcome in the
240
+ [issue tracker](https://github.com/Ayberkrk/timoshenko/issues). See
241
+ [`CONTRIBUTING.md`](https://github.com/Ayberkrk/timoshenko/blob/main/CONTRIBUTING.md)
242
+ for the checks a pull request needs to pass; issues labelled
243
+ [good first issue](https://github.com/Ayberkrk/timoshenko/labels/good%20first%20issue)
244
+ are small, self-contained starting points.
245
+
246
+ ## Citation
247
+
248
+ If you use Timoshenko in research, please cite it with its DOI,
249
+ [10.5281/zenodo.22968739](https://doi.org/10.5281/zenodo.22968739), which
250
+ always resolves to the latest release; each release also has its own DOI on
251
+ [Zenodo](https://zenodo.org/records/22968740). The metadata is in
252
+ [`CITATION.cff`](https://github.com/Ayberkrk/timoshenko/blob/main/CITATION.cff),
253
+ which GitHub's "Cite this repository" button reads.
254
+
255
+ ## License
256
+
257
+ Apache License 2.0. See [LICENSE](https://github.com/Ayberkrk/timoshenko/blob/main/LICENSE).
@@ -0,0 +1,219 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/Ayberkrk/timoshenko/main/assets/timoshenko-logo.png" alt="Timoshenko Engine logo" width="920">
3
+ </p>
4
+
5
+ <h1 align="center">Timoshenko Engine</h1>
6
+
7
+ <p align="center">
8
+ Reusable structural engineering building blocks for Python applications.
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://pypi.org/project/timoshenko-engine/"><img alt="PyPI" src="https://img.shields.io/pypi/v/timoshenko-engine?style=for-the-badge"></a>
13
+ <img alt="Python versions" src="https://img.shields.io/pypi/pyversions/timoshenko-engine?style=for-the-badge">
14
+ <img alt="Alpha" src="https://img.shields.io/badge/stage-alpha-orange?style=for-the-badge">
15
+ <img alt="Apache 2.0 license" src="https://img.shields.io/badge/license-Apache--2.0-green?style=for-the-badge">
16
+ <a href="https://doi.org/10.5281/zenodo.22968739"><img alt="DOI" src="https://img.shields.io/badge/DOI-10.5281%2Fzenodo.22968739-blue?style=for-the-badge"></a>
17
+ <a href="https://github.com/Ayberkrk/timoshenko/actions/workflows/tests.yml"><img alt="Tests" src="https://github.com/Ayberkrk/timoshenko/actions/workflows/tests.yml/badge.svg"></a>
18
+ <a href="https://github.com/Ayberkrk/timoshenko/tree/python-coverage-comment-action-data"><img alt="Coverage" src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FAyberkrk%2Ftimoshenko%2Fpython-coverage-comment-action-data%2Fendpoint.json&style=for-the-badge"></a>
19
+ <a href="https://timoshenko.readthedocs.io/en/latest/"><img alt="Documentation" src="https://img.shields.io/readthedocs/timoshenko?style=for-the-badge"></a>
20
+ </p>
21
+
22
+ Timoshenko packages common structural calculations, modal analysis, sensor
23
+ workflows, and monitoring components so applications can reuse them instead
24
+ of rebuilding the same foundations for every project. It is an embeddable
25
+ Python library, not a hosted monitoring service or a general finite-element
26
+ solver.
27
+
28
+ > **Alpha:** the API may still change between releases. Every behavior change
29
+ > is listed in the [changelog](https://timoshenko.readthedocs.io/en/latest/changelog/).
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ python -m pip install timoshenko-engine
35
+ ```
36
+
37
+ The distribution is named `timoshenko-engine`; the import package is
38
+ `timoshenko`. Optional MQTT support is an extra:
39
+
40
+ ```bash
41
+ python -m pip install "timoshenko-engine[mqtt]"
42
+ ```
43
+
44
+ ## Quick start
45
+
46
+ Compare measured vibration with a reference model. This example simulates a
47
+ record in which both modes are 5% below the model:
48
+
49
+ ```python
50
+ import numpy as np
51
+ import timoshenko as tm
52
+
53
+ structure = tm.Structure(
54
+ structure_id="building-01",
55
+ story_masses_kg=[120_000.0, 110_000.0],
56
+ story_stiffness_n_m=[85_000_000.0, 70_000_000.0],
57
+ )
58
+ print(structure.natural_frequencies_hz) # (2.626, 6.476)
59
+
60
+ fs = 100.0
61
+ t = np.arange(60_000) / fs
62
+ f1, f2 = (0.95 * f for f in structure.natural_frequencies_hz)
63
+ signal = np.sin(2 * np.pi * f1 * t) + 0.4 * np.sin(2 * np.pi * f2 * t)
64
+ sensors = tm.SensorData(signal, sampling_hz=fs, unit="m/s^2")
65
+
66
+ result = tm.monitor(structure, sensors, review_threshold_pct=3.0)
67
+ print(result.structure.update_scale_factor) # 0.903, since stiffness scales with frequency squared
68
+ for change in result.health.mode_changes:
69
+ print(change.mode_number, change.change_pct) # 1 -5.0, then 2 -5.0
70
+ print(result.health.review_recommended) # True
71
+ ```
72
+
73
+ <p align="center">
74
+ <img src="https://raw.githubusercontent.com/Ayberkrk/timoshenko/main/assets/modal-comparison.svg" alt="Both identified modes plot 5% below the reference model, past the 3% review threshold" width="800">
75
+ </p>
76
+
77
+ Real records load from CSV or JSON with
78
+ `tm.load_sensors("acceleration.csv", sampling_hz=100.0, column="acc")`. The
79
+ steps inside `tm.monitor` are also available separately as `tm.modal.identify`,
80
+ `tm.update`, and `tm.health.assess`. The review flag is raised only when you
81
+ pass a threshold: a meaningful value depends on the structure and its
82
+ environmental variability, so the engine does not choose one.
83
+
84
+ ## Engineering calculations
85
+
86
+ Closed-form functions take and return SI units and validate their inputs:
87
+
88
+ ```python
89
+ import timoshenko as tm
90
+
91
+ section = tm.rectangle_section(width_m=0.3, height_m=0.6)
92
+ beam = tm.simply_supported_uniform_load(
93
+ 20_000.0, 6.0, 30e9, section.second_moment_y_m4,
94
+ shear_modulus_pa=12.5e9, area_m2=section.area_m2,
95
+ )
96
+ print(beam.bending_m, beam.shear_m) # 2.083 mm bending, 0.048 mm shear
97
+
98
+ frequency = tm.propagate_uncertainty(
99
+ tm.natural_frequency_hz,
100
+ {"mass_kg": 250.0, "stiffness_n_m": 4e5},
101
+ standard_uncertainties={"mass_kg": 5.0, "stiffness_n_m": 8e3},
102
+ )
103
+ print(frequency.estimate, frequency.standard_uncertainty) # 6.366 Hz ± 0.090 Hz
104
+ ```
105
+
106
+ ## What it provides
107
+
108
+ | Area | Components |
109
+ |---|---|
110
+ | Engineering calculations | Section properties (including polygons with holes), beam deflection with shear, torsion, Euler buckling, plane stress, thin-wall pressure, SDOF vibration, Rayleigh damping, and GUM / Monte Carlo uncertainty |
111
+ | Structural models | Lumped-mass shear-building models and their natural frequencies |
112
+ | Modal analysis | Single-channel peak picking with resolution-checked damping, and multi-channel FDD with complex mode shapes |
113
+ | Model comparison | Nearest-frequency mode pairing, global stiffness updating, and evidence-oriented health assessment |
114
+ | Monitoring | Bounded rolling-window sessions, restart from local history, and source adapters for CSV, MQTT, and OGC SensorThings |
115
+ | Data and reporting | Project manifests with SHA-256 provenance, local SQLite history, and standalone HTML reports |
116
+
117
+ Sensor collection, alarm policy, and engineering interpretation remain with the
118
+ application that embeds Timoshenko.
119
+
120
+ ## Multi-channel modal analysis
121
+
122
+ Frequency domain decomposition needs synchronized channels with a common
123
+ sample rate and unit:
124
+
125
+ ```python
126
+ signals = tm.load_multichannel_csv(
127
+ "aligned_accelerometers.csv",
128
+ columns=["deck_left", "deck_center", "deck_right"],
129
+ sampling_hz=100.0,
130
+ units=["m/s^2"] * 3,
131
+ )
132
+ fdd = tm.identify_fdd(signals, nperseg=1024, max_modes=5)
133
+ for mode in fdd.modes:
134
+ print(mode.frequency_hz, mode.shape_real)
135
+ ```
136
+
137
+ ## Monitoring sessions
138
+
139
+ A host application feeds timestamped observation batches. The session aligns
140
+ samples to the sample grid, waits for a fresh contiguous window after gaps, and
141
+ analyzes every configured hop:
142
+
143
+ ```python
144
+ session = tm.MonitoringSession(
145
+ structure,
146
+ sensor_ids=["deck-left", "deck-right"],
147
+ units=["m/s^2", "m/s^2"],
148
+ sampling_hz=100.0,
149
+ window_samples=2048,
150
+ hop_samples=512,
151
+ analysis_options={"nperseg": 512, "max_modes": 4},
152
+ review_threshold_pct=5.0,
153
+ )
154
+ result = session.ingest(batch) # batch: tm.ObservationBatch from your gateway or a source adapter
155
+ for report in result.reports:
156
+ tm.report.save_html(report, "reports/latest.html")
157
+ ```
158
+
159
+ The session does not open network connections or run in the background. Use
160
+ `tm.SessionRunner` with a source such as `tm.CSVObservationSource` or
161
+ `tm.MqttObservationSource` to drive it.
162
+
163
+ ## Documentation
164
+
165
+ The full documentation is at
166
+ [timoshenko.readthedocs.io](https://timoshenko.readthedocs.io/en/latest/). Good places to start:
167
+
168
+ - [API reference](https://timoshenko.readthedocs.io/en/latest/api/)
169
+ - [Numerical methods and limits](https://timoshenko.readthedocs.io/en/latest/numerical-methods/)
170
+ - [Architecture](https://timoshenko.readthedocs.io/en/latest/architecture/)
171
+ - [Monitoring sessions](https://timoshenko.readthedocs.io/en/latest/live-sessions/) and [source adapters](https://timoshenko.readthedocs.io/en/latest/adapters/)
172
+ - [Runnable examples](https://github.com/Ayberkrk/timoshenko/tree/main/examples), including a [cross-check of PyNite shear-deformable beams](https://timoshenko.readthedocs.io/en/latest/pynite-verification/)
173
+ - [Changelog](https://timoshenko.readthedocs.io/en/latest/changelog/)
174
+
175
+ ## Limits and engineering posture
176
+
177
+ - Timoshenko is alpha software and is not a structural safety certification tool.
178
+ - The shear-building model is a small lumped-mass reference model, not a full FEM solver.
179
+ - Modal identification is a screening estimate. A single sensor may miss a mode near a modal node; unpaired peaks are reported rather than compared with the wrong mode.
180
+ - Damping is a coarse screening estimate, reported only when the averaged spectrum resolves the half-power bandwidth. FDD does not estimate damping.
181
+ - The model update applies a single stiffness scale. It cannot locate or size local damage.
182
+ - A frequency shift is evidence for human review, not a damage verdict. Temperature, sensor placement, boundary conditions, and other effects also shift measured frequencies.
183
+ - Closed-form mechanics and section functions rely on their documented ideal assumptions. They are not code-compliance checks.
184
+
185
+ ## Development
186
+
187
+ ```bash
188
+ python -m pip install -e ".[test,lint]"
189
+ python -m pytest --cov
190
+ python -m ruff check .
191
+ python -m mypy
192
+ ```
193
+
194
+ To preview the documentation site:
195
+
196
+ ```bash
197
+ python -m pip install -e ".[docs]"
198
+ python -m mkdocs serve
199
+ ```
200
+
201
+ Bug reports and pull requests are welcome in the
202
+ [issue tracker](https://github.com/Ayberkrk/timoshenko/issues). See
203
+ [`CONTRIBUTING.md`](https://github.com/Ayberkrk/timoshenko/blob/main/CONTRIBUTING.md)
204
+ for the checks a pull request needs to pass; issues labelled
205
+ [good first issue](https://github.com/Ayberkrk/timoshenko/labels/good%20first%20issue)
206
+ are small, self-contained starting points.
207
+
208
+ ## Citation
209
+
210
+ If you use Timoshenko in research, please cite it with its DOI,
211
+ [10.5281/zenodo.22968739](https://doi.org/10.5281/zenodo.22968739), which
212
+ always resolves to the latest release; each release also has its own DOI on
213
+ [Zenodo](https://zenodo.org/records/22968740). The metadata is in
214
+ [`CITATION.cff`](https://github.com/Ayberkrk/timoshenko/blob/main/CITATION.cff),
215
+ which GitHub's "Cite this repository" button reads.
216
+
217
+ ## License
218
+
219
+ Apache License 2.0. See [LICENSE](https://github.com/Ayberkrk/timoshenko/blob/main/LICENSE).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "timoshenko-engine"
7
- version = "2.0.1"
7
+ version = "2.0.3"
8
8
  description = "Composable structural engineering calculations and analysis primitives for Python applications."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -29,11 +29,13 @@ classifiers = [
29
29
  "Programming Language :: Python :: 3.12",
30
30
  "Programming Language :: Python :: 3.13",
31
31
  "Topic :: Scientific/Engineering",
32
+ "Typing :: Typed",
32
33
  ]
33
34
 
34
35
  [project.optional-dependencies]
35
36
  mqtt = ["paho-mqtt>=2.1,<3"]
36
- test = ["pytest>=7"]
37
+ test = ["pytest>=7", "pytest-cov>=5"]
38
+ lint = ["ruff>=0.6", "mypy>=1.10"]
37
39
  docs = ["mkdocs-material>=9.5,<10"]
38
40
 
39
41
  [project.entry-points."timoshenko.plugins"]
@@ -44,7 +46,8 @@ sensorthings = "timoshenko.sensorthings:SensorThingsPlugin"
44
46
  Homepage = "https://github.com/Ayberkrk/timoshenko"
45
47
  Repository = "https://github.com/Ayberkrk/timoshenko"
46
48
  Issues = "https://github.com/Ayberkrk/timoshenko/issues"
47
- Documentation = "https://github.com/Ayberkrk/timoshenko/tree/main/docs"
49
+ Documentation = "https://timoshenko.readthedocs.io/"
50
+ Changelog = "https://github.com/Ayberkrk/timoshenko/blob/main/docs/changelog.md"
48
51
 
49
52
  [tool.setuptools]
50
53
  package-dir = { "" = "src" }
@@ -52,6 +55,29 @@ package-dir = { "" = "src" }
52
55
  [tool.setuptools.packages.find]
53
56
  where = ["src"]
54
57
 
58
+ [tool.setuptools.package-data]
59
+ timoshenko = ["py.typed"]
60
+
55
61
  [tool.pytest.ini_options]
56
62
  testpaths = ["tests"]
57
63
  xfail_strict = true
64
+
65
+ [tool.coverage.run]
66
+ source = ["timoshenko"]
67
+ relative_files = true
68
+
69
+ [tool.coverage.report]
70
+ show_missing = true
71
+
72
+ [tool.ruff]
73
+ target-version = "py310"
74
+ src = ["src", "tests"]
75
+
76
+ [tool.ruff.lint]
77
+ select = ["E4", "E7", "E9", "F"]
78
+
79
+ [tool.mypy]
80
+ files = ["src/timoshenko"]
81
+ check_untyped_defs = true
82
+ warn_unused_ignores = true
83
+ warn_redundant_casts = true
@@ -59,7 +59,7 @@ from .update import update
59
59
  from .uncertainty import UncertaintyError, UncertaintyResult, propagate as propagate_uncertainty
60
60
  from .vibration import RayleighDampingResult, damping_ratio, harmonic_response, natural_frequency_hz, rayleigh_damping_coefficients
61
61
 
62
- __version__ = "2.0.1"
62
+ __version__ = "2.0.3"
63
63
 
64
64
  __all__ = [
65
65
  "HealthAssessment",
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Iterator, Protocol, runtime_checkable
5
+ from typing import Iterator, Literal, Protocol, runtime_checkable
6
6
 
7
7
  from .observations import ObservationBatch
8
8
  from .session import MonitoringSession, SessionIngestResult
@@ -73,7 +73,7 @@ class SessionRunner:
73
73
  self._consuming = False
74
74
  return self
75
75
 
76
- def __exit__(self, exc_type, exc, traceback) -> bool:
76
+ def __exit__(self, exc_type, exc, traceback) -> Literal[False]:
77
77
  self._active = False
78
78
  try:
79
79
  self.source.close()
@@ -8,7 +8,7 @@ import math
8
8
  import numpy as np
9
9
 
10
10
  from .sensors import SensorData
11
- from .oma import FDDMode, FDDResult, identify_fdd
11
+ from .oma import FDDMode as FDDMode, FDDResult as FDDResult, identify_fdd as identify_fdd
12
12
 
13
13
 
14
14
  @dataclass(frozen=True)
@@ -105,7 +105,7 @@ def identify(
105
105
  )
106
106
  for idx in selected
107
107
  )
108
- notes = ["Frequency spacing is limited by the record duration."] if resolution > 0.25 else []
108
+ notes: list[str] = ["Frequency spacing is limited by the record duration."] if resolution > 0.25 else []
109
109
  if any(mode.damping_ratio is not None for mode in modes):
110
110
  notes.append(
111
111
  "Damping ratios are coarse half-power screening estimates from an averaged spectrum; "
@@ -116,7 +116,6 @@ def identify(
116
116
  "Damping is withheld where the record is too short to resolve the half-power bandwidth "
117
117
  f"with {_MIN_DAMPING_AVERAGES} averages and at least {_MIN_DAMPING_BINS:g} frequency bins."
118
118
  )
119
- notes = tuple(notes)
120
119
  return ModalResult(
121
120
  modes=modes,
122
121
  sampling_hz=sensor_data.sampling_hz,
@@ -124,7 +123,7 @@ def identify(
124
123
  resolution_hz=resolution,
125
124
  channel=sensor_data.channel,
126
125
  status="ok" if modes else "no_peaks_found",
127
- notes=notes,
126
+ notes=tuple(notes),
128
127
  )
129
128
 
130
129
 
@@ -54,11 +54,11 @@ class MultiChannelData:
54
54
 
55
55
  @property
56
56
  def sample_count(self) -> int:
57
- return int(self.samples.shape[0])
57
+ return int(np.shape(self.samples)[0])
58
58
 
59
59
  @property
60
60
  def channel_count(self) -> int:
61
- return int(self.samples.shape[1])
61
+ return int(np.shape(self.samples)[1])
62
62
 
63
63
 
64
64
  def load_multichannel_csv(
@@ -93,7 +93,7 @@ def load_multichannel_csv(
93
93
  for row_number, row in enumerate(reader, start=2):
94
94
  record: list[float] = []
95
95
  for name in names:
96
- raw = row.get(name)
96
+ raw = row.get(name) or ""
97
97
  try:
98
98
  value = float(raw)
99
99
  except (TypeError, ValueError):
@@ -93,7 +93,7 @@ def identify_fdd(
93
93
  raise TypeError("observations must be MultiChannelData; use tm.load_multichannel_csv() or construct it explicitly")
94
94
  if len(set(observations.units)) != 1:
95
95
  raise ValueError("FDD requires channels with the same measurement unit; calibrate/transform mixed-unit channels first")
96
- sample_count, channel_count = observations.samples.shape
96
+ sample_count, channel_count = np.shape(observations.samples)
97
97
  nperseg_value, hop, segment_count, resolution, low, high = _plan(
98
98
  sample_count,
99
99
  observations.sampling_hz,
@@ -223,6 +223,7 @@ def run_project(project: LoadedProject | str | Path) -> ProjectRunResult:
223
223
  if not isinstance(loaded, LoadedProject):
224
224
  raise TypeError("project must be a manifest path or LoadedProject from tm.load_project()")
225
225
  manifest = loaded.manifest
226
+ modal: ModalResult | FDDResult
226
227
  if manifest.method == "peak_picking":
227
228
  if not isinstance(loaded.observations, SensorData):
228
229
  raise TypeError("peak_picking manifest must load one SensorData channel")
File without changes
@@ -94,7 +94,6 @@ def rectangular_tube(
94
94
  if 2.0 * thickness >= min(width, height):
95
95
  raise ValueError("twice wall_thickness_m must be smaller than both outer dimensions")
96
96
  inner_height = height - 2.0 * thickness
97
- inner_width = width - 2.0 * thickness
98
97
  horizontal_plate_offset = (height - thickness) / 2.0
99
98
  vertical_plate_offset = (width - thickness) / 2.0
100
99
  area = 2.0 * width * thickness + 2.0 * thickness * inner_height
@@ -16,7 +16,7 @@ import numpy as np
16
16
  class SensorData:
17
17
  """A single regularly sampled sensor channel."""
18
18
 
19
- samples: tuple[float, ...] | Sequence[float]
19
+ samples: Sequence[float] | np.ndarray
20
20
  sampling_hz: float
21
21
  unit: str = "m/s^2"
22
22
  channel: str = "sensor"