physure 0.2.0__tar.gz → 0.2.1__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 (289) hide show
  1. {physure-0.2.0 → physure-0.2.1}/Cargo.lock +2 -2
  2. {physure-0.2.0 → physure-0.2.1}/Cargo.toml +1 -1
  3. {physure-0.2.0 → physure-0.2.1}/PKG-INFO +11 -7
  4. {physure-0.2.0 → physure-0.2.1}/README.md +10 -6
  5. {physure-0.2.0 → physure-0.2.1}/physure/__init__.py +47 -1
  6. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/_arithmetic_mixin.py +15 -0
  7. {physure-0.2.0 → physure-0.2.1}/physure/domain/symbolic/native.py +8 -4
  8. {physure-0.2.0 → physure-0.2.1}/physure/ext/grammar.py +188 -2
  9. physure-0.2.1/physure/ext/helpers.py +146 -0
  10. {physure-0.2.0 → physure-0.2.1}/physure/repl.py +9 -1
  11. {physure-0.2.0 → physure-0.2.1}/physure-core/README.md +9 -2
  12. {physure-0.2.0 → physure-0.2.1}/physure-core/src/quantity.rs +17 -0
  13. {physure-0.2.0 → physure-0.2.1}/physure-core/src/symbolic/ast.rs +2 -1
  14. {physure-0.2.0 → physure-0.2.1}/physure-core/src/symbolic/tests.rs +10 -0
  15. {physure-0.2.0 → physure-0.2.1}/physure-core/src/units/rational.rs +4 -0
  16. {physure-0.2.0 → physure-0.2.1}/physure-python/README.md +10 -6
  17. {physure-0.2.0 → physure-0.2.1}/physure-python/benchmarks/benchmark_lineage.py +7 -7
  18. {physure-0.2.0 → physure-0.2.1}/physure-python/benchmarks/benchmark_propagation.py +4 -4
  19. {physure-0.2.0 → physure-0.2.1}/physure-python/src/lib.rs +12 -0
  20. physure-0.2.1/physure-python/tests/domain/test_bare_number_add_sub.py +60 -0
  21. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_grammar.py +1 -1
  22. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/symbolic/test_tracing.py +9 -9
  23. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/test_jit_uncertainty.py +4 -4
  24. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/test_new_features.py +2 -2
  25. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/test_plotting.py +10 -10
  26. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/test_rust_transcendental_delegation.py +6 -6
  27. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/test_uncertainty_api.py +4 -4
  28. {physure-0.2.0 → physure-0.2.1}/pyproject.toml +1 -1
  29. {physure-0.2.0 → physure-0.2.1}/LICENSE +0 -0
  30. {physure-0.2.0 → physure-0.2.1}/physure/__main__.py +0 -0
  31. {physure-0.2.0 → physure-0.2.1}/physure/_core.pyi +0 -0
  32. {physure-0.2.0 → physure-0.2.1}/physure/_generated_types.py +0 -0
  33. {physure-0.2.0 → physure-0.2.1}/physure/_jit/__init__.py +0 -0
  34. {physure-0.2.0 → physure-0.2.1}/physure/_jit/tracer.py +0 -0
  35. {physure-0.2.0 → physure-0.2.1}/physure/application/__init__.py +0 -0
  36. {physure-0.2.0 → physure-0.2.1}/physure/application/context.py +0 -0
  37. {physure-0.2.0 → physure-0.2.1}/physure/application/factories.py +0 -0
  38. {physure-0.2.0 → physure-0.2.1}/physure/application/fit_service.py +0 -0
  39. {physure-0.2.0 → physure-0.2.1}/physure/application/functions/__init__.py +0 -0
  40. {physure-0.2.0 → physure-0.2.1}/physure/application/functions/functions.py +0 -0
  41. {physure-0.2.0 → physure-0.2.1}/physure/application/io.py +0 -0
  42. {physure-0.2.0 → physure-0.2.1}/physure/application/parsing.py +0 -0
  43. {physure-0.2.0 → physure-0.2.1}/physure/application/solver_service.py +0 -0
  44. {physure-0.2.0 → physure-0.2.1}/physure/application/startup.py +0 -0
  45. {physure-0.2.0 → physure-0.2.1}/physure/application/tracing/context.py +0 -0
  46. {physure-0.2.0 → physure-0.2.1}/physure/backends/__init__.py +0 -0
  47. {physure-0.2.0 → physure-0.2.1}/physure/backends/core_backend.py +0 -0
  48. {physure-0.2.0 → physure-0.2.1}/physure/backends/jax_backend.py +0 -0
  49. {physure-0.2.0 → physure-0.2.1}/physure/backends/kernels/covariance.py +0 -0
  50. {physure-0.2.0 → physure-0.2.1}/physure/backends/numpy_backend.py +0 -0
  51. {physure-0.2.0 → physure-0.2.1}/physure/backends/python_backend.py +0 -0
  52. {physure-0.2.0 → physure-0.2.1}/physure/backends/torch/autograd_store.py +0 -0
  53. {physure-0.2.0 → physure-0.2.1}/physure/backends/torch_backend.py +0 -0
  54. {physure-0.2.0 → physure-0.2.1}/physure/cli.py +0 -0
  55. {physure-0.2.0 → physure-0.2.1}/physure/constants.py +0 -0
  56. {physure-0.2.0 → physure-0.2.1}/physure/core/__init__.py +0 -0
  57. {physure-0.2.0 → physure-0.2.1}/physure/core/autograd.py +0 -0
  58. {physure-0.2.0 → physure-0.2.1}/physure/core/dispatcher.py +0 -0
  59. {physure-0.2.0 → physure-0.2.1}/physure/core/formatting.py +0 -0
  60. {physure-0.2.0 → physure-0.2.1}/physure/core/functional.py +0 -0
  61. {physure-0.2.0 → physure-0.2.1}/physure/core/parsing/sanitizer.py +0 -0
  62. {physure-0.2.0 → physure-0.2.1}/physure/core/parsing/sympy_parser.py +0 -0
  63. {physure-0.2.0 → physure-0.2.1}/physure/core/parsing/transformer.py +0 -0
  64. {physure-0.2.0 → physure-0.2.1}/physure/core/protocols.py +0 -0
  65. {physure-0.2.0 → physure-0.2.1}/physure/core/registry.py +0 -0
  66. {physure-0.2.0 → physure-0.2.1}/physure/core/registry.pyi +0 -0
  67. {physure-0.2.0 → physure-0.2.1}/physure/domain/__init__.py +0 -0
  68. {physure-0.2.0 → physure-0.2.1}/physure/domain/exceptions.py +0 -0
  69. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/__init__.py +0 -0
  70. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/_backend_mixin.py +0 -0
  71. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/base_entity.py +0 -0
  72. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/conversions.py +0 -0
  73. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/converters.py +0 -0
  74. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/dimensions.py +0 -0
  75. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/equivalencies.py +0 -0
  76. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/ports/__init__.py +0 -0
  77. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/ports/unit_repository.py +0 -0
  78. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/quantity.py +0 -0
  79. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/system.py +0 -0
  80. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/uncertainty.py +0 -0
  81. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/units.py +0 -0
  82. {physure-0.2.0 → physure-0.2.1}/physure/domain/measurement/vectorized_uncertainty.py +0 -0
  83. {physure-0.2.0 → physure-0.2.1}/physure/domain/symbolic/__init__.py +0 -0
  84. {physure-0.2.0 → physure-0.2.1}/physure/domain/symbolic/export.py +0 -0
  85. {physure-0.2.0 → physure-0.2.1}/physure/domain/symbolic/expression.py +0 -0
  86. {physure-0.2.0 → physure-0.2.1}/physure/domain/symbolic/graph.py +0 -0
  87. {physure-0.2.0 → physure-0.2.1}/physure/domain/symbolic/quantity.py +0 -0
  88. {physure-0.2.0 → physure-0.2.1}/physure/domain/symbolic/tracer.py +0 -0
  89. {physure-0.2.0 → physure-0.2.1}/physure/ext/__init__.py +0 -0
  90. {physure-0.2.0 → physure-0.2.1}/physure/ext/chemistry/__init__.py +0 -0
  91. {physure-0.2.0 → physure-0.2.1}/physure/ext/chemistry/equivalency.py +0 -0
  92. {physure-0.2.0 → physure-0.2.1}/physure/ext/chemistry/reaction.py +0 -0
  93. {physure-0.2.0 → physure-0.2.1}/physure/ext/chemistry/species.py +0 -0
  94. {physure-0.2.0 → physure-0.2.1}/physure/ext/chemistry/thermo_kinetics.py +0 -0
  95. {physure-0.2.0 → physure-0.2.1}/physure/ext/compiler.py +0 -0
  96. {physure-0.2.0 → physure-0.2.1}/physure/ext/currency.py +0 -0
  97. {physure-0.2.0 → physure-0.2.1}/physure/ext/io.py +0 -0
  98. {physure-0.2.0 → physure-0.2.1}/physure/ext/numba_support.py +0 -0
  99. {physure-0.2.0 → physure-0.2.1}/physure/ext/pandas_support.py +0 -0
  100. {physure-0.2.0 → physure-0.2.1}/physure/ext/symbolic_regression.py +0 -0
  101. {physure-0.2.0 → physure-0.2.1}/physure/functional.py +0 -0
  102. {physure-0.2.0 → physure-0.2.1}/physure/infrastructure/__init__.py +0 -0
  103. {physure-0.2.0 → physure-0.2.1}/physure/infrastructure/config/physure.conf +0 -0
  104. {physure-0.2.0 → physure-0.2.1}/physure/infrastructure/config/systems/__init__.py +0 -0
  105. {physure-0.2.0 → physure-0.2.1}/physure/infrastructure/config/systems/imperial.conf +0 -0
  106. {physure-0.2.0 → physure-0.2.1}/physure/infrastructure/config/systems/international.conf +0 -0
  107. {physure-0.2.0 → physure-0.2.1}/physure/nn/__init__.py +0 -0
  108. {physure-0.2.0 → physure-0.2.1}/physure/nn/layers.py +0 -0
  109. {physure-0.2.0 → physure-0.2.1}/physure/nn/loss.py +0 -0
  110. {physure-0.2.0 → physure-0.2.1}/physure/nn/networks.py +0 -0
  111. {physure-0.2.0 → physure-0.2.1}/physure/nn/utils.py +0 -0
  112. {physure-0.2.0 → physure-0.2.1}/physure/pandas_ext.py +0 -0
  113. {physure-0.2.0 → physure-0.2.1}/physure/plotting.py +0 -0
  114. {physure-0.2.0 → physure-0.2.1}/physure/py.typed +0 -0
  115. {physure-0.2.0 → physure-0.2.1}/physure/scripts/generate_types.py +0 -0
  116. {physure-0.2.0 → physure-0.2.1}/physure/symbolic.py +0 -0
  117. {physure-0.2.0 → physure-0.2.1}/physure/units/__init__.py +0 -0
  118. {physure-0.2.0 → physure-0.2.1}/physure/units/__init__.pyi +0 -0
  119. {physure-0.2.0 → physure-0.2.1}/physure/units/_index.py +0 -0
  120. {physure-0.2.0 → physure-0.2.1}/physure/units/core.py +0 -0
  121. {physure-0.2.0 → physure-0.2.1}/physure/units/electromagnetism.py +0 -0
  122. {physure-0.2.0 → physure-0.2.1}/physure-core/.cargo/config.toml +0 -0
  123. {physure-0.2.0 → physure-0.2.1}/physure-core/.github/workflows/CI.yml +0 -0
  124. {physure-0.2.0 → physure-0.2.1}/physure-core/.gitignore +0 -0
  125. {physure-0.2.0 → physure-0.2.1}/physure-core/Cargo.lock +0 -0
  126. {physure-0.2.0 → physure-0.2.1}/physure-core/Cargo.toml +0 -0
  127. {physure-0.2.0 → physure-0.2.1}/physure-core/LICENSE +0 -0
  128. {physure-0.2.0 → physure-0.2.1}/physure-core/benches/core_benchmarks.rs +0 -0
  129. {physure-0.2.0 → physure-0.2.1}/physure-core/examples/quickstart.rs +0 -0
  130. {physure-0.2.0 → physure-0.2.1}/physure-core/scripts/filter_lcov.py +0 -0
  131. {physure-0.2.0 → physure-0.2.1}/physure-core/sonar-project.properties +0 -0
  132. {physure-0.2.0 → physure-0.2.1}/physure-core/src/covariance/arrow.rs +0 -0
  133. {physure-0.2.0 → physure-0.2.1}/physure-core/src/covariance/mod.rs +0 -0
  134. {physure-0.2.0 → physure-0.2.1}/physure-core/src/covariance/pruning.rs +0 -0
  135. {physure-0.2.0 → physure-0.2.1}/physure-core/src/covariance/store.rs +0 -0
  136. {physure-0.2.0 → physure-0.2.1}/physure-core/src/covariance/tests.rs +0 -0
  137. {physure-0.2.0 → physure-0.2.1}/physure-core/src/error.rs +0 -0
  138. {physure-0.2.0 → physure-0.2.1}/physure-core/src/lib.rs +0 -0
  139. {physure-0.2.0 → physure-0.2.1}/physure-core/src/math/dimension_vector.rs +0 -0
  140. {physure-0.2.0 → physure-0.2.1}/physure-core/src/math/dual.rs +0 -0
  141. {physure-0.2.0 → physure-0.2.1}/physure-core/src/math/hessian_propagation.rs +0 -0
  142. {physure-0.2.0 → physure-0.2.1}/physure-core/src/math/interval.rs +0 -0
  143. {physure-0.2.0 → physure-0.2.1}/physure-core/src/math/mod.rs +0 -0
  144. {physure-0.2.0 → physure-0.2.1}/physure-core/src/math/sparse_kernels.rs +0 -0
  145. {physure-0.2.0 → physure-0.2.1}/physure-core/src/math/tests.rs +0 -0
  146. {physure-0.2.0 → physure-0.2.1}/physure-core/src/serialization.rs +0 -0
  147. {physure-0.2.0 → physure-0.2.1}/physure-core/src/symbolic/compiler.rs +0 -0
  148. {physure-0.2.0 → physure-0.2.1}/physure-core/src/symbolic/diff.rs +0 -0
  149. {physure-0.2.0 → physure-0.2.1}/physure-core/src/symbolic/expr.rs +0 -0
  150. {physure-0.2.0 → physure-0.2.1}/physure-core/src/symbolic/factor.rs +0 -0
  151. {physure-0.2.0 → physure-0.2.1}/physure-core/src/symbolic/integrate.rs +0 -0
  152. {physure-0.2.0 → physure-0.2.1}/physure-core/src/symbolic/mod.rs +0 -0
  153. {physure-0.2.0 → physure-0.2.1}/physure-core/src/uncertainty/gaussian.rs +0 -0
  154. {physure-0.2.0 → physure-0.2.1}/physure-core/src/uncertainty/mod.rs +0 -0
  155. {physure-0.2.0 → physure-0.2.1}/physure-core/src/uncertainty/monte_carlo.rs +0 -0
  156. {physure-0.2.0 → physure-0.2.1}/physure-core/src/uncertainty/tests.rs +0 -0
  157. {physure-0.2.0 → physure-0.2.1}/physure-core/src/uncertainty/trait_def.rs +0 -0
  158. {physure-0.2.0 → physure-0.2.1}/physure-core/src/uncertainty/unscented.rs +0 -0
  159. {physure-0.2.0 → physure-0.2.1}/physure-core/src/units/converters.rs +0 -0
  160. {physure-0.2.0 → physure-0.2.1}/physure-core/src/units/definition.rs +0 -0
  161. {physure-0.2.0 → physure-0.2.1}/physure-core/src/units/dimension.rs +0 -0
  162. {physure-0.2.0 → physure-0.2.1}/physure-core/src/units/mod.rs +0 -0
  163. {physure-0.2.0 → physure-0.2.1}/physure-core/src/units/parser.rs +0 -0
  164. {physure-0.2.0 → physure-0.2.1}/physure-core/src/units/registry.rs +0 -0
  165. {physure-0.2.0 → physure-0.2.1}/physure-core/src/units/tests.rs +0 -0
  166. {physure-0.2.0 → physure-0.2.1}/physure-core/tests/edge_cases_and_concurrency.rs +0 -0
  167. {physure-0.2.0 → physure-0.2.1}/physure-python/Cargo.toml +0 -0
  168. {physure-0.2.0 → physure-0.2.1}/physure-python/LICENSE +0 -0
  169. {physure-0.2.0 → physure-0.2.1}/physure-python/MANIFEST.in +0 -0
  170. {physure-0.2.0 → physure-0.2.1}/physure-python/Makefile +0 -0
  171. {physure-0.2.0 → physure-0.2.1}/physure-python/benchmarks/benchmark_comparison.py +0 -0
  172. {physure-0.2.0 → physure-0.2.1}/physure-python/benchmarks/benchmark_vectorized.py +0 -0
  173. {physure-0.2.0 → physure-0.2.1}/physure-python/benchmarks/comprehensive_plot.py +0 -0
  174. {physure-0.2.0 → physure-0.2.1}/physure-python/benchmarks/profile_vectorized.py +0 -0
  175. {physure-0.2.0 → physure-0.2.1}/physure-python/benchmarks/run_compile_bench.py +0 -0
  176. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/UNITS.md +0 -0
  177. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/benchmarks.md +0 -0
  178. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/chemistry_roadmap.md +0 -0
  179. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/diagrams_roadmap.md +0 -0
  180. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/index.md +0 -0
  181. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/reference/quantity.md +0 -0
  182. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/scientific_roadmap.md +0 -0
  183. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/superpowers/plans/2026-07-11-mkml-math-functions.md +0 -0
  184. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/superpowers/plans/2026-07-11-mkml-notation-layer.md +0 -0
  185. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/superpowers/plans/2026-07-12-mkml-user-functions.md +0 -0
  186. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/superpowers/plans/2026-07-16-kill-rust-fallback-illusion.md +0 -0
  187. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/superpowers/plans/2026-07-16-rust-transcendental-delegation.md +0 -0
  188. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/superpowers/plans/2026-07-16-unit-parser-zero-exponent-guard.md +0 -0
  189. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/superpowers/specs/2026-07-11-mkml-math-functions-design.md +0 -0
  190. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/superpowers/specs/2026-07-11-notation-layer-design.md +0 -0
  191. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/superpowers/specs/2026-07-12-mkml-user-functions-design.md +0 -0
  192. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/superpowers/specs/2026-07-16-kill-rust-fallback-illusion-design.md +0 -0
  193. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/superpowers/specs/2026-07-16-rust-transcendental-delegation-design.md +0 -0
  194. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/superpowers/specs/2026-07-16-unit-parser-zero-exponent-guard-design.md +0 -0
  195. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/symbolic_math_roadmap.md +0 -0
  196. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/torch_compile_integration.md +0 -0
  197. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/tutorials/01_best_practices.ipynb +0 -0
  198. {physure-0.2.0 → physure-0.2.1}/physure-python/docs/tutorials/jax_tutorial.ipynb +0 -0
  199. {physure-0.2.0 → physure-0.2.1}/physure-python/examples/advanced_physics.py +0 -0
  200. {physure-0.2.0 → physure-0.2.1}/physure-python/examples/pandas_integration.py +0 -0
  201. {physure-0.2.0 → physure-0.2.1}/physure-python/examples/pinn_harmonic_oscillator.ipynb +0 -0
  202. {physure-0.2.0 → physure-0.2.1}/physure-python/mkdocs.yml +0 -0
  203. {physure-0.2.0 → physure-0.2.1}/physure-python/scripts/benchmark_import.py +0 -0
  204. {physure-0.2.0 → physure-0.2.1}/physure-python/scripts/compile_units.py +0 -0
  205. {physure-0.2.0 → physure-0.2.1}/physure-python/scripts/generate_constants_config.py +0 -0
  206. {physure-0.2.0 → physure-0.2.1}/physure-python/scripts/generate_units_readme.py +0 -0
  207. {physure-0.2.0 → physure-0.2.1}/physure-python/sonar-project.properties +0 -0
  208. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/__init__.py +0 -0
  209. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/conftest.py +0 -0
  210. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/__init__.py +0 -0
  211. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_backends.py +0 -0
  212. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_backends_and_quantity.py +0 -0
  213. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_concurrency.py +0 -0
  214. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_consistency.py +0 -0
  215. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_covariance_store.py +0 -0
  216. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_functional.py +0 -0
  217. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_functional_core.py +0 -0
  218. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_functional_jax.py +0 -0
  219. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_gradients.py +0 -0
  220. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_invariants.py +0 -0
  221. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_jit.py +0 -0
  222. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_pydantic.py +0 -0
  223. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_registry.py +0 -0
  224. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_serialization.py +0 -0
  225. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_sparse_backends.py +0 -0
  226. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/core/test_vectorized_unc.py +0 -0
  227. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/dynamics/__init__.py +0 -0
  228. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/dynamics/test_fit_service.py +0 -0
  229. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/dynamics/test_solver.py +0 -0
  230. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_autograd.py +0 -0
  231. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_chemistry_integration.py +0 -0
  232. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_compiler.py +0 -0
  233. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_currency.py +0 -0
  234. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_io.py +0 -0
  235. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_molar_equivalency.py +0 -0
  236. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_pandas_support.py +0 -0
  237. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_reaction.py +0 -0
  238. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_species.py +0 -0
  239. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_symbolic_regression.py +0 -0
  240. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_thermo_kinetics.py +0 -0
  241. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/ext/test_torch_compile.py +0 -0
  242. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/functions/__init__.py +0 -0
  243. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/functions/test_functions.py +0 -0
  244. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/integration_tests/__init__.py +0 -0
  245. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/integration_tests/test_measurement_system.py +0 -0
  246. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/integration_tests/test_physics_realworld.py +0 -0
  247. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/integration_tests/test_workflow.py +0 -0
  248. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/__init__.py +0 -0
  249. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_advanced.py +0 -0
  250. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_affine_arithmetic.py +0 -0
  251. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_api.py +0 -0
  252. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_autograd_propagation.py +0 -0
  253. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_backend_mixin.py +0 -0
  254. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_constants_codata.py +0 -0
  255. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_constants_module.py +0 -0
  256. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_conversions.py +0 -0
  257. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_conversions_realworld.py +0 -0
  258. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_correctness_fixes.py +0 -0
  259. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_dimensions.py +0 -0
  260. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_lazy_prefixes.py +0 -0
  261. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_nonlinear_and_calculus.py +0 -0
  262. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_quantity.py +0 -0
  263. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_uncertainty_models.py +0 -0
  264. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/measurement_tests/test_units.py +0 -0
  265. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/notation_tests/__init__.py +0 -0
  266. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/notation_tests/test_base_entity.py +0 -0
  267. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/notation_tests/test_lexer.py +0 -0
  268. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/notation_tests/test_parsers.py +0 -0
  269. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/performance/benchmarks.py +0 -0
  270. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/pinn/__init__.py +0 -0
  271. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/pinn/test_integration.py +0 -0
  272. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/pinn/test_nn_utils.py +0 -0
  273. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/strategies.py +0 -0
  274. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/symbolic/__init__.py +0 -0
  275. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/symbolic/test_calculus.py +0 -0
  276. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/symbolic/test_native_coverage.py +0 -0
  277. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/symbolic/test_native_expr.py +0 -0
  278. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/symbolic/test_solve_all.py +0 -0
  279. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/symbolic/test_solver.py +0 -0
  280. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/test_ecosystem.py +0 -0
  281. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/test_equivalencies.py +0 -0
  282. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/test_jit_core.py +0 -0
  283. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/test_performance_and_interop.py +0 -0
  284. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/test_repl.py +0 -0
  285. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/test_uncertainty.py +0 -0
  286. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/test_uncertainty_gum.py +0 -0
  287. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/tools/__init__.py +0 -0
  288. {physure-0.2.0 → physure-0.2.1}/physure-python/tests/tools/test_stub_gen.py +0 -0
  289. {physure-0.2.0 → physure-0.2.1}/physure-python/uv.lock +0 -0
