ucon 0.5.2__tar.gz → 0.6.0__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.
- {ucon-0.5.2 → ucon-0.6.0}/.github/workflows/publish.yaml +5 -5
- {ucon-0.5.2 → ucon-0.6.0}/.github/workflows/tests.yaml +4 -6
- ucon-0.6.0/Makefile +129 -0
- {ucon-0.5.2 → ucon-0.6.0}/PKG-INFO +73 -97
- {ucon-0.5.2 → ucon-0.6.0}/README.md +60 -87
- {ucon-0.5.2 → ucon-0.6.0}/ROADMAP.md +17 -12
- ucon-0.5.2/docs/decisions/unity-distance-metric-for-nearest-scale.md → ucon-0.6.0/docs/decisions/001-unity-distance-metric-for-nearest-scale.md +35 -17
- ucon-0.6.0/docs/decisions/002-composite-units.md +116 -0
- ucon-0.6.0/docs/decisions/003-composable-unit-algebra.md +123 -0
- ucon-0.5.2/docs/decisions/unit-algebra-naming.md → ucon-0.6.0/docs/decisions/004-unit-algebra-naming.md +49 -48
- ucon-0.5.2/docs/decisions/pseudo-dimension-tuple-values.md → ucon-0.6.0/docs/decisions/005-pseudo-dimension-tuple-values.md +39 -75
- ucon-0.6.0/docs/decisions/006-pydantic-integration-pattern.md +92 -0
- ucon-0.6.0/docs/examples/basis-transform-fantasy-units.md +89 -0
- ucon-0.6.0/pyproject.toml +86 -0
- ucon-0.6.0/requirements.txt +1 -0
- {ucon-0.5.2 → ucon-0.6.0}/setup.py +9 -0
- ucon-0.6.0/tests/ucon/mcp/__init__.py +1 -0
- ucon-0.6.0/tests/ucon/mcp/test_server.py +351 -0
- ucon-0.6.0/tests/ucon/test_pickle.py +197 -0
- ucon-0.6.0/tests/ucon/test_pydantic.py +350 -0
- ucon-0.6.0/tests/ucon/test_unit_parsing.py +290 -0
- {ucon-0.5.2 → ucon-0.6.0}/ucon/__init__.py +4 -1
- {ucon-0.5.2 → ucon-0.6.0}/ucon/core.py +7 -2
- ucon-0.6.0/ucon/mcp/__init__.py +8 -0
- ucon-0.6.0/ucon/mcp/server.py +250 -0
- ucon-0.6.0/ucon/pydantic.py +199 -0
- {ucon-0.5.2 → ucon-0.6.0}/ucon/units.py +259 -11
- {ucon-0.5.2 → ucon-0.6.0}/ucon.egg-info/PKG-INFO +73 -97
- {ucon-0.5.2 → ucon-0.6.0}/ucon.egg-info/SOURCES.txt +21 -7
- ucon-0.6.0/ucon.egg-info/entry_points.txt +2 -0
- ucon-0.6.0/ucon.egg-info/requires.txt +11 -0
- ucon-0.6.0/ucon.egg-info/top_level.txt +1 -0
- ucon-0.6.0/uv.lock +1603 -0
- ucon-0.5.2/docs/decisions/composable-unit-algebra.md +0 -241
- ucon-0.5.2/docs/decisions/composite-units.md +0 -160
- ucon-0.5.2/noxfile.py +0 -132
- ucon-0.5.2/requirements.txt +0 -2
- ucon-0.5.2/ucon.egg-info/top_level.txt +0 -2
- {ucon-0.5.2 → ucon-0.6.0}/.gitignore +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/LICENSE +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/NOTICE +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/docs/explainers/exponent-scale-relationship.md +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/docs/explainers/type-operation-matrix.md +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/docs/explainers/why-algebraic-closure-matters.md +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/docs/explainers/why-type-safety-matters.md +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/docs/proposals/interface-unifying-the-value-layer.md +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/docs/proposals/project_unified-algebraic-core.md +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/docs/proposals/support-for-fractional-exponents.md +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/docs/proposals/unified-unit-presentation.md +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/setup.cfg +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/__init__.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/__init__.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/conversion/__init__.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/conversion/test_graph.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/conversion/test_map.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/test_algebra.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/test_basis_transform.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/test_core.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/test_default_graph_conversions.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/test_dimensionless_units.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/test_graph_basis_transform.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/test_quantity.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/test_rebased_unit.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/test_uncertainty.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/test_unit_system.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/test_units.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/tests/ucon/test_vector_fraction.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/ucon/algebra.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/ucon/graph.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/ucon/maps.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/ucon/quantity.py +0 -0
- {ucon-0.5.2 → ucon-0.6.0}/ucon.egg-info/dependency_links.txt +0 -0
|
@@ -16,16 +16,16 @@ jobs:
|
|
|
16
16
|
fetch-depth: 0 # needed for ancestry check
|
|
17
17
|
ref: ${{ github.ref_name }}
|
|
18
18
|
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v5
|
|
21
|
+
|
|
19
22
|
- name: Set up Python
|
|
20
23
|
uses: actions/setup-python@v5
|
|
21
24
|
with:
|
|
22
25
|
python-version: '3.14'
|
|
23
26
|
|
|
24
|
-
- name: Install dependencies
|
|
25
|
-
run: pip install -r requirements.txt
|
|
26
|
-
|
|
27
27
|
- name: Build distribution 📦
|
|
28
|
-
run:
|
|
28
|
+
run: LOCAL_VERSION_SCHEME=true make build
|
|
29
29
|
|
|
30
30
|
- name: Publish to Test PyPI
|
|
31
31
|
if: github.ref == 'refs/heads/main'
|
|
@@ -39,7 +39,7 @@ jobs:
|
|
|
39
39
|
- name: Verify tag is on mainline
|
|
40
40
|
if: startsWith(github.ref, 'refs/tags/')
|
|
41
41
|
run: |
|
|
42
|
-
git fetch origin main --
|
|
42
|
+
git fetch origin main --unshallow 2>/dev/null || git fetch origin main
|
|
43
43
|
TAG=${GITHUB_REF#refs/tags/}
|
|
44
44
|
TAG_COMMIT=$(git rev-list -n 1 "$TAG")
|
|
45
45
|
if ! git merge-base --is-ancestor "$TAG_COMMIT" origin/main; then
|
|
@@ -26,18 +26,16 @@ jobs:
|
|
|
26
26
|
- name: Checkout code
|
|
27
27
|
uses: actions/checkout@v5
|
|
28
28
|
|
|
29
|
+
- name: Install uv
|
|
30
|
+
uses: astral-sh/setup-uv@v5
|
|
31
|
+
|
|
29
32
|
- name: Set up Python ${{ matrix.python-version }}
|
|
30
33
|
uses: actions/setup-python@v6
|
|
31
34
|
with:
|
|
32
35
|
python-version: ${{ matrix.python-version }}
|
|
33
36
|
|
|
34
|
-
- name: Install dependencies
|
|
35
|
-
run: |
|
|
36
|
-
python -m pip install --upgrade pip
|
|
37
|
-
pip install -r requirements.txt
|
|
38
|
-
|
|
39
37
|
- name: Run tests
|
|
40
|
-
run:
|
|
38
|
+
run: make test PYTHON=${{ matrix.python-version }}
|
|
41
39
|
|
|
42
40
|
- name: Upload coverage to Codecov
|
|
43
41
|
if: success() && github.repository_owner == 'withtwoemms'
|
ucon-0.6.0/Makefile
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# © 2025 The Radiativity Company
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
# See the LICENSE file for details.
|
|
4
|
+
|
|
5
|
+
# --- Configuration ---
|
|
6
|
+
PROJECTNAME := ucon
|
|
7
|
+
PYTHON ?= 3.12
|
|
8
|
+
SUPPORTED_PYTHONS := 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14
|
|
9
|
+
UV_VENV ?= .${PROJECTNAME}-${PYTHON}
|
|
10
|
+
UV_INSTALLED := .uv-installed
|
|
11
|
+
DEPS_INSTALLED := ${UV_VENV}/.deps-installed
|
|
12
|
+
TESTDIR := tests/
|
|
13
|
+
TESTNAME ?=
|
|
14
|
+
COVERAGE ?= true
|
|
15
|
+
|
|
16
|
+
# --- Color Setup ---
|
|
17
|
+
GREEN := \033[0;32m
|
|
18
|
+
CYAN := \033[0;36m
|
|
19
|
+
YELLOW := \033[1;33m
|
|
20
|
+
RESET := \033[0m
|
|
21
|
+
|
|
22
|
+
# --- Help Command ---
|
|
23
|
+
.PHONY: help
|
|
24
|
+
help:
|
|
25
|
+
@echo "\n${YELLOW}ucon Development Commands${RESET}\n"
|
|
26
|
+
@echo " ${CYAN}install${RESET} - Install package with all extras"
|
|
27
|
+
@echo " ${CYAN}install-test${RESET} - Install with test dependencies only"
|
|
28
|
+
@echo " ${CYAN}test${RESET} - Run tests (PYTHON=X.Y for specific version)"
|
|
29
|
+
@echo " ${CYAN}test-all${RESET} - Run tests across all supported Python versions"
|
|
30
|
+
@echo " ${CYAN}coverage${RESET} - Generate coverage report"
|
|
31
|
+
@echo " ${CYAN}build${RESET} - Build source and wheel distributions"
|
|
32
|
+
@echo " ${CYAN}venv${RESET} - Create virtual environment"
|
|
33
|
+
@echo " ${CYAN}clean${RESET} - Remove build artifacts and caches"
|
|
34
|
+
@echo ""
|
|
35
|
+
@echo "${YELLOW}Variables:${RESET}\n"
|
|
36
|
+
@echo " PYTHON=${PYTHON} - Python version for test target"
|
|
37
|
+
@echo " UV_VENV=${UV_VENV} - Path to virtual environment"
|
|
38
|
+
@echo " TESTNAME= - Specific test to run (e.g., tests.ucon.test_core)"
|
|
39
|
+
@echo " COVERAGE=${COVERAGE} - Enable coverage (true/false)"
|
|
40
|
+
@echo ""
|
|
41
|
+
|
|
42
|
+
# --- uv Installation ---
|
|
43
|
+
${UV_INSTALLED}:
|
|
44
|
+
@command -v uv >/dev/null 2>&1 || { \
|
|
45
|
+
echo "${GREEN}Installing uv...${RESET}"; \
|
|
46
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh; \
|
|
47
|
+
}
|
|
48
|
+
@touch ${UV_INSTALLED}
|
|
49
|
+
|
|
50
|
+
# --- Virtual Environment ---
|
|
51
|
+
${UV_VENV}: ${UV_INSTALLED}
|
|
52
|
+
@echo "${GREEN}Creating virtual environment at ${UV_VENV}...${RESET}"
|
|
53
|
+
@uv venv --python ${PYTHON} ${UV_VENV}
|
|
54
|
+
|
|
55
|
+
${DEPS_INSTALLED}: pyproject.toml uv.lock | ${UV_VENV}
|
|
56
|
+
@echo "${GREEN}Syncing dependencies into ${UV_VENV}...${RESET}"
|
|
57
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv sync --python ${PYTHON} --extra test --extra pydantic --extra mcp
|
|
58
|
+
@touch ${DEPS_INSTALLED}
|
|
59
|
+
|
|
60
|
+
.PHONY: venv
|
|
61
|
+
venv: ${DEPS_INSTALLED}
|
|
62
|
+
@echo "${CYAN}Virtual environment ready at ${UV_VENV}${RESET}"
|
|
63
|
+
@echo "${CYAN}Activate with:${RESET} source ${UV_VENV}/bin/activate"
|
|
64
|
+
|
|
65
|
+
# --- Installation ---
|
|
66
|
+
.PHONY: install-test
|
|
67
|
+
install-test: ${UV_VENV}
|
|
68
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv sync --python ${PYTHON} --extra test
|
|
69
|
+
|
|
70
|
+
.PHONY: install
|
|
71
|
+
install: ${UV_VENV}
|
|
72
|
+
@echo "${GREEN}Installing with all extras into ${UV_VENV}...${RESET}"
|
|
73
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv sync --python ${PYTHON} --extra test --extra pydantic --extra mcp
|
|
74
|
+
|
|
75
|
+
# --- Testing ---
|
|
76
|
+
.PHONY: test
|
|
77
|
+
test: ${DEPS_INSTALLED}
|
|
78
|
+
@echo "${GREEN}Running tests with Python ${PYTHON}...${RESET}"
|
|
79
|
+
ifeq ($(COVERAGE),true)
|
|
80
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage run --source=ucon --branch \
|
|
81
|
+
--omit="**/tests/*,**/site-packages/*.py,setup.py" \
|
|
82
|
+
-m unittest $(if $(TESTNAME),$(TESTNAME),discover --start-directory ${TESTDIR} --top-level-directory .)
|
|
83
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage report -m
|
|
84
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage xml
|
|
85
|
+
else
|
|
86
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} -m unittest \
|
|
87
|
+
$(if $(TESTNAME),$(TESTNAME),discover --start-directory ${TESTDIR} --top-level-directory .)
|
|
88
|
+
endif
|
|
89
|
+
|
|
90
|
+
.PHONY: test-all
|
|
91
|
+
test-all: ${UV_INSTALLED}
|
|
92
|
+
@echo "${GREEN}Running tests across all supported Python versions...${RESET}"
|
|
93
|
+
@for pyver in $(SUPPORTED_PYTHONS); do \
|
|
94
|
+
echo "\n${CYAN}=== Python $$pyver ===${RESET}"; \
|
|
95
|
+
uv run --python $$pyver -m unittest discover \
|
|
96
|
+
--start-directory ${TESTDIR} --top-level-directory . \
|
|
97
|
+
|| echo "${YELLOW}Python $$pyver: FAILED or not available${RESET}"; \
|
|
98
|
+
done
|
|
99
|
+
|
|
100
|
+
# --- Coverage ---
|
|
101
|
+
.PHONY: coverage
|
|
102
|
+
coverage: ${DEPS_INSTALLED}
|
|
103
|
+
@echo "${GREEN}Generating coverage report...${RESET}"
|
|
104
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage report -m
|
|
105
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage html
|
|
106
|
+
@echo "${CYAN}HTML report at htmlcov/index.html${RESET}"
|
|
107
|
+
|
|
108
|
+
# --- Building ---
|
|
109
|
+
.PHONY: build
|
|
110
|
+
build: ${UV_INSTALLED}
|
|
111
|
+
@echo "${GREEN}Building distributions...${RESET}"
|
|
112
|
+
@uv build
|
|
113
|
+
@echo "${CYAN}Distributions at dist/${RESET}"
|
|
114
|
+
|
|
115
|
+
# --- Cleaning ---
|
|
116
|
+
.PHONY: clean
|
|
117
|
+
clean:
|
|
118
|
+
@echo "${GREEN}Cleaning build artifacts...${RESET}"
|
|
119
|
+
@rm -rf dist/ build/ *.egg-info/
|
|
120
|
+
@rm -rf ${UV_VENV} ${DEPS_INSTALLED} ${UV_INSTALLED}
|
|
121
|
+
@rm -rf .uv_cache .pytest_cache htmlcov/
|
|
122
|
+
@rm -f coverage.xml .coverage
|
|
123
|
+
@find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
|
124
|
+
@echo "${CYAN}Clean complete.${RESET}"
|
|
125
|
+
|
|
126
|
+
.PHONY: clean-all
|
|
127
|
+
clean-all: clean
|
|
128
|
+
@echo "${YELLOW}Removing uv.lock...${RESET}"
|
|
129
|
+
@rm -f uv.lock
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ucon
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: A tool for dimensional analysis: a 'Unit CONverter'
|
|
5
5
|
Home-page: https://github.com/withtwoemms/ucon
|
|
6
6
|
Author: Emmanuel I. Obi
|
|
7
|
+
Author-email: "Emmanuel I. Obi" <withtwoemms@gmail.com>
|
|
7
8
|
Maintainer: Emmanuel I. Obi
|
|
8
|
-
Maintainer-email: withtwoemms@gmail.com
|
|
9
|
+
Maintainer-email: "Emmanuel I. Obi" <withtwoemms@gmail.com>
|
|
9
10
|
License: Apache-2.0
|
|
11
|
+
Project-URL: Homepage, https://github.com/withtwoemms/ucon
|
|
12
|
+
Project-URL: Repository, https://github.com/withtwoemms/ucon
|
|
10
13
|
Classifier: Development Status :: 4 - Beta
|
|
11
14
|
Classifier: Intended Audience :: Developers
|
|
12
15
|
Classifier: Intended Audience :: Education
|
|
13
16
|
Classifier: Intended Audience :: Science/Research
|
|
14
17
|
Classifier: Topic :: Software Development :: Build Tools
|
|
15
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
18
|
Classifier: Programming Language :: Python :: 3.7
|
|
17
19
|
Classifier: Programming Language :: Python :: 3.8
|
|
18
20
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -21,19 +23,20 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
21
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
25
|
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Requires-Python: >=3.7
|
|
24
27
|
Description-Content-Type: text/markdown
|
|
25
28
|
License-File: LICENSE
|
|
26
29
|
License-File: NOTICE
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: coverage[toml]>=5.5; extra == "test"
|
|
32
|
+
Provides-Extra: pydantic
|
|
33
|
+
Requires-Dist: pydantic>=2.0; extra == "pydantic"
|
|
34
|
+
Provides-Extra: mcp
|
|
35
|
+
Requires-Dist: mcp>=1.0; python_version >= "3.10" and extra == "mcp"
|
|
27
36
|
Dynamic: author
|
|
28
|
-
Dynamic: classifier
|
|
29
|
-
Dynamic: description
|
|
30
|
-
Dynamic: description-content-type
|
|
31
37
|
Dynamic: home-page
|
|
32
|
-
Dynamic: license
|
|
33
38
|
Dynamic: license-file
|
|
34
39
|
Dynamic: maintainer
|
|
35
|
-
Dynamic: maintainer-email
|
|
36
|
-
Dynamic: summary
|
|
37
40
|
|
|
38
41
|
<table>
|
|
39
42
|
<tr>
|
|
@@ -68,6 +71,7 @@ It combines **units**, **scales**, and **dimensions** into a composable algebra
|
|
|
68
71
|
- Metric and binary prefixes (`kilo`, `kibi`, `micro`, `mebi`, etc.)
|
|
69
72
|
- Pseudo-dimensions for angles, solid angles, and ratios with semantic isolation
|
|
70
73
|
- Uncertainty propagation through arithmetic and conversions
|
|
74
|
+
- Pydantic v2 integration for API validation and JSON serialization
|
|
71
75
|
- A clean foundation for physics, chemistry, data modeling, and beyond
|
|
72
76
|
|
|
73
77
|
Think of it as **`decimal.Decimal` for the physical world** — precise, predictable, and type-safe.
|
|
@@ -96,6 +100,7 @@ To best answer this question, we turn to an age-old technique ([dimensional anal
|
|
|
96
100
|
| **`BasisTransform`** | `ucon.core` | Matrix-based transformation between dimensional exponent spaces. | Converting between incompatible dimensional structures; exact arithmetic with `Fraction`. |
|
|
97
101
|
| **`RebasedUnit`** | `ucon.core` | A unit rebased to another system's dimension, preserving provenance. | Cross-basis conversions; tracking original unit through basis changes. |
|
|
98
102
|
| **`units` module** | `ucon.units` | Defines canonical unit instances (SI, imperial, information, and derived units). | Quick access to standard physical units (`units.meter`, `units.foot`, `units.byte`, etc.). |
|
|
103
|
+
| **`pydantic` module** | `ucon.pydantic` | Pydantic v2 integration with `Number` type for model validation and JSON serialization. | Using `Number` in Pydantic models; API request/response validation; JSON round-trip serialization. |
|
|
99
104
|
|
|
100
105
|
### Under the Hood
|
|
101
106
|
|
|
@@ -142,35 +147,40 @@ Simple:
|
|
|
142
147
|
pip install ucon
|
|
143
148
|
```
|
|
144
149
|
|
|
150
|
+
With Pydantic v2 support:
|
|
151
|
+
```bash
|
|
152
|
+
pip install ucon[pydantic]
|
|
153
|
+
```
|
|
154
|
+
|
|
145
155
|
## Usage
|
|
146
156
|
|
|
147
|
-
|
|
157
|
+
### Quantities and Arithmetic
|
|
158
|
+
|
|
159
|
+
Dimensional analysis like this:
|
|
148
160
|
```
|
|
149
161
|
2 mL bromine | 3.119 g bromine
|
|
150
162
|
--------------x----------------- #=> 6.238 g bromine
|
|
151
163
|
1 | 1 mL bromine
|
|
152
164
|
```
|
|
153
|
-
becomes straightforward
|
|
165
|
+
becomes straightforward:
|
|
154
166
|
```python
|
|
155
167
|
from ucon import Number, Scale, units
|
|
156
168
|
from ucon.quantity import Ratio
|
|
157
169
|
|
|
158
|
-
# Two milliliters of bromine
|
|
159
170
|
mL = Scale.milli * units.liter
|
|
160
171
|
two_mL_bromine = Number(quantity=2, unit=mL)
|
|
161
172
|
|
|
162
|
-
# Density of bromine: 3.119 g/mL
|
|
163
173
|
bromine_density = Ratio(
|
|
164
174
|
numerator=Number(unit=units.gram, quantity=3.119),
|
|
165
175
|
denominator=Number(unit=mL),
|
|
166
176
|
)
|
|
167
177
|
|
|
168
|
-
# Multiply to find mass
|
|
169
178
|
grams_bromine = bromine_density.evaluate() * two_mL_bromine
|
|
170
179
|
print(grams_bromine) # <6.238 g>
|
|
171
180
|
```
|
|
172
181
|
|
|
173
|
-
Scale
|
|
182
|
+
### Scale Prefixes
|
|
183
|
+
|
|
174
184
|
```python
|
|
175
185
|
km = Scale.kilo * units.meter # UnitProduct with kilo-scaled meter
|
|
176
186
|
mg = Scale.milli * units.gram # UnitProduct with milli-scaled gram
|
|
@@ -178,12 +188,12 @@ mg = Scale.milli * units.gram # UnitProduct with milli-scaled gram
|
|
|
178
188
|
print(km.shorthand) # 'km'
|
|
179
189
|
print(mg.shorthand) # 'mg'
|
|
180
190
|
|
|
181
|
-
# Scale arithmetic
|
|
182
191
|
print(km.fold_scale()) # 1000.0
|
|
183
192
|
print(mg.fold_scale()) # 0.001
|
|
184
193
|
```
|
|
185
194
|
|
|
186
|
-
|
|
195
|
+
### Callable Units and Conversion
|
|
196
|
+
|
|
187
197
|
```python
|
|
188
198
|
from ucon import units, Scale
|
|
189
199
|
|
|
@@ -202,16 +212,16 @@ distance_mi = distance.to(units.mile)
|
|
|
202
212
|
print(distance_mi) # <3.107... mi>
|
|
203
213
|
```
|
|
204
214
|
|
|
205
|
-
Dimensionless
|
|
215
|
+
### Dimensionless Units
|
|
216
|
+
|
|
217
|
+
Angles, solid angles, and ratios are semantically isolated:
|
|
206
218
|
```python
|
|
207
219
|
import math
|
|
208
220
|
from ucon import units
|
|
209
221
|
|
|
210
|
-
# Angle conversions
|
|
211
222
|
angle = units.radian(math.pi)
|
|
212
223
|
print(angle.to(units.degree)) # <180.0 deg>
|
|
213
224
|
|
|
214
|
-
# Ratio conversions
|
|
215
225
|
ratio = units.percent(50)
|
|
216
226
|
print(ratio.to(units.ppm)) # <500000.0 ppm>
|
|
217
227
|
|
|
@@ -219,101 +229,60 @@ print(ratio.to(units.ppm)) # <500000.0 ppm>
|
|
|
219
229
|
units.radian(1).to(units.percent) # raises ConversionNotFound
|
|
220
230
|
```
|
|
221
231
|
|
|
222
|
-
Uncertainty
|
|
232
|
+
### Uncertainty Propagation
|
|
233
|
+
|
|
223
234
|
```python
|
|
224
235
|
from ucon import units
|
|
225
236
|
|
|
226
|
-
# Measurements with uncertainty
|
|
227
237
|
length = units.meter(1.234, uncertainty=0.005)
|
|
228
238
|
width = units.meter(0.567, uncertainty=0.003)
|
|
229
239
|
|
|
230
240
|
print(length) # <1.234 ± 0.005 m>
|
|
231
241
|
|
|
232
|
-
#
|
|
242
|
+
# Propagates through arithmetic (quadrature)
|
|
233
243
|
area = length * width
|
|
234
244
|
print(area) # <0.699678 ± 0.00424... m²>
|
|
235
245
|
|
|
236
|
-
#
|
|
246
|
+
# Propagates through conversion
|
|
237
247
|
length_ft = length.to(units.foot)
|
|
238
248
|
print(length_ft) # <4.048... ± 0.0164... ft>
|
|
239
249
|
```
|
|
240
250
|
|
|
241
|
-
|
|
242
|
-
This goes beyond simple unit conversion (meter → foot) into structural transformation:
|
|
251
|
+
### Pydantic Integration
|
|
243
252
|
|
|
244
253
|
```python
|
|
245
|
-
from
|
|
246
|
-
from ucon import
|
|
247
|
-
from ucon
|
|
248
|
-
from ucon.maps import LinearMap
|
|
249
|
-
|
|
250
|
-
# The realm of Valdris has three fundamental dimensions:
|
|
251
|
-
# - Aether (A): magical energy substrate
|
|
252
|
-
# - Resonance (R): vibrational frequency of magic
|
|
253
|
-
# - Substance (S): physical matter
|
|
254
|
-
#
|
|
255
|
-
# These combine into SI dimensions via a transformation matrix:
|
|
256
|
-
#
|
|
257
|
-
# | L | | 2 0 0 | | A |
|
|
258
|
-
# | M | = | 1 0 1 | × | R |
|
|
259
|
-
# | T | |-2 -1 0 | | S |
|
|
260
|
-
#
|
|
261
|
-
# Reading the columns:
|
|
262
|
-
# - 1 aether contributes: L², M, T⁻² (energy-like)
|
|
263
|
-
# - 1 resonance contributes: T⁻¹ (frequency-like)
|
|
264
|
-
# - 1 substance contributes: M (mass-like)
|
|
265
|
-
|
|
266
|
-
# Fantasy base units
|
|
267
|
-
mote = Unit(name='mote', dimension=Dimension.energy, aliases=('mt',))
|
|
268
|
-
chime = Unit(name='chime', dimension=Dimension.frequency, aliases=('ch',))
|
|
269
|
-
ite = Unit(name='ite', dimension=Dimension.mass, aliases=('it',))
|
|
270
|
-
|
|
271
|
-
valdris = UnitSystem(
|
|
272
|
-
name="Valdris",
|
|
273
|
-
bases={
|
|
274
|
-
Dimension.energy: mote,
|
|
275
|
-
Dimension.frequency: chime,
|
|
276
|
-
Dimension.mass: ite,
|
|
277
|
-
}
|
|
278
|
-
)
|
|
254
|
+
from pydantic import BaseModel
|
|
255
|
+
from ucon.pydantic import Number
|
|
256
|
+
from ucon import units
|
|
279
257
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
src=valdris,
|
|
283
|
-
dst=units.si,
|
|
284
|
-
src_dimensions=(Dimension.energy, Dimension.frequency, Dimension.mass),
|
|
285
|
-
dst_dimensions=(Dimension.energy, Dimension.frequency, Dimension.mass),
|
|
286
|
-
matrix=(
|
|
287
|
-
(2, 0, 0), # energy: 2 × aether
|
|
288
|
-
(1, 0, 1), # frequency: aether + substance
|
|
289
|
-
(-2, -1, 0), # mass: -2×aether - resonance
|
|
290
|
-
),
|
|
291
|
-
)
|
|
258
|
+
class Measurement(BaseModel):
|
|
259
|
+
value: Number
|
|
292
260
|
|
|
293
|
-
#
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
basis_transform=valdris_to_si,
|
|
297
|
-
edges={
|
|
298
|
-
(mote, units.joule): LinearMap(42), # 1 mote = 42 J
|
|
299
|
-
(chime, units.hertz): LinearMap(7), # 1 chime = 7 Hz
|
|
300
|
-
(ite, units.kilogram): LinearMap(Fraction(1, 2)), # 1 ite = 0.5 kg
|
|
301
|
-
}
|
|
302
|
-
)
|
|
261
|
+
# From JSON/dict input
|
|
262
|
+
m = Measurement(value={"quantity": 5, "unit": "km"})
|
|
263
|
+
print(m.value) # <5 km>
|
|
303
264
|
|
|
304
|
-
#
|
|
305
|
-
|
|
306
|
-
energy_map(10) # 420 joules from 10 motes
|
|
265
|
+
# From Number instance
|
|
266
|
+
m2 = Measurement(value=units.meter(10))
|
|
307
267
|
|
|
308
|
-
#
|
|
309
|
-
|
|
310
|
-
|
|
268
|
+
# Serialize to JSON
|
|
269
|
+
print(m.model_dump_json())
|
|
270
|
+
# {"value": {"quantity": 5.0, "unit": "km", "uncertainty": null}}
|
|
311
271
|
|
|
312
|
-
#
|
|
313
|
-
|
|
272
|
+
# Supports both Unicode and ASCII unit notation
|
|
273
|
+
m3 = Measurement(value={"quantity": 9.8, "unit": "m/s^2"}) # ASCII
|
|
274
|
+
m4 = Measurement(value={"quantity": 9.8, "unit": "m/s²"}) # Unicode
|
|
314
275
|
```
|
|
315
276
|
|
|
316
|
-
|
|
277
|
+
**Design notes:**
|
|
278
|
+
- **Serialization format**: Units serialize as human-readable shorthand strings (`"km"`, `"m/s^2"`) rather than structured dicts, aligning with how scientists express units.
|
|
279
|
+
- **Parsing priority**: When parsing `"kg"`, ucon returns `Scale.kilo * gram` rather than looking up a `kilogram` Unit, ensuring consistent round-trip serialization and avoiding redundant unit definitions.
|
|
280
|
+
|
|
281
|
+
### Custom Unit Systems
|
|
282
|
+
|
|
283
|
+
`BasisTransform` enables conversions between incompatible dimensional structures (e.g., fantasy game physics, CGS units, domain-specific systems).
|
|
284
|
+
|
|
285
|
+
See full example: [docs/examples/basis-transform-fantasy-units.md](./docs/examples/basis-transform-fantasy-units.md)
|
|
317
286
|
|
|
318
287
|
---
|
|
319
288
|
|
|
@@ -326,7 +295,7 @@ This enables fantasy game physics, or any field where the dimensional structure
|
|
|
326
295
|
| **0.5.0** | Dimensionless Units | Pseudo-dimensions for angle, solid angle, ratio | ✅ Complete |
|
|
327
296
|
| **0.5.x** | Uncertainty | Propagation through arithmetic and conversions | ✅ Complete |
|
|
328
297
|
| **0.5.x** | Unit Systems | `BasisTransform`, `UnitSystem`, cross-basis conversion | ✅ Complete |
|
|
329
|
-
| **0.6.x** | Pydantic Integration | Type-safe quantity validation |
|
|
298
|
+
| **0.6.x** | Pydantic Integration | Type-safe quantity validation, JSON serialization | ✅ Complete |
|
|
330
299
|
| **0.7.x** | NumPy Arrays | Vectorized conversion and arithmetic | ⏳ Planned |
|
|
331
300
|
|
|
332
301
|
See full roadmap: [ROADMAP.md](./ROADMAP.md)
|
|
@@ -336,14 +305,21 @@ See full roadmap: [ROADMAP.md](./ROADMAP.md)
|
|
|
336
305
|
## Contributing
|
|
337
306
|
|
|
338
307
|
Contributions, issues, and pull requests are welcome!
|
|
339
|
-
|
|
308
|
+
|
|
309
|
+
Set up your development environment:
|
|
310
|
+
```bash
|
|
311
|
+
make venv
|
|
312
|
+
source .ucon-3.12/bin/activate
|
|
340
313
|
```
|
|
341
|
-
|
|
314
|
+
|
|
315
|
+
Run the test suite before committing:
|
|
316
|
+
```bash
|
|
317
|
+
make test
|
|
342
318
|
```
|
|
343
|
-
Then run the full test suite (against all supported python versions) before committing:
|
|
344
319
|
|
|
320
|
+
Run tests across all supported Python versions:
|
|
345
321
|
```bash
|
|
346
|
-
|
|
322
|
+
make test-all
|
|
347
323
|
```
|
|
348
324
|
---
|
|
349
325
|
|