{"id":27913719,"url":"https://github.com/aroefer/kv_lite","last_synced_at":"2026-04-29T01:32:15.395Z","repository":{"id":236849384,"uuid":"635198735","full_name":"ARoefer/kv_lite","owner":"ARoefer","description":"Version 2.0 of Kineverse, a framework for modeling kinematics for robotic manipulation and control. ","archived":false,"fork":false,"pushed_at":"2026-04-07T13:26:47.000Z","size":376,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":6,"default_branch":"main","last_synced_at":"2026-04-07T15:26:21.006Z","etag":null,"topics":["autodiff","casadi","kinematics-model","numpy","robotic-manipulation"],"latest_commit_sha":null,"homepage":"","language":"Jupyter Notebook","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ARoefer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-05-02T07:17:19.000Z","updated_at":"2026-04-07T13:26:51.000Z","dependencies_parsed_at":null,"dependency_job_id":"9c189b52-4dc2-4c79-b6ce-43743eca65ce","html_url":"https://github.com/ARoefer/kv_lite","commit_stats":null,"previous_names":["aroefer/kv_lite"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ARoefer/kv_lite","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ARoefer%2Fkv_lite","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ARoefer%2Fkv_lite/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ARoefer%2Fkv_lite/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ARoefer%2Fkv_lite/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ARoefer","download_url":"https://codeload.github.com/ARoefer/kv_lite/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ARoefer%2Fkv_lite/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32407164,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-28T19:38:08.556Z","status":"ssl_error","status_checked_at":"2026-04-28T19:37:55.688Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["autodiff","casadi","kinematics-model","numpy","robotic-manipulation"],"created_at":"2025-05-06T14:43:13.023Z","updated_at":"2026-04-29T01:32:15.377Z","avatar_url":"https://github.com/ARoefer.png","language":"Jupyter Notebook","funding_links":[],"categories":[],"sub_categories":[],"readme":"# KV-Lite - Kineverse 2.0\n\nThis package is the second attempt at implementing the *Kineverse* articulation framework.\n\n - [Original Website](http://kineverse.cs.uni-freiburg.de/)\n - [Paper](https://arxiv.org/abs/2012.05362)\n\n## Installation\n\nThe package is implemented as a ROS-package as it is mostly meant for the use in the robotics context, however, as of now (20th June 2023), it does not have a hard ROS dependency.\n\nClone the repository - preferably to a ROS workspace - and install the requirements using `pip`:\n\n```bash\n# In kv_lite\npip install -r requirements.txt\n```\n\nIn the case of ROS, build your workspace, reload it and you're good to go.\n\n### Additional Dependencies\n\nSome of the examples require additional packages. Currently it is\n\n - [roebots](https://github.com/ARoefer/roebots) a package collecting some utility functions for working with robotics. Used here for resolving ROS package paths without a running ROS core.\n\n - [prime_bullet](https://github.com/ARoefer/prime_bullet) an object-oriented wrapper for PyBullet which tries to offer a game-engine like interaction with the physics simulator.\n\n\n## Usage\n\n### Preamble\n\n*KV-lite* is a framework for building and managing kinematics of articulated objects (doors, drawers, robots, general mechanisms). The aim is to be more flexible in the range of possible articulations than frameworks such as URDF, or SDF. These structures are managed as graphs, which can be compiled into analytical, differentiable forward-kinematic expressions. The expressions can be queried for their arguments, often also referred to as *(free) symbols*, for which a KV-lite model can hold constraints. In the end, these constraints and expressions can be used in any algorithm.\n\nThe general promise of KV-lite/Kineverse is: *If there is a closed-form expression for your articulation, you can use it.*\n\n### Concepts\n\nBefore going to full models of articulated structures, we need to take a look at KV-lite's basic building blocks. Fundamentally, there are only four things:\n\n - Symbols: The variables of expressions. Implemented as `KVSymbol`.\n - Expressions: Some mathematical expression. Implemented as `KVExpr`.\n - Matrices: A structured arrangement of expressions. Implemented as `KVArray` as a specialization of `numpy`'s arrays.\n - Constraints: Triples of an expressions `lb, ub, e` which expresses the constraint `lb \u003c= e \u003c= ub`. Implemented simply as `Constraint`.\n\nLet us look at an example of using these:\n\n```python\nimport kv_lite as kv\n\n# Creating our first symbol\na = kv.symbol('a')\n\n# We can use a normally to build up symbolic expressions\ne1 = a * 4\nprint(e1)  # \u003e\u003e (4*a)\n\n# We have to use the symbolic functions from gm\ne2 = kv.cos(e1)\nprint(e2)  # \u003e\u003e cos((4*a))\n\n# We can inspect our expressions\nprint(e2.is_symbolic)  # \u003e\u003e True\nprint(e2.symbols)      # \u003e\u003e frozenset({KV(a)})\n\n# Constant expressions also exist\ne_constant = kv.expr(4) * kv.expr(5)\nprint(e_constant)              # \u003e\u003e KV(20)\nprint(e_constant.is_symbolic)  # \u003e\u003e False\nprint(e_constant.symbols)      # \u003e\u003e frozenset()\n# Non-symbolic expressions can be turned directly into float\nprint(float(e_constant))       # \u003e\u003e 20.0\n\n# We can evaluate expressions by assigning values to their symbols\nprint(e1.eval({a: 3}))   # \u003e\u003e 12.0\nprint(e2.eval({a: 2}))   # \u003e\u003e -0.1455...\nprint(e_constant.eval()) # \u003e\u003e 20.0\n\n# Note two things:\n#  1. All expressions always evaluate to float\n#  2. eval() only filters for expected variables:\n\nprint(e1.eval({a: 3, kv.Symbol('b'): 2}))  # \u003e\u003e 12.0\n\n# We can easily generate the jacobian of an expression\nprint(e1.jacobian([a]))  # \u003e\u003e [[KV(4)]]\nprint(e2.jacobian([a]))  # \u003e\u003e [[KV(@1=4, (-(@1*sin((@1*a)))))]]\n```\n\nIn the last stages of the code above, we generate instances of `KVArray`. As stated before, this is the array implementation of KV-lite. It is a small extension of `numpy`'s `ndarray` type and thus supports all typical numpy array operations, such as indexing, slicing, stacking, and whatever else. All functions provided in KV-lite are vectorized, meaning that they broadcast across arrays. *Careful*: When given a container, KV-lite operations will **always** return a `KVArray`. Let us look at a couple of examples of using `KVArray`:\n\n```python\nimport kv_lite as kv\n\n# Let's create a couple more symbols for ourselves\na, b, c, d = [kv.Symbol(x) for x in 'abcd']\n\n# CAREFUL: The left-hand side determines the array type, thus we create a KVArray here\nm = kv.diag([1, 2, 3]) # Equivalent to kv.array(np.diag([1, 2, 3]))\nv = kv.array([a, b, c]).reshape((3, 1))\n\nr = m.dot(v)  # @ operator would work too\nprint(r) \n# [[KV(a)]\n#  [KV((2*b))]\n#  [KV((3*c))], dtype=object]\n\n# KVArray's offer the same introspection as expressions\nprint(r.is_symbolic)  # \u003e\u003e True\nprint(r.symbols)      # \u003e\u003e frozenset({KV(c), KV(b), KV(a)})\nprint(r.jacobian([a, b, c]))\n# [[KV(1), KV(0), KV(0)]\n#  [KV(0), KV(2), KV(0)]\n#  [KV(0), KV(0), KV(3)]]\n\nj_r = r.jacobian([a, b, c])\n\n# Typical numpy indexing works as expected\nprint(j_r.T)\n# [[KV(1) KV(0) KV(0)]\n#  [KV(0) KV(2) KV(0)]\n#  [KV(0) KV(0) KV(3)]]\nprint(j_r[ 1,   0])\n# 0\nprint(j_r[ 0])\n# [KV(1) KV(0) KV(0)]\nprint(j_r[ :,   2])\n# [KV(0) KV(0) KV(3)]\nprint(j_r[:2, 2:3])\n# [[KV(0)]\n#  [KV(0)]]\n\n# Jacobian wrt foreign symbols is 0\nprint(r.jacobian([d]))\n\n```\n\n### Transformations \u0026 Spatial Types\n\nCreating arbitrary matrices is fine and dandy, but also offers a growing set of implementations for typical spatial entities. By default, KV-lite uses 4x4 homogenous transforms as base representation for entities from SO(3) and SE(3). Let us look at a quick run-down of the options\n\n```python\nimport numpy as np\nimport kv_lite as kv\n\n# Create a homogeneous vector -\u003e Not affected by translation\nv = kv.vector3(1, 2, 3)\nprint(v)\n# [[1]\n#  [2]\n#  [3]\n#  [0]]\n\n# L2 norm of a vector\nprint(kv.norm(v))  # \u003e\u003e 3.74165738...\n\n# Create a homogeneous point -\u003e Affected by translation and rotation\np = kv.point3(1, 2, 3)\nprint(p)\n# [[1]\n#  [2]\n#  [3]\n#  [1]]\n\n# Create an identity transform\nidentity = kv.Transform.identity()\n\n# A pure linear translation along the x axis by two meters\ntrans1 = kv.Transform.from_xyz(2, 0, 0)\n\n# A 90 degree rotation around the Y-axis\nrot1   = kv.Transform.from_euler(0, 0, np.deg2rad(90))\n\n# A 45 degree rotation around v\nrot2   = kv.Transform.from_axis_angle(v / kv.norm(v), np.deg2rad(45))\n\n# An identity rotation from a quaternion\nrot3   = kv.Transform.from_quat(0, 0, 0, 1)\n\n# A combined rotation and translation\n# The same exists for from_xyz_aa, from_xyz_quat\ntf1    = kv.Transform.from_xyz_euler(2, 0, 0, 0, 0, np.deg2rad(90))\n\n# Use Transform.inverse to invert homogeneous transformations\ntf1_inv = kv.Transform.inverse(tf1)\n\n# kv.Transform also provides some structured introspection into transforms\nrot4   = kv.Transform.rot(tf1_inv)   # Generate a pure rotation transform from tf1_inv\ntrans2 = kv.Transform.trans(tf1_inv) # Generate a pure translation transform from tf1_inv\n\ntf1_inv_x = kv.Transform.x(tf1_inv)  # X-column of transform\ntf1_inv_y = kv.Transform.y(tf1_inv)  # Y-column of transform\ntf1_inv_z = kv.Transform.z(tf1_inv)  # Z-column of transform\ntf1_inv_w = kv.Transform.w(tf1_inv)  # W-column of transform - identical to kv.Transform.pos(tf1_inv)\n\n# Of course, transform creation also works with symbols!\na, b = [kv.Symbol(x) for x in 'ab']\n\n# A transform translating by 2a along X and rotating by b around Z\ntf2  = kv.Transform.from_xyz_euler(2*a, 0, 0, 0, b, 0)\nprint(tf2)\n# [[KV(cos(b)) KV(0) KV(sin(b)) KV((2*a))]\n#  [KV(0) KV(1) KV(0) KV(0)]\n#  [KV((-sin(b))) KV(0) KV(cos(b)) KV(0)]\n#  [KV(0) KV(0) KV(0) KV(1)]]\n\n# Transformations are chained by matrix multiplication\ntf3 = tf1.dot(tf2)  # @ would work as well\n```\n\n### Symbol Typing\n\nSo far, we have always created all symbols using `KVSymbol`. This is fine for a general use of KV-lite and its symbolic math functionality, but to use its full modelling capabilities we must understand symbol typing. In KV-lite, it is possible to create symbols of certain types: Positions, Velocities, Accelerations, Jerks, and Snaps. These symbols behave normally, but have the additional feature, that they can be differentiated and integrated. Using this system, it is possible to create different constraints for the position, velocity, etc. of one degree of freedom of a model. Additionally, we can generate the tangent expression of an expression, which models the rate of change of the function at a given point, given the derivatives of it's symbols.\nLet's make this topic a bit more approachable:\n\n```python\nimport kv_lite as kv\n\n# We create a few symbols modelling the position of degrees of freedom a, b, c\na, b, c = [kv.Position(x) for x in 'abc']\nprint(a, b, c)  # \u003e\u003e a__position b__position c__position\n\n# We can generate the symbols referencing the derivative and the integral of a typed symbol\nprint(a.derivative())             # \u003e\u003e a__velocity\nprint(a.derivative().integral())  # \u003e\u003e a__position\n\n# We cannot integrate beyond position or differentiate beyond snap\ntry:\n    a.integral()\nexcept RuntimeError as e:\n    print(e)  # \u003e\u003e Cannot integrate symbol beyond position.\n\n# Let us create an expression\ne1 = a * 4 -b\nprint(e1)  # \u003e\u003e ((4*a__position)-b__position)\n\n# We can now not just generate the jacobian wrt a__position,\n# but also generate the tangent expression\nprint(e1.tangent())  # \u003e\u003e ((4*a__velocity)-b__velocity)\n\n# Note: A typed symbol's tangent is the symbol of its derivation\nprint(a.tangent())  # \u003e\u003e a__velocity\n```\n\nAnalogously to `kv.Position`, there are also `kv.Velocity`, `kv.Acceleration`, `kv.Jerk`, and `kv.Snap`.\n\n### Models\n\nAll the mathematical tools we have seen before are used to build models of articulated structures. Models are represented by the `Model` class. KV-lite represents articulated structures as acyclic directed forest graph. The nodes in this graph define *frames* and are connected by edges, which represent the *transformations* between these frames. The graph can be queried for a frame w.r.t. another frame, which is calculated by traversing the graph and aggregating the transformations represented by the edges. If you are familiar with ROS' TF-tree, none of this will be new to you.\nLastly, the model also holds the constraints for the symbols used in describing the transformations. Constraints can be added manually, but generally edges can also define them so that they are automatically added or removed with the edge. The model can be queried for the constraints relevant to a given (set) of symbols.\n\nEnough theory, let us look at a few examples:\n\n```python\nimport kv_lite as kv\nimport numpy   as np\n\n# km -\u003e kinematic model\nkm = kv.Model()\n\n# The frame \"world\" is always defined\n# get_fk() returns a \"FrameView\" which holds the frame data \n# as well as the specified transformation\nw_T_w = km.get_fk('world', 'world')\nprint(w_T_w)\n# T (world -\u003e world):\n# [[1. 0. 0. 0.]\n#  [0. 1. 0. 0.]\n#  [0. 0. 1. 0.]\n#  [0. 0. 0. 1.]]\n\n# Name of the frame\nprint(w_T_w.name)       # \u003e\u003e world\n# Name of the reference frame of the transform\nprint(w_T_w.reference)  # \u003e\u003e world\n# Datatype of the frame\nprint(w_T_w.dtype)      # \u003e\u003e \u003cclass 'kv_lite.graph.Frame'\u003e\n# Original frame's data\nprint(w_T_w.frame)      # \u003e\u003e Frame(name='world')\n# Homogeneous transformation\nprint(w_T_w.transform)\n# [[1. 0. 0. 0.]\n#  [0. 1. 0. 0.]\n#  [0. 0. 1. 0.]\n#  [0. 0. 0. 1.]]\n\n# Adding a new frame with no additional data\nkm.add_frame(kv.Frame('lol'))\n\ntry:\n    lol_T_w = km.get_fk('lol')   # By default we look up everything to \"world\"\nexcept kv.FKChainException as e: # When no path is found, an exception is raised\n    print(e)\n\n# Let us create some symbols for different degrees of freedom\na, b, c = [kv.Position(x) for x in 'abc']\n\n# Adding an edge connecting \"lol\" to \"world\" with a simple translation along X\nkm.add_edge(kv.TransformEdge('world', 'lol', kv.Transform.from_xyz(a + 1, 0, 0)))\n\n# Now we can look up the forward kinematic of \"lol\" to \"world\"\nlol_T_w = km.get_fk('lol')\nprint(lol_T_w)\n# T (lol -\u003e world):\n# [[KV(1) KV(0) KV(0) KV((a__position+1))]\n#  [0.0 1.0 0.0 0.0]\n#  [0.0 0.0 1.0 0.0]\n#  [0.0 0.0 0.0 1.0]]\n\n# We can of course also look up the inverse:\nw_T_lol = km.get_fk('world', 'lol')\nprint(w_T_lol)\n# [[KV(1) 0.0 0.0 KV(-(a__position+1))]\n#  [KV(0) 1.0 0.0 KV(0)]\n#  [KV(0) 0.0 1.0 KV(0)]\n#  [0.0 0.0 0.0 1.0]]\n\n# Adding another frame\nkm.add_frame(kv.Frame('foo'))\n\n# Add an edge connecting \"foo\" to \"lol\", rotating it around lol's Y axis at a distance of 1 meter\n# Constraints need some name to identify them uniquely\nkm.add_edge(kv.ConstrainedTransformEdge('lol', 'foo', kv.Transform.from_euler(0, b, 0).dot(kv.Transform.from_xyz(0, 0, 1)),\n                                        {'limit position b': kv.Constraint(np.deg2rad(-45), np.deg2rad(-45), b)}))\n\n# Getting the FK of \"foo\" in \"world\"\nfoo_T_w = km.get_fk('foo')\nprint(foo_T_w)\n# T (foo -\u003e world):\n# [[KV(cos(b__position)) KV(0) KV(sin(b__position)) KV((sin(b__position)+(a__position+1)))]\n#  [KV(0) KV(1) KV(0) KV(0)]\n#  [KV((-sin(b__position))) KV(0) KV(cos(b__position)) KV(cos(b__position))]\n#  [KV(0) KV(0) KV(0) KV(1)]]\n\n# We can now use the constraint query feature\nprint(km.get_constraints(foo_T_w.transform.symbols))\n# {'limit position b': C(-0.7853981633974483 \u003c= b__position \u003c= -0.7853981633974483)}\n```\n\n### URDF\n\nManual model building is good to understand, but typically you will already have articulated structures specified as URDF that you want to load. KV-lite offers a few extension modules of the core functionality. One of these is for loading URDF files. The process is simple, but for this example we need the data from [prime_bullet](https://github.com/ARoefer/prime_bullet).\n\n```python\nimport kv_lite      as kv\nimport prime_bullet as pb\n\nfrom pathlib import Path\n\n# Create an empty model\nkm = kv.Model()\n\nwith open(pb.res_pkg_path('package://prime_bullet/urdf/windmill.urdf')) as f:\n    # Load the URDF into the model\n    windmill = kv.urdf.load_urdf(km, f.read())\n\n# \"windmill\" is a URDF-style interface to the model\nprint(windmill.links)      # Print the links in the model\nprint(windmill.joints)     # Print the joints in the model\nprint(windmill.q)          # Print position symbols\nprint(windmill.q_dot)      # Print velocity symbols\nprint(windmill.root_link)  # Local name of root frame\n\n# Note that the URDF interface can look up FKs using the local link names\n# It will try the local lookup first, then try to resolve names globally\nwings_T_root = windmill.get_fk('wings', 'base')\n\ntry:\n    wings_T_w = windmill.get_fk('wings')  # The default reference frame is still \"world\"\nexcept kv.FKChainException as e:          # There is no connection between \"windmill/base\" and \"world\"\n    print(e)\n\n# Add a static transform between the base of the windmill and \"world\"\nkm.add_edge(kv.TransformEdge('world', windmill.root, kv.Transform.from_xyz(1, 0, 0)))\n\n# Now the lookup works\nwings_T_w = windmill.get_fk('wings')\n```\n\n### Exponential Coordinates\n\nThe current version of KV-lite includes an implementation of SO3/SE3 exponential maps transformations. This is just a brief overview of the existing functionality:\n\n```python\nimport kv_lite as kv\n\n# Generate an exponential map transform\ntf = kv.exp.twist_to_se3(kv.vector3(1, 0, 0), \n                         kv.vector3(0, 0, 1),\n                         kv.Position('q'))\n\nprint(tf)\n\n# Create a model\nkm = kv.Model()\nkm.add_frame('foo')\n\n# We can also use the transform as an edge\nkm.add_edge(kv.exp.TwistJointEdge(kv.vector3(1, 0, 0),\n                                  kv.vector3(0, 0, 1),\n                                  kv.Position('q')))\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faroefer%2Fkv_lite","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faroefer%2Fkv_lite","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faroefer%2Fkv_lite/lists"}