{ "cells": [ { "cell_type": "markdown", "id": "6a76ab59-b8e7-41f4-a8e4-45eb0ac533c7", "metadata": {}, "source": [ "# Environments\n", "\n", "## Definitions:\n", "* \"derived unit\": a unit in the SI system that is a compound of the SI base units (e.g. N, J, W, Hz)\n", "* \"defined unit\": a non-SI unit whose definition comes from an SI unit, e.g. an inch [is legally defined as 25.4 mm](https://en.wikipedia.org/wiki/Inch) and a lb is [approximately equal to 4.448222 N](https://en.wikipedia.org/wiki/Pound_(force))\n", "* \"environment object\": describes the singleton `Environment` class (see below)\n", "* \"environment\" will be used to describe a Python namespace that is populated with variables of `Physical` instances\n", "* \"environment file\" describes a .json file where the unit definitions reside\n", "\n", "`forallpeople` is an SI units library that allows other non-SI units to be defined and used. The definitions for the **derived** SI units (e.g. N, Pa, W, J, etc.) and the definitions for the **defined** non-SI units (e.g. lb, ft, psf) are all located in an **environment file**.\n", "\n", "An environment object within `forallpeople` is a singleton instance of the `forallpeople.environment.Environment` class. Its purpose is to read the definitions of the units in the environment file and to populate the module namespace with those definitions as variables that are ready to be used for computation. It is not intended to be interacted with directly but instead through the `forallpeople.environment` function." ] }, { "cell_type": "markdown", "id": "bfffd501-ba24-4359-8eea-087e1c4d7649", "metadata": {}, "source": [ "## Loading Environments\n", "\n", "Use the `environment` function to load an environment JSON file. The JSON files are located in the package in the \"environments\" directory. i.e. `path/to/your/python/environment/site-packages/forallpeople/environments` when installed on your system.\n", "\n", "> Currently, this is the only location searched for environment files. This will likely change in a future update to allow environment files to be loaded from user-defined locations.\n", ">\n", "> The location of your current `forallpeople` installation can generally be found by importing the module and then using the `.__file__` attribute of the imported module's variable name." ] }, { "cell_type": "code", "execution_count": 1, "id": "922300e9-1d79-49aa-a7e2-b67b54c64ccc", "metadata": {}, "outputs": [], "source": [ "import forallpeople as si\n", "si.environment('default')" ] }, { "cell_type": "markdown", "id": "2e1233d4-c92b-4ec2-b013-c7eeff0055c6", "metadata": {}, "source": [ "When the above code is run, the environment file is parsed and all of the units listed in that file are loaded in the `forallpeople` module namespace which, in this example, is under the prefix `si.`" ] }, { "cell_type": "markdown", "id": "d87a623a-166c-40ad-be08-df5a96cf5b14", "metadata": {}, "source": [ "### Bundled environments\n", "\n", "`forallpeople` comes with the following bundled environments:\n", "* `default`: Defines the SI derived units\n", "* `us_customary`: Defines the most common units within the US Customary units system\n", "* `structural`: Defines a series of units in both SI and US Customary units systems that are commonly used in the field of structural engineering (my field of practice) in Canada.\n", "* `electrical`: My attempt at creating something useful for electrical engineers\n", "* `thermal`: My attempt at creating something useful for building envelope engineers\n", "* `test_definitions`: Used for the internal test suite. Not intended for daily use." ] }, { "cell_type": "markdown", "id": "593753a2-cbf4-4e12-86b9-8e129bc7481a", "metadata": {}, "source": [ "### Inspecting environments\n", "\n", "To see what units have been loaded as variables, run the `environment` function with no arguments." ] }, { "cell_type": "code", "execution_count": 2, "id": "e3bc32ac", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'Hz': 1.000 Hz, 'N': 1.000 N, 'Pa': 1.000 Pa, 'J': 1.000 J, 'W': 1.000 W, 'C': 1.000 C, 'V': 1.000 V, 'F': 1.000 F, 'Ohm': 1.000 Ω, 'S': 1.000 S, 'Wb': 1.000 Wb, 'T': 1.000 T, 'H': 1.000 H, 'Celsius': 1.000 °C, 'lux': 1.000 lux, 'Gy': 1.000 Gy, 'katal': 1.000 kat, 'minute': 1.000 minutes, 'hour': 1.000 hours, 'day': 1.000 days} \n", " {'kg': 1.000 kg, 'm': 1.000 m, 's': 1.000 s, 'A': 1.000 A, 'cd': 1.000 cd, 'K': 1.000 °C, 'mol': 1.000 mol}\n" ] } ], "source": [ "si.environment()" ] }, { "cell_type": "markdown", "id": "ccc3aa83", "metadata": {}, "source": [ "This prints the contents of two dictionaries:\n", "1. The dictionary of all the units that are defined in the environment file.\n", "2. The dictionary of the SI base units which are always present when importing `forallpeople`\n", "\n", "The keys of the dictionaries are the identifiers (variable names) used in namespace. So, `si.Hz` and `si.N`. The values of the dictionaries are the actual `Physical` instances." ] }, { "cell_type": "markdown", "id": "f6b94614-7bf7-4e6a-8c58-288da1c30965", "metadata": {}, "source": [ "## Environment files\n", "\n", "An environemnt file is a JSON file which typically looks like this (excerpted from the included environment, \"structural.json\"):\n", "\n", "```json\n", "{\n", " \"mm\": {\n", " \"Dimension\": [0,1,0,0,0,0,0],\n", " \"Value\": 0.001},\n", " \"ft\": {\n", " \"Dimension\": [0,1,0,0,0,0,0],\n", " \"Symbol\": \"ft\",\n", " \"Factor\": \"1/0.3048\"},\n", " \"inch\": {\n", " \"Dimension\": [0,1,0,0,0,0,0],\n", " \"Symbol\": \"inch\",\n", " \"Factor\": \"12/0.3048\"},\n", " \"N\": {\n", " \"Dimension\": [1,1,-2,0,0,0,0]},\n", " \"kN\": {\n", " \"Dimension\": [1,1,-2,0,0,0,0],\n", " \"Value\": 1000},\n", " \"MN\": {\n", " \"Dimension\": [1,1,-2,0,0,0,0],\n", " \"Value\": 1000000},\n", " \n", " // ...additional entries here\n", "}\n", "```\n", "\n", "A single entry consists of the following schema:\n", "\n", "> Note, in this example `//` are used to indicate \"comments\" which are not valid in JSON so this example is not directly \"copy-pastable\" into an environment file.\n", "\n", "```json\n", "\"identifier\": {\n", " \"Dimension\": [0, 0, 0, 0, 0, 0, 0], // req, an integer array of length 7\n", " \"Symbol\": \"str\", // opt, if not given, the identifier is used as the symbol\n", " \"Factor\": 1, // opt, int | float | str, if not given, a default value of 1.0 is assumed\n", " \"Value\": 1, // opt, int | float, if not given, a default value of 1.0 is assumed\n", " \"Default\": false, // opt, bool, if not given, a default value of false is assumed\n", "}\n", "```\n", "* Identifier: this is must be a valid Python identifier because it will be used as the variable name to which this quantity is assigned to when the environment file is loaded into the environment. The identifier will also be the display symbol if an alternate symbol string is not provided.\n", "* Dimension: describes a vector of SI base unit components **in the following order**: kg, m, s, A, cd, K, mol\n", "* Symbol: an alternate string to use instead of the identifier\n", "* Factor: a value that will be **multiplied by** to scale the SI base unit so to \"convert\" the SI base unit quantity into an equivalent amount of this new unit. The factor is commonly represented as a string representing an arithmetic expression (e.g. `\"1/0.45359237/9.80665/0.3048\"`) but can also be an `int` or `float`. If an arithmetic string is provided, the string will be evaulated by converting each value first into a `fractions.Fraction` and then performing the operations described (`eval` is _not_ used under-the-hood).\n", "* Default: if `True` (or `true` in JSON), then this unit will be the default displayed unit for any `Physical` instance that has matching Dimension and a Factor != 1.\n", "\n", "> Note: By design, default values (currently) ONLY apply to defined units (units that have a factor != 1). The motivation for enabling a default unit is to have a preferred fallback when a calculation using **defined** units results in a `.factor` attribute that does not have a match in the environment. In this case, having a representation suddenly switch to SI units is surprising.\n", ">\n", "> To avoid this surprise, a unit definition can be specified as the default so a calculation using lbs can be multiplied/divided by many other values and, no matter how the `.factor` attribute gets mangled, the result can be specified in lbs (as long as the dimensions are consistent with the dimensions of lbs)." ] }, { "cell_type": "markdown", "id": "d1330e33-02bd-487c-b592-f41ac0e396d1", "metadata": {}, "source": [ "### Customizing your environments (making new environment files)\n", "\n", "The recommended way of creating a new environment is to copy one of the existing environments, give it a new name, and begin customizing it by adding, removing, or modifying existing definitions. I recommend comparing and contrasting the \"default.json\" (which only defines **derived** units and has very few extra attributes in each definition) to the \"structural.json\" (which contains many **defined** units in the US Customary system...[because I practice engineering in Canada]).\n", "\n", "\n", "Things to keep in mind:\n", "\n", "- The factor expressions used for US customary units (in \"structural.json\" and \"us_customary.json\") are generally based off of the legal definitions of their components (meaning that these values are _exact_ values). So the conversion from a kg to an international avoirdupois pound is exactly 0.45359237, which is a number you will see in the environment files that have any mass components being scaled into a US equivalent. You can find these legal definitions on Wikipedia (e.g. see above example in \"Definitions\") but they are all based off of these two definitions: https://en.wikipedia.org/wiki/International_yard_and_pound\n", "- Adding a \"Default\" attribute will only be meaningful when applied to **defined** units\n", "- JSON does not allow you to have trailing commas (whereas Python does) - Watch out for that if you have invalid JSON as a result of copy-pasting definitions from the middle of the file to the end of the file.\n", "- JSON does not allow you to use comments so do not try to put any in\n", "- The attributes all have to be Title case. This was an early design decision that I would not make again but I am not willing to make a breaking change _just_ for that (but I will update that if there are other breaking changes that need to be made)." ] }, { "cell_type": "markdown", "id": "b52e631c", "metadata": {}, "source": [ "### Unit Resolution\n", "\n", "The representation of units (quantities) in a `forallpeople` environment are determined by three attributes:\n", "1. Dimension: the vector of a quantity's component SI base units (e.g. N has a dimension of `(1, 1, -2, 0, 0, 0, 0)` representing kg1 * m1 * s-2)\n", "2. Factor: if the unit is a **defined** unit then it will have a `.factor` attribute != 1\n", "3. Prefix: a prefix is only taken into consideration for SI base units and SI **derived** units (never for **defined** units)\n", "\n", "An entry into an environment file describes the quantity name, it's associated dimension, it's factor, and its value (prefix is auto-generated based on the value).\n", "\n", "The look-up order is as follows:\n", "1. Dimension: are there one or more definitions with a dimension that matches the current state of an instance?\n", "2. Factor: does one of the matching dimension definitions also have a matching `.factor` attribute?\n", "\n", "* If the factor and dimension are a match, it is a **defined** unit and the defined unit representation will be given priority.\n", "* If dimension matches but the factor does not match but the factor is != 1, then the **derived** definition will be used as a fallback _unless a default unit is indicated_ (see below).\n", "* If dimension matches and the factor == 1, then the **derived** representation is given priority\n", "* If there is not a dimension match, the representation will fallback to a compound string of SI base units (e.g. kg1 * m1 * s-2)\n", "\n" ] } ], "metadata": { "kernelspec": { "display_name": "forallpeople", "language": "python", "name": "forallpeople" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.4" } }, "nbformat": 4, "nbformat_minor": 5 }