@@ -1044,7 +1044,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
1044
1044
 
1045
1045
  [[package]]
1046
1046
  name = "physure"
1047
- version = "0.2.0"
1047
+ version = "0.2.1"
1048
1048
  dependencies = [
1049
1049
  "approx 0.5.1",
1050
1050
  "arrow",
@@ -1063,7 +1063,7 @@ dependencies = [
1063
1063
 
1064
1064
  [[package]]
1065
1065
  name = "physure-python"
1066
- version = "0.2.0"
1066
+ version = "0.2.1"
1067
1067
  dependencies = [
1068
1068
  "arrow",
1069
1069
  "chrono",
@@ -9,7 +9,7 @@ resolver = "2"
9
9
  members = ["physure-core", "physure-python"]
10
10
 
11
11
  [workspace.package]
12
- version = "0.2.0"
12
+ version = "0.2.1"
13
13
  edition = "2021"
14
14
  license = "MIT"
15
15
  repository = "https://github.com/Alexisrx96/physure"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: physure
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Science/Research
@@ -80,17 +80,21 @@ Project-URL: Documentation, https://physure.readthedocs.io/
80
80
  Project-URL: Homepage, https://github.com/Alexisrx96/physure
81
81
  Project-URL: Source, https://github.com/Alexisrx96/physure
82
82
 
83
- # physure 🔬
84
-
85
83
  <div align="center">
86
84
 
85
+ <picture>
86
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Alexisrx96/physure/main/assets/logo-horizontal-dark.svg">
87
+ <img src="https://raw.githubusercontent.com/Alexisrx96/physure/main/assets/logo-horizontal-light.svg" alt="physure" width="440">
88
+ </picture>
89
+
87
90
  <h3><b>Unit-aware, dimension-correct computing for Python — powered by a Rust core</b></h3>
88
91
  <p><i>Units, dimensions, and correlated uncertainty tracked through every calculation, with zero overhead under <code>torch.compile</code> / <code>jax.jit</code>.</i></p>
89
92
 
90
- [![PyPI](https://img.shields.io/pypi/v/physure)](https://pypi.org/project/physure/)
91
- [![Python 3.11–3.14](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/physure/)
92
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/Alexisrx96/physure/blob/main/LICENSE)
93
- [![Rust core](https://img.shields.io/badge/core-Rust%20%F0%9F%A6%80-orange)](https://github.com/Alexisrx96/physure/tree/main/physure-core/)
93
+ [![PyPI](https://img.shields.io/pypi/v/physure?color=F59E0B&labelColor=18181A)](https://pypi.org/project/physure/)
94
+ [![crates.io](https://img.shields.io/crates/v/physure?color=F59E0B&labelColor=18181A)](https://crates.io/crates/physure)
95
+ [![CI](https://img.shields.io/github/actions/workflow/status/Alexisrx96/physure/tests.yml?branch=main&labelColor=18181A)](https://github.com/Alexisrx96/physure/actions/workflows/tests.yml)
96
+ [![Python 3.11–3.14](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-F59E0B?labelColor=18181A)](https://pypi.org/project/physure/)
97
+ [![License: MIT](https://img.shields.io/badge/license-MIT-F59E0B?labelColor=18181A)](https://github.com/Alexisrx96/physure/blob/main/LICENSE)
94
98
 
95
99
  </div>
96
100
 
@@ -1,14 +1,18 @@
1
- # physure 🔬
2
-
3
1
  <div align="center">
4
2
 
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Alexisrx96/physure/main/assets/logo-horizontal-dark.svg">
5
+ <img src="https://raw.githubusercontent.com/Alexisrx96/physure/main/assets/logo-horizontal-light.svg" alt="physure" width="440">
6
+ </picture>
7
+
5
8
  <h3><b>Unit-aware, dimension-correct computing for Python — powered by a Rust core</b></h3>
6
9
  <p><i>Units, dimensions, and correlated uncertainty tracked through every calculation, with zero overhead under <code>torch.compile</code> / <code>jax.jit</code>.</i></p>
7
10
 
8
- [![PyPI](https://img.shields.io/pypi/v/physure)](https://pypi.org/project/physure/)
9
- [![Python 3.11–3.14](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/physure/)
10
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/Alexisrx96/physure/blob/main/LICENSE)
11
- [![Rust core](https://img.shields.io/badge/core-Rust%20%F0%9F%A6%80-orange)](https://github.com/Alexisrx96/physure/tree/main/physure-core/)
11
+ [![PyPI](https://img.shields.io/pypi/v/physure?color=F59E0B&labelColor=18181A)](https://pypi.org/project/physure/)
12
+ [![crates.io](https://img.shields.io/crates/v/physure?color=F59E0B&labelColor=18181A)](https://crates.io/crates/physure)
13
+ [![CI](https://img.shields.io/github/actions/workflow/status/Alexisrx96/physure/tests.yml?branch=main&labelColor=18181A)](https://github.com/Alexisrx96/physure/actions/workflows/tests.yml)
14
+ [![Python 3.11–3.14](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-F59E0B?labelColor=18181A)](https://pypi.org/project/physure/)
15
+ [![License: MIT](https://img.shields.io/badge/license-MIT-F59E0B?labelColor=18181A)](https://github.com/Alexisrx96/physure/blob/main/LICENSE)
12
16
 
13
17
  </div>
14
18
 
@@ -26,7 +26,7 @@ from collections.abc import Callable
26
26
  from typing import TYPE_CHECKING, Any
27
27
 
28
28
  # ── Version ───────────────────────────────────────────────────────────────────
29
- __version__ = "0.2.0"
29
+ __version__ = "0.2.1"
30
30
 
31
31
  # ── Rust-first: hard import of native core ────────────────────────────────────
32
32
  # Unlike physure, physure has NO pure-Python fallback.
@@ -143,6 +143,26 @@ _PLOT_ATTRS = {
143
143
  "plot_pairplot",
144
144
  "plot_covariance",
145
145
  }
146
+ _HELPER_ATTRS = {
147
+ "approx_eq",
148
+ "linspace",
149
+ "sqrt",
150
+ "sin",
151
+ "cos",
152
+ "tan",
153
+ "asin",
154
+ "acos",
155
+ "atan",
156
+ "atan2",
157
+ "sinh",
158
+ "cosh",
159
+ "tanh",
160
+ "exp",
161
+ "log",
162
+ "log10",
163
+ "pi",
164
+ "e",
165
+ }
146
166
 
147
167
 
148
168
  def _load_io(name: str) -> Any:
@@ -151,6 +171,12 @@ def _load_io(name: str) -> Any:
151
171
  return save_state if name == "save_state" else load_state
152
172
 
153
173
 
174
+ def _load_helpers(name: str) -> Any:
175
+ import physure.ext.helpers as helpers
176
+
177
+ return getattr(helpers, name)
178
+
179
+
154
180
  def _load_q(name: str) -> Any:
155
181
  from physure.application.factories import QuantityFactory
156
182
 
@@ -283,6 +309,8 @@ for _attr in ("equivalencies", "spectral", "thermodynamic"):
283
309
  _ATTR_LOADERS[_attr] = _load_equivalencies
284
310
  for _attr in _PLOT_ATTRS:
285
311
  _ATTR_LOADERS[_attr] = _load_plotting
312
+ for _attr in _HELPER_ATTRS:
313
+ _ATTR_LOADERS[_attr] = _load_helpers
286
314
  del _attr
287
315
 
288
316
 
@@ -332,15 +360,28 @@ __all__ = [
332
360
  "Uncertainty",
333
361
  "UnitNotFoundError",
334
362
  "UnknownUnitError",
363
+ "acos",
364
+ "approx_eq",
365
+ "asin",
366
+ "atan",
367
+ "atan2",
368
+ "cos",
369
+ "cosh",
335
370
  "create_default_system",
336
371
  "create_system",
337
372
  "default_system",
373
+ "e",
338
374
  "equivalencies",
375
+ "exp",
339
376
  "get_active_system",
340
377
  "get_current_system",
341
378
  "get_unit",
342
379
  "jit",
380
+ "linspace",
343
381
  "load_state",
382
+ "log",
383
+ "log10",
384
+ "pi",
344
385
  "plot",
345
386
  "plot_covariance",
346
387
  "plot_interactive",
@@ -348,8 +389,13 @@ __all__ = [
348
389
  "plot_parallel_coordinates",
349
390
  "plot_slices",
350
391
  "save_state",
392
+ "sin",
393
+ "sinh",
394
+ "sqrt",
351
395
  "spectral",
352
396
  "system_context",
397
+ "tan",
398
+ "tanh",
353
399
  "thermodynamic",
354
400
  "uncertainty_mode",
355
401
  "units",
@@ -1144,10 +1144,24 @@ class ArithmeticMixin:
1144
1144
  self._record_op(op, other, res)
1145
1145
  return res
1146
1146
 
1147
+ def _reject_bare_number_add_sub(self, other: Quantity | Numeric) -> None:
1148
+ """A bare number carries no unit: '5 m + 2' must raise, never.
1149
+
1150
+ silently assume the 2 is metres. Only a dimensionless quantity
1151
+ may absorb bare numbers additively.
1152
+ """
1153
+ if isinstance(other, _q()) or self.dimension.is_dimensionless:
1154
+ return
1155
+ if isinstance(other, (int, float, complex)) or self._backend.is_array(
1156
+ other
1157
+ ):
1158
+ raise IncompatibleUnitsError(self.unit, CompoundUnit({}))
1159
+
1147
1160
  def _add_sub_impl(
1148
1161
  self, other: Quantity | Numeric, is_add: bool
1149
1162
  ) -> Quantity:
1150
1163
  op = "add" if is_add else "sub"
1164
+ self._reject_bare_number_add_sub(other)
1151
1165
  rust_res = self._check_and_handle_rust_propagation(other, op)
1152
1166
  if rust_res is not None:
1153
1167
  return rust_res
@@ -1186,6 +1200,7 @@ class ArithmeticMixin:
1186
1200
 
1187
1201
  def __rsub__(self, other: Quantity | Numeric) -> Quantity:
1188
1202
  """Right subtraction."""
1203
+ self._reject_bare_number_add_sub(other)
1189
1204
  rust_res = self._check_and_handle_rust_propagation(other, "rsub")
1190
1205
  if rust_res is not None:
1191
1206
  return rust_res
@@ -140,8 +140,8 @@ def _add_like_unit(nodes: tuple[Node, ...]) -> CompoundUnit | None:
140
140
  return result
141
141
 
142
142
 
143
- def _infer_unit_none(node: Node) -> CompoundUnit | None:
144
- return None
143
+ def _infer_unit_number(node: Number) -> CompoundUnit | None:
144
+ return CompoundUnit({})
145
145
 
146
146
 
147
147
  def _infer_unit_quantity(node: Quantity) -> CompoundUnit | None:
@@ -198,9 +198,13 @@ def _infer_unit_transcendental(
198
198
  return None
199
199
 
200
200
 
201
+ def _infer_unit_symbol(node: Symbol) -> CompoundUnit | None:
202
+ return None
203
+
204
+
201
205
  _INFER_UNIT_DISPATCH: dict[type, Callable[[Any], CompoundUnit | None]] = {
202
- Number: _infer_unit_none,
203
- Symbol: _infer_unit_none,
206
+ Number: _infer_unit_number,
207
+ Symbol: _infer_unit_symbol,
204
208
  Quantity: _infer_unit_quantity,
205
209
  Add: _infer_unit_add,
206
210
  Sub: _infer_unit_sub,
@@ -73,7 +73,7 @@ _NUMBER_PAT = r"\d+\.?\d*(?:[eE]\s*[+-]?\s*\d+)?|\.\d+(?:[eE]\s*[+-]?\s*\d+)?"
73
73
  _IDENT_PAT = r"[^\W\d]\w*"
74
74
  _SUP_PAT = r"[⁻⁰¹²³⁴⁵⁶⁷⁸⁹]+"
75
75
  _SQRT_PAT = r"√"
76
- _OP_PAT = r"\+/-|±|<=|>=|!=|==|=>|->|\*\s*\*|\*\*|[-+*/^()=?<>×÷,:|]" # noqa: RUF001
76
+ _OP_PAT = r"\+/-|±|<=|>=|!=|==|=>|->|≈|\*\s*\*|\*\*|[-+*/^()=?<>×÷,:|]" # noqa: RUF001
77
77
 
78
78
  _OP_ALIASES = {"×": "*", "÷": "/"} # noqa: RUF001
79
79
  _TOKEN_RE = re.compile(
@@ -158,6 +158,25 @@ def _top_level_index(tokens: list[Token], op: str) -> int:
158
158
  return -1
159
159
 
160
160
 
161
+ def _approx_eq(
162
+ a: Any, b: Any, rel_tol: float = 0.1, abs_tol: float = 1e-5
163
+ ) -> bool:
164
+ a_mag = getattr(a, "magnitude", a)
165
+ b_mag = getattr(b, "magnitude", b)
166
+ if hasattr(a, "to") and hasattr(b, "unit"):
167
+ try:
168
+ b_converted = b.to(a.unit)
169
+ b_mag = b_converted.magnitude
170
+ except Exception:
171
+ return False
172
+ try:
173
+ return math.isclose(
174
+ float(a_mag), float(b_mag), rel_tol=rel_tol, abs_tol=abs_tol
175
+ )
176
+ except Exception:
177
+ return False
178
+
179
+
161
180
  _COMPARISONS: dict[str, Callable[[Any, Any], bool]] = {
162
181
  "<": operator.lt,
163
182
  ">": operator.gt,
@@ -165,6 +184,7 @@ _COMPARISONS: dict[str, Callable[[Any, Any], bool]] = {
165
184
  ">=": operator.ge,
166
185
  "==": operator.eq,
167
186
  "!=": operator.ne,
187
+ "≈": _approx_eq,
168
188
  }
169
189
 
170
190
 
@@ -511,9 +531,160 @@ _FUNCTIONS: dict[str, tuple[int, float, Callable[..., GrammarValue]]] = {
511
531
  "exp": (1, 1, lambda x: _transcendental(x, "exp")),
512
532
  "log": (1, 1, lambda x: _transcendental(x, "log")),
513
533
  "ln": (1, 1, lambda x: _transcendental(x, "log")),
534
+ "linspace": (2, 3, lambda *a: _linspace_fn(*a)),
535
+ "plot": (1, 3, lambda *a: _plot_fn(*a)),
514
536
  }
515
537
 
516
538
 
539
+ def _linspace_fn(
540
+ start: GrammarValue, stop: GrammarValue, num: GrammarValue = 50
541
+ ) -> Any:
542
+ import numpy as np
543
+
544
+ from physure.domain.measurement.quantity import Quantity
545
+
546
+ start_mag = getattr(start, "magnitude", start)
547
+ stop_mag = getattr(stop, "magnitude", stop)
548
+ unit = getattr(start, "unit", getattr(stop, "unit", None))
549
+ system = getattr(start, "system", getattr(stop, "system", None))
550
+
551
+ arr = np.linspace(float(start_mag), float(stop_mag), int(num))
552
+ if unit is not None:
553
+ return Quantity.from_input(arr, unit, system)
554
+ return arr
555
+
556
+
557
+ def _format_unit_str(unit_raw: str) -> str:
558
+ u = str(unit_raw).strip()
559
+ if u in ("1", "", "None"):
560
+ return ""
561
+ aliases = {
562
+ "kg·m/s²": "N",
563
+ "kg*m/s^2": "N",
564
+ "kg·m³/(A·s³)": "N/C",
565
+ "A·s": "C",
566
+ }
567
+ return aliases.get(u, u)
568
+
569
+
570
+ def _draw_ascii_plot(
571
+ x: Any, y: Any, title: str = "Plot", x_unit: str = "", y_unit: str = ""
572
+ ) -> str:
573
+ import numpy as np
574
+
575
+ x_arr = np.asarray(x, dtype=float)
576
+ y_arr = np.asarray(y, dtype=float)
577
+ if len(x_arr) == 0 or len(y_arr) == 0:
578
+ return f"📊 {title}: [No data points]"
579
+
580
+ width = 46
581
+ height = 8
582
+
583
+ x_grid = np.linspace(x_arr.min(), x_arr.max(), width)
584
+ y_grid = np.interp(x_grid, x_arr, y_arr)
585
+
586
+ y_min, y_max = y_grid.min(), y_grid.max()
587
+ y_span = (y_max - y_min) if (y_max != y_min) else 1.0
588
+
589
+ fmt_x = _format_unit_str(x_unit)
590
+ fmt_y = _format_unit_str(y_unit)
591
+
592
+ lines = []
593
+ lines.append(f" 📊 {title}")
594
+ lines.append(f" {y_max:.3e} {fmt_y}".strip().rjust(18) + " ┐")
595
+
596
+ for r in range(height - 1, -1, -1):
597
+ y_level = y_min + (r / (height - 1)) * y_span
598
+ row_chars = []
599
+ for c in range(width):
600
+ val = y_grid[c]
601
+ diff = abs(val - y_level) / y_span
602
+ if diff < (1.0 / (2 * height)):
603
+ row_chars.append("█")
604
+ elif val > y_level:
605
+ row_chars.append("░")
606
+ else:
607
+ row_chars.append(" ")
608
+ lines.append(" │ " + "".join(row_chars))
609
+
610
+ lines.append(
611
+ f" {y_min:.3e} {fmt_y}".strip().rjust(18) + " └" + "─" * width
612
+ )
613
+ x_min_str = f"{x_arr.min():.3e} {fmt_x}".strip()
614
+ x_max_str = f"{x_arr.max():.3e} {fmt_x}".strip()
615
+ pad_len = max(1, width - len(x_min_str) - len(x_max_str) + 12)
616
+ lines.append(
617
+ " " + x_min_str + " " * pad_len + x_max_str
618
+ )
619
+
620
+ return "\n".join(lines)
621
+
622
+
623
+ def _plot_fn(*args: Any, **kwargs: Any) -> Any:
624
+ import base64
625
+ import io
626
+
627
+ import matplotlib
628
+
629
+ matplotlib.use("Agg")
630
+ import matplotlib.pyplot as plt
631
+ import numpy as np
632
+
633
+ x_val = args[0] if len(args) >= 2 else None
634
+ y_val = args[1] if len(args) >= 2 else args[0]
635
+
636
+ y_mag = getattr(y_val, "magnitude", y_val)
637
+ y_unit = _format_unit_str(getattr(y_val, "unit", ""))
638
+ y_arr = np.asarray(y_mag, dtype=float)
639
+
640
+ if x_val is not None:
641
+ x_mag = getattr(x_val, "magnitude", x_val)
642
+ x_unit = _format_unit_str(getattr(x_val, "unit", ""))
643
+ x_arr = np.asarray(x_mag, dtype=float)
644
+ else:
645
+ x_arr = np.arange(len(y_arr))
646
+ x_unit = ""
647
+
648
+ title = str(kwargs.get("title", "Physure Live Plot"))
649
+ ascii_plot = _draw_ascii_plot(
650
+ x_arr, y_arr, title=title, x_unit=x_unit, y_unit=y_unit
651
+ )
652
+
653
+ fig, ax = plt.subplots(figsize=(6.5, 3.8))
654
+ ax.plot(x_arr, y_arr, color="#4ec9b0", linewidth=2.5)
655
+ ax.fill_between(x_arr, y_arr, color="#4ec9b0", alpha=0.12)
656
+
657
+ x_label_str = f"x ({x_unit})" if x_unit else "x"
658
+ y_label_str = f"y ({y_unit})" if y_unit else "y"
659
+
660
+ ax.set_xlabel(x_label_str, color="#cccccc", fontsize=9.5)
661
+ ax.set_ylabel(y_label_str, color="#cccccc", fontsize=9.5)
662
+
663
+ ax.set_title(
664
+ title, color="#569cd6", fontsize=11, fontweight="bold", pad=12
665
+ )
666
+ ax.grid(True, linestyle=":", alpha=0.35, color="#666666")
667
+ ax.tick_params(colors="#cccccc", labelsize=8.5)
668
+
669
+ for spine in ax.spines.values():
670
+ spine.set_color("#444444")
671
+ ax.spines["top"].set_visible(False)
672
+ ax.spines["right"].set_visible(False)
673
+
674
+ fig.patch.set_facecolor("#1e1e1e")
675
+ ax.set_facecolor("#252526")
676
+ fig.tight_layout()
677
+
678
+ buf = io.BytesIO()
679
+ fig.savefig(buf, format="png", bbox_inches="tight", dpi=140)
680
+ buf.seek(0)
681
+ b64 = base64.b64encode(buf.read()).decode("utf-8")
682
+ plt.close(fig)
683
+
684
+ b64_tag = f"[PLOT_IMAGE:data:image/png;base64,{b64}]"
685
+ return f"{ascii_plot}\n{b64_tag}"
686
+
687
+
517
688
  def _format_sig_figs(val: GrammarValue, sig_figs: int) -> GrammarValue:
518
689
  """Formats a scalar or Quantity magnitude to sig_figs significant figures."""
519
690
  mag = getattr(val, "magnitude", val)
@@ -584,7 +755,7 @@ class GrammarInterpreter:
584
755
  text = text[1:]
585
756
  if text.endswith("\n"):
586
757
  text = text[:-1]
587
- results.append(text)
758
+ results.append(self._interpolate_text(text))
588
759
  current_line += match.group(0).count("\n")
589
760
  pos = match.end()
590
761
 
@@ -592,6 +763,21 @@ class GrammarInterpreter:
592
763
  results.extend(self._run_segment(tail, start_line_num=current_line))
593
764
  return results
594
765
 
766
+ def _interpolate_text(self, text: str) -> str:
767
+ def replacer(m: re.Match[str]) -> str:
768
+ expr = m.group(1).strip()
769
+ try:
770
+ res_list = self._run_segment(expr)
771
+ if res_list:
772
+ val = res_list[-1]
773
+ if val is not None:
774
+ return str(val)
775
+ except Exception as e:
776
+ return f"{{ERR: {e}}}"
777
+ return m.group(0)
778
+
779
+ return re.sub(r"\{([^}]+)\}", replacer, text)
780
+
595
781
  def _run_segment(
596
782
  self, segment: str, start_line_num: int = 1
597
783
  ) -> list[GrammarValue | None]:
@@ -0,0 +1,146 @@
1
+ """Helper functions for exported Physure DSL scripts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+ from physure.application.factories import QuantityFactory
9
+
10
+ Q_ = QuantityFactory()
11
+
12
+ pi = math.pi
13
+ e = math.e
14
+
15
+
16
+ def _to_float(val: Any) -> float:
17
+ return float(getattr(val, "magnitude", val))
18
+
19
+
20
+ def approx_eq(
21
+ a: Any, b: Any, rel_tol: float = 0.1, abs_tol: float = 1e-5
22
+ ) -> bool:
23
+ """Check approximate equality between numbers or Quantities."""
24
+ if hasattr(a, "approx_eq") and hasattr(b, "_core") and hasattr(a, "_core"):
25
+ try:
26
+ return bool(a._core.approx_eq(b._core, rel_tol, abs_tol))
27
+ except Exception:
28
+ pass
29
+ a_mag = getattr(a, "magnitude", a)
30
+ b_mag = getattr(b, "magnitude", b)
31
+ if hasattr(a, "to") and hasattr(b, "unit"):
32
+ try:
33
+ b_mag = b.to(a.unit).magnitude
34
+ except Exception:
35
+ return False
36
+ try:
37
+ return math.isclose(
38
+ float(a_mag), float(b_mag), rel_tol=rel_tol, abs_tol=abs_tol
39
+ )
40
+ except Exception:
41
+ return False
42
+
43
+
44
+ def linspace(start: Any, stop: Any, num: int = 50) -> Any:
45
+ """Generate linearly spaced numbers or Quantities."""
46
+ import numpy as np
47
+
48
+ start_val = getattr(start, "magnitude", start)
49
+ stop_val = getattr(stop, "magnitude", stop)
50
+ unit = getattr(start, "unit", getattr(stop, "unit", None))
51
+ arr = np.linspace(float(start_val), float(stop_val), int(num))
52
+ if unit:
53
+ return Q_(arr, str(unit))
54
+ return arr
55
+
56
+
57
+ def plot(*args: Any) -> None:
58
+ """Plot physical quantities using matplotlib."""
59
+ try:
60
+ import matplotlib.pyplot as plt
61
+
62
+ _fig, ax = plt.subplots()
63
+ if len(args) >= 2:
64
+ x, y = args[0], args[1]
65
+ x_val = getattr(x, "magnitude", x)
66
+ y_val = getattr(y, "magnitude", y)
67
+ ax.plot(x_val, y_val)
68
+ elif len(args) == 1:
69
+ y = args[0]
70
+ y_val = getattr(y, "magnitude", y)
71
+ ax.plot(y_val)
72
+ plt.show()
73
+ except Exception as err:
74
+ print(f"Plot error: {err}")
75
+
76
+
77
+ def sqrt(x: Any) -> Any:
78
+ """Square root supporting Quantities and numbers."""
79
+ if hasattr(x, "unit") or hasattr(x, "magnitude"):
80
+ return x**0.5
81
+ return math.sqrt(float(x))
82
+
83
+
84
+ def sin(x: Any) -> float:
85
+ """Sine of scalar value."""
86
+ return math.sin(_to_float(x))
87
+
88
+
89
+ def cos(x: Any) -> float:
90
+ """Cosine of scalar value."""
91
+ return math.cos(_to_float(x))
92
+
93
+
94
+ def tan(x: Any) -> float:
95
+ """Tangent of scalar value."""
96
+ return math.tan(_to_float(x))
97
+
98
+
99
+ def asin(x: Any) -> float:
100
+ """Arcsine of scalar value."""
101
+ return math.asin(_to_float(x))
102
+
103
+
104
+ def acos(x: Any) -> float:
105
+ """Arccosine of scalar value."""
106
+ return math.acos(_to_float(x))
107
+
108
+
109
+ def atan(x: Any) -> float:
110
+ """Arctangent of scalar value."""
111
+ return math.atan(_to_float(x))
112
+
113
+
114
+ def atan2(y: Any, x: Any) -> float:
115
+ """Two-argument arctangent of scalar values."""
116
+ return math.atan2(_to_float(y), _to_float(x))
117
+
118
+
119
+ def sinh(x: Any) -> float:
120
+ """Hyperbolic sine of scalar value."""
121
+ return math.sinh(_to_float(x))
122
+
123
+
124
+ def cosh(x: Any) -> float:
125
+ """Hyperbolic cosine of scalar value."""
126
+ return math.cosh(_to_float(x))
127
+
128
+
129
+ def tanh(x: Any) -> float:
130
+ """Hyperbolic tangent of scalar value."""
131
+ return math.tanh(_to_float(x))
132
+
133
+
134
+ def exp(x: Any) -> float:
135
+ """Exponential of scalar value."""
136
+ return math.exp(_to_float(x))
137
+
138
+
139
+ def log(x: Any) -> float:
140
+ """Natural logarithm of scalar value."""
141
+ return math.log(_to_float(x))
142
+
143
+
144
+ def log10(x: Any) -> float:
145
+ """Base-10 logarithm of scalar value."""
146
+ return math.log10(_to_float(x))
@@ -24,7 +24,15 @@ _BANNER = (
24
24
  def _print_results(results: list[Any]) -> None:
25
25
  for result in results:
26
26
  if result is not None:
27
- print(result)
27
+ s = str(result)
28
+ lines = [
29
+ line
30
+ for line in s.split("\n")
31
+ if not line.startswith("[PLOT_IMAGE:")
32
+ ]
33
+ clean_str = "\n".join(lines).rstrip()
34
+ if clean_str:
35
+ print(clean_str)
28
36
 
29
37
 
30
38
  def _run_source(source: str) -> int:
@@ -1,7 +1,14 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Alexisrx96/physure/main/assets/logo-horizontal-dark.svg">
3
+ <img src="https://raw.githubusercontent.com/Alexisrx96/physure/main/assets/logo-horizontal-light.svg" alt="physure" width="380">
4
+ </picture>
5
+
1
6
  # physure 🦀
2
7
 
3
- [![crates.io](https://img.shields.io/crates/v/physure)](https://crates.io/crates/physure)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/Alexisrx96/physure/blob/main/LICENSE)
8
+ [![crates.io](https://img.shields.io/crates/v/physure?color=F59E0B&labelColor=18181A)](https://crates.io/crates/physure)
9
+ [![docs.rs](https://img.shields.io/docsrs/physure?labelColor=18181A)](https://docs.rs/physure)
10
+ [![CI](https://img.shields.io/github/actions/workflow/status/Alexisrx96/physure/tests.yml?branch=main&labelColor=18181A)](https://github.com/Alexisrx96/physure/actions/workflows/tests.yml)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-F59E0B?labelColor=18181A)](https://github.com/Alexisrx96/physure/blob/main/LICENSE)
5
12
 
6
13
  **Pure Rust physics engine: dimensional analysis with rational exponents, physical quantities, and uncertainty propagation.**
7
